@hilbras/omninode 2.0.0-alpha.1 → 2.0.0-alpha.11
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 +73 -10
- package/dist/cli/index.js +1727 -323
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1037 -113
- package/dist/index.js +1864 -438
- 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.11";
|
|
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({
|
|
@@ -108,7 +109,9 @@ var pipelineStepConfigSchema = z.object({
|
|
|
108
109
|
model: z.string().optional(),
|
|
109
110
|
depends_on: z.array(z.string()).optional(),
|
|
110
111
|
retries: z.number().int().min(0).max(10).optional(),
|
|
111
|
-
condition: z.enum(["always", "on-success", "on-failure"]).optional()
|
|
112
|
+
condition: z.enum(["always", "on-success", "on-failure"]).optional(),
|
|
113
|
+
/** Constraints passed to the planner for plan steps (§15). */
|
|
114
|
+
constraints: z.array(z.string()).optional()
|
|
112
115
|
}).strict();
|
|
113
116
|
var pipelineConfigSchema = z.object({
|
|
114
117
|
id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
|
|
@@ -132,7 +135,9 @@ var projectConfigSchema = z.object({
|
|
|
132
135
|
memory: z.object({
|
|
133
136
|
provider: z.string().min(1).default("local"),
|
|
134
137
|
base_url: z.string().url().optional(),
|
|
135
|
-
api_key_env_var: z.string().min(1).optional()
|
|
138
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
139
|
+
/** When true, memory failures fail tasks instead of being tolerated (§12). */
|
|
140
|
+
required: z.boolean().optional()
|
|
136
141
|
}).strict().optional()
|
|
137
142
|
}).strict();
|
|
138
143
|
var appConfigSchema = z.object({
|
|
@@ -214,7 +219,8 @@ function toAppConfig(yaml) {
|
|
|
214
219
|
memory: {
|
|
215
220
|
provider: yaml.project.memory.provider,
|
|
216
221
|
baseUrl: yaml.project.memory.base_url,
|
|
217
|
-
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
222
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var,
|
|
223
|
+
required: yaml.project.memory.required
|
|
218
224
|
}
|
|
219
225
|
} : {}
|
|
220
226
|
},
|
|
@@ -235,7 +241,8 @@ function toPipelineDefinition(raw) {
|
|
|
235
241
|
model: step.model,
|
|
236
242
|
dependsOn: step.depends_on,
|
|
237
243
|
retries: step.retries,
|
|
238
|
-
condition: step.condition
|
|
244
|
+
condition: step.condition,
|
|
245
|
+
constraints: step.constraints
|
|
239
246
|
}))
|
|
240
247
|
};
|
|
241
248
|
}
|
|
@@ -245,6 +252,7 @@ function toProviderConfig(raw) {
|
|
|
245
252
|
type: raw.type,
|
|
246
253
|
baseUrl: raw.base_url,
|
|
247
254
|
apiKeyEnvVar: raw.api_key_env_var,
|
|
255
|
+
timeoutMs: raw.timeout_ms,
|
|
248
256
|
headers: raw.headers,
|
|
249
257
|
enabled: raw.enabled,
|
|
250
258
|
metadata: raw.metadata
|
|
@@ -262,6 +270,11 @@ function toAgentConfig(raw) {
|
|
|
262
270
|
inputMode: raw.input_mode,
|
|
263
271
|
timeoutMs: raw.timeout_ms,
|
|
264
272
|
inheritEnv: raw.inherit_env,
|
|
273
|
+
envPolicy: raw.env_policy,
|
|
274
|
+
envAllowlist: raw.env_allowlist,
|
|
275
|
+
envDenylist: raw.env_denylist,
|
|
276
|
+
maxOutputBytes: raw.max_output_bytes,
|
|
277
|
+
allowExternalCwd: raw.allow_external_cwd,
|
|
265
278
|
metadata: raw.metadata
|
|
266
279
|
};
|
|
267
280
|
}
|
|
@@ -358,35 +371,32 @@ function authHeaders(config, env = process.env) {
|
|
|
358
371
|
}
|
|
359
372
|
|
|
360
373
|
// src/providers/http.ts
|
|
374
|
+
var HttpResponseError = class extends Error {
|
|
375
|
+
constructor(message, status, cause) {
|
|
376
|
+
super(message, { cause });
|
|
377
|
+
this.status = status;
|
|
378
|
+
this.name = "HttpResponseError";
|
|
379
|
+
}
|
|
380
|
+
status;
|
|
381
|
+
};
|
|
361
382
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
362
383
|
async function requestJson(url, options = {}) {
|
|
363
384
|
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
|
-
}
|
|
385
|
+
const response = await fetch(url, {
|
|
386
|
+
method,
|
|
387
|
+
headers: {
|
|
388
|
+
accept: "application/json",
|
|
389
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
390
|
+
...headers
|
|
391
|
+
},
|
|
392
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
393
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
394
|
+
});
|
|
382
395
|
let parsed;
|
|
383
396
|
try {
|
|
384
397
|
parsed = await response.json();
|
|
385
398
|
} catch (error) {
|
|
386
|
-
throw new
|
|
387
|
-
cause: error,
|
|
388
|
-
details: { status: response.status }
|
|
389
|
-
});
|
|
399
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
390
400
|
}
|
|
391
401
|
return { status: response.status, body: parsed };
|
|
392
402
|
}
|
|
@@ -447,24 +457,167 @@ var Logger = class _Logger {
|
|
|
447
457
|
};
|
|
448
458
|
var logger = new Logger();
|
|
449
459
|
|
|
460
|
+
// src/providers/errors.ts
|
|
461
|
+
function kindToCode(kind) {
|
|
462
|
+
switch (kind) {
|
|
463
|
+
case "AUTHENTICATION_ERROR":
|
|
464
|
+
return "PROVIDER_AUTH_FAILED";
|
|
465
|
+
case "MODEL_NOT_FOUND":
|
|
466
|
+
return "MODEL_NOT_FOUND";
|
|
467
|
+
default:
|
|
468
|
+
return "PROVIDER_UNAVAILABLE";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function isRetryable(kind) {
|
|
472
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
473
|
+
}
|
|
474
|
+
function kindFromStatus(status) {
|
|
475
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
476
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
477
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
478
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
479
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
480
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
481
|
+
return "UNKNOWN_ERROR";
|
|
482
|
+
}
|
|
483
|
+
function extractMessage(body) {
|
|
484
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
485
|
+
const root = body;
|
|
486
|
+
const error = root.error ?? root;
|
|
487
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
488
|
+
return typeof message === "string" ? message : void 0;
|
|
489
|
+
}
|
|
490
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
491
|
+
if (!value) return void 0;
|
|
492
|
+
const seconds = Number(value);
|
|
493
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
494
|
+
const date = Date.parse(value);
|
|
495
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
496
|
+
}
|
|
497
|
+
function isTimeoutError(error) {
|
|
498
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
499
|
+
}
|
|
500
|
+
function fallbackMessage(kind, context, status) {
|
|
501
|
+
const { provider, operation } = context;
|
|
502
|
+
switch (kind) {
|
|
503
|
+
case "AUTHENTICATION_ERROR":
|
|
504
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
505
|
+
case "RATE_LIMIT_ERROR":
|
|
506
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
507
|
+
case "MODEL_NOT_FOUND":
|
|
508
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
509
|
+
case "TIMEOUT":
|
|
510
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
511
|
+
case "SERVER_ERROR":
|
|
512
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
513
|
+
case "INVALID_REQUEST":
|
|
514
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
515
|
+
default:
|
|
516
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function providerHttpError(context) {
|
|
520
|
+
const status = context.status ?? 0;
|
|
521
|
+
const kind = kindFromStatus(status);
|
|
522
|
+
const detail = extractMessage(context.body);
|
|
523
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
524
|
+
return new ProviderError(
|
|
525
|
+
context.code ?? kindToCode(kind),
|
|
526
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
527
|
+
{
|
|
528
|
+
cause: context.cause,
|
|
529
|
+
details: {
|
|
530
|
+
kind,
|
|
531
|
+
provider: context.provider,
|
|
532
|
+
operation: context.operation,
|
|
533
|
+
status,
|
|
534
|
+
retryable: isRetryable(kind),
|
|
535
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
536
|
+
...detail ? { providerMessage: detail } : {}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
function providerTransportError(context) {
|
|
542
|
+
const cause = context.cause;
|
|
543
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
544
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
545
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
546
|
+
cause: context.cause,
|
|
547
|
+
details: {
|
|
548
|
+
kind,
|
|
549
|
+
provider: context.provider,
|
|
550
|
+
operation: context.operation,
|
|
551
|
+
retryable: isRetryable(kind)
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
450
556
|
// src/providers/openai-compatible/index.ts
|
|
557
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
451
558
|
var OpenAICompatibleProvider = class {
|
|
452
559
|
config;
|
|
560
|
+
providerId;
|
|
561
|
+
authentication;
|
|
453
562
|
log;
|
|
454
|
-
|
|
563
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
564
|
+
discovered;
|
|
565
|
+
env;
|
|
566
|
+
constructor(config, log = logger, env = process.env) {
|
|
455
567
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
568
|
+
this.env = env;
|
|
569
|
+
this.providerId = config.providerId ?? config.name;
|
|
570
|
+
this.authentication = {
|
|
571
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
572
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
573
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
574
|
+
};
|
|
456
575
|
this.log = log.child({ provider: config.name });
|
|
457
576
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
577
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
578
|
+
get requestTimeoutMs() {
|
|
579
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
580
|
+
}
|
|
581
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
582
|
+
async connect() {
|
|
583
|
+
authHeaders(this.config, this.env);
|
|
584
|
+
const models = await this.listModels();
|
|
585
|
+
this.discovered = models;
|
|
586
|
+
return models;
|
|
587
|
+
}
|
|
588
|
+
async getModel(modelId) {
|
|
589
|
+
const models = this.discovered ?? await this.connect();
|
|
590
|
+
return models.find((m) => m.id === modelId);
|
|
591
|
+
}
|
|
592
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
593
|
+
async rawModelsResponse() {
|
|
594
|
+
const response = await this.modelsRequest();
|
|
595
|
+
return response.body;
|
|
596
|
+
}
|
|
597
|
+
async modelsRequest() {
|
|
598
|
+
let response;
|
|
599
|
+
try {
|
|
600
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
601
|
+
headers: authHeaders(this.config, this.env),
|
|
602
|
+
timeoutMs: this.requestTimeoutMs
|
|
603
|
+
});
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (error instanceof ProviderError) throw error;
|
|
606
|
+
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
464
607
|
}
|
|
608
|
+
const { status, body } = response;
|
|
465
609
|
if (status !== 200) {
|
|
466
|
-
throw
|
|
610
|
+
throw providerHttpError({
|
|
611
|
+
provider: this.config.name,
|
|
612
|
+
operation: "listModels",
|
|
613
|
+
status,
|
|
614
|
+
body
|
|
615
|
+
});
|
|
467
616
|
}
|
|
617
|
+
return response;
|
|
618
|
+
}
|
|
619
|
+
async listModels() {
|
|
620
|
+
const body = await this.rawModelsResponse();
|
|
468
621
|
const entries = extractModelEntries(body, this.config);
|
|
469
622
|
const models = [];
|
|
470
623
|
let skipped = 0;
|
|
@@ -473,22 +626,44 @@ var OpenAICompatibleProvider = class {
|
|
|
473
626
|
skipped += 1;
|
|
474
627
|
continue;
|
|
475
628
|
}
|
|
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
|
-
});
|
|
629
|
+
models.push(this.toModelInfo(entry2));
|
|
486
630
|
}
|
|
487
631
|
if (skipped > 0) {
|
|
488
632
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
489
633
|
}
|
|
490
634
|
return models;
|
|
491
635
|
}
|
|
636
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
637
|
+
toModelInfo(entry2) {
|
|
638
|
+
const parameters = Array.isArray(entry2.supported_parameters) ? entry2.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
639
|
+
const contextWindow = typeof entry2.context_length === "number" ? entry2.context_length : typeof entry2.context_window === "number" ? entry2.context_window : void 0;
|
|
640
|
+
const inputTypes = normalizeModalities(entry2.input_modalities);
|
|
641
|
+
const outputTypes = normalizeModalities(entry2.output_modalities);
|
|
642
|
+
return {
|
|
643
|
+
provider: this.config.name,
|
|
644
|
+
id: entry2.id,
|
|
645
|
+
displayName: entry2.id,
|
|
646
|
+
...typeof entry2.name === "string" ? { name: entry2.name } : {},
|
|
647
|
+
status: "available",
|
|
648
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
649
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
650
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
651
|
+
...parameters !== void 0 ? {
|
|
652
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
653
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
654
|
+
supportsStreaming: parameters.includes("stream")
|
|
655
|
+
} : {},
|
|
656
|
+
metadata: {
|
|
657
|
+
...typeof entry2.owned_by === "string" ? { ownedBy: entry2.owned_by } : {},
|
|
658
|
+
...typeof entry2.created === "number" ? { createdAt: entry2.created } : {},
|
|
659
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/** Streaming where the gateway supports it (§11). */
|
|
664
|
+
stream(request, onChunk) {
|
|
665
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
666
|
+
}
|
|
492
667
|
async healthCheck() {
|
|
493
668
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
494
669
|
try {
|
|
@@ -518,27 +693,35 @@ var OpenAICompatibleProvider = class {
|
|
|
518
693
|
}
|
|
519
694
|
}
|
|
520
695
|
async chat(request) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
);
|
|
696
|
+
let response;
|
|
697
|
+
try {
|
|
698
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
699
|
+
method: "POST",
|
|
700
|
+
headers: authHeaders(this.config, this.env),
|
|
701
|
+
timeoutMs: this.requestTimeoutMs,
|
|
702
|
+
body: {
|
|
703
|
+
model: request.model,
|
|
704
|
+
messages: request.messages,
|
|
705
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
706
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
707
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (error instanceof ProviderError) throw error;
|
|
712
|
+
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
539
713
|
}
|
|
714
|
+
const { status, body } = response;
|
|
540
715
|
if (status !== 200) {
|
|
541
|
-
throw
|
|
716
|
+
throw providerHttpError({
|
|
717
|
+
provider: this.config.name,
|
|
718
|
+
operation: "chat",
|
|
719
|
+
status,
|
|
720
|
+
body,
|
|
721
|
+
...status === 404 ? {
|
|
722
|
+
code: "MODEL_NOT_FOUND"
|
|
723
|
+
} : {}
|
|
724
|
+
});
|
|
542
725
|
}
|
|
543
726
|
const payload = body;
|
|
544
727
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -560,6 +743,81 @@ var OpenAICompatibleProvider = class {
|
|
|
560
743
|
};
|
|
561
744
|
}
|
|
562
745
|
};
|
|
746
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
747
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
748
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
749
|
+
let response;
|
|
750
|
+
try {
|
|
751
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
752
|
+
method: "POST",
|
|
753
|
+
headers: {
|
|
754
|
+
accept: "text/event-stream",
|
|
755
|
+
"content-type": "application/json",
|
|
756
|
+
...authHeaders(config, env)
|
|
757
|
+
},
|
|
758
|
+
body: JSON.stringify({
|
|
759
|
+
model: request.model,
|
|
760
|
+
messages: request.messages,
|
|
761
|
+
stream: true,
|
|
762
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
763
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
764
|
+
}),
|
|
765
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
766
|
+
});
|
|
767
|
+
} catch (error) {
|
|
768
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
769
|
+
}
|
|
770
|
+
if (!response.ok) {
|
|
771
|
+
const body = await response.json().catch(() => void 0);
|
|
772
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
773
|
+
}
|
|
774
|
+
if (!response.body) {
|
|
775
|
+
throw providerHttpError({
|
|
776
|
+
provider: config.name,
|
|
777
|
+
operation: "stream",
|
|
778
|
+
status: 500,
|
|
779
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
let content = "";
|
|
783
|
+
let model = request.model;
|
|
784
|
+
let finishReason;
|
|
785
|
+
const decoder = new TextDecoder();
|
|
786
|
+
let buffer = "";
|
|
787
|
+
for await (const piece of response.body) {
|
|
788
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
789
|
+
let newline = buffer.indexOf("\n");
|
|
790
|
+
while (newline >= 0) {
|
|
791
|
+
const line = buffer.slice(0, newline).trim();
|
|
792
|
+
buffer = buffer.slice(newline + 1);
|
|
793
|
+
newline = buffer.indexOf("\n");
|
|
794
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
795
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
796
|
+
if (data === "[DONE]") continue;
|
|
797
|
+
let event;
|
|
798
|
+
try {
|
|
799
|
+
event = JSON.parse(data);
|
|
800
|
+
} catch {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (typeof event.model === "string") model = event.model;
|
|
804
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
805
|
+
const first = choices[0];
|
|
806
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
807
|
+
if (delta.length > 0) content += delta;
|
|
808
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
809
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
813
|
+
}
|
|
814
|
+
function normalizeModalities(value) {
|
|
815
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
816
|
+
if (!list) return void 0;
|
|
817
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
818
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
819
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
820
|
+
}
|
|
563
821
|
function normalizeBaseUrl(baseUrl) {
|
|
564
822
|
return baseUrl.replace(/\/+$/, "");
|
|
565
823
|
}
|
|
@@ -574,14 +832,6 @@ function extractModelEntries(body, config) {
|
|
|
574
832
|
{ details: { endpoint: "models" } }
|
|
575
833
|
);
|
|
576
834
|
}
|
|
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
835
|
function unavailable(config, message, status) {
|
|
586
836
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
587
837
|
details: { status }
|
|
@@ -594,11 +844,33 @@ function inferHealth(error) {
|
|
|
594
844
|
|
|
595
845
|
// src/providers/omnihilbras/index.ts
|
|
596
846
|
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
847
|
+
gateway;
|
|
848
|
+
async listModels() {
|
|
849
|
+
const raw = await this.rawModelsResponse();
|
|
850
|
+
await this.extractGatewayMetadata(raw);
|
|
851
|
+
const models = await super.listModels();
|
|
852
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
853
|
+
}
|
|
597
854
|
/**
|
|
598
855
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
599
856
|
* (use healthCheck() for a non-throwing probe).
|
|
600
857
|
*/
|
|
601
|
-
|
|
858
|
+
/**
|
|
859
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
860
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
861
|
+
*/
|
|
862
|
+
gatewayMetadata() {
|
|
863
|
+
return this.gateway;
|
|
864
|
+
}
|
|
865
|
+
async extractGatewayMetadata(body) {
|
|
866
|
+
if (body !== null && typeof body === "object") {
|
|
867
|
+
const gateway = body.gateway;
|
|
868
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
869
|
+
this.gateway = gateway;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async connectAndRegister(registry) {
|
|
602
874
|
const apiKey = resolveApiKey(this.config);
|
|
603
875
|
const discovered = await this.listModels();
|
|
604
876
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -822,7 +1094,7 @@ async function testProvider(name, options = {}) {
|
|
|
822
1094
|
if (options.connect && provider instanceof OmniHilbrasProvider) {
|
|
823
1095
|
const registry = new ModelRegistry();
|
|
824
1096
|
try {
|
|
825
|
-
const result = await provider.
|
|
1097
|
+
const result = await provider.connectAndRegister(registry);
|
|
826
1098
|
printProviderStatus(result.status);
|
|
827
1099
|
console.log(` registered: ${result.registered}`);
|
|
828
1100
|
} catch (error) {
|
|
@@ -897,19 +1169,416 @@ function registerModelsCommand(program) {
|
|
|
897
1169
|
|
|
898
1170
|
// src/agents/process/index.ts
|
|
899
1171
|
import { spawn } from "child_process";
|
|
900
|
-
|
|
901
|
-
|
|
1172
|
+
|
|
1173
|
+
// src/agent-protocol/version.ts
|
|
1174
|
+
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
1175
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1176
|
+
"omninode-agent-protocol/1",
|
|
1177
|
+
PROTOCOL_V2
|
|
1178
|
+
];
|
|
1179
|
+
var MAX_MESSAGE_BYTES = 1048576;
|
|
1180
|
+
function isSupportedProtocol(protocol) {
|
|
1181
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/agent-protocol/types.ts
|
|
1185
|
+
function newMessageId(prefix = "msg") {
|
|
1186
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1187
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1188
|
+
}
|
|
1189
|
+
function envelope(type, payload, ids = {}) {
|
|
1190
|
+
return {
|
|
1191
|
+
protocol: PROTOCOL_V2,
|
|
1192
|
+
messageId: newMessageId(),
|
|
1193
|
+
...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
|
|
1194
|
+
...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
|
|
1195
|
+
...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
|
|
1196
|
+
...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
|
|
1197
|
+
...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
|
|
1198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1199
|
+
type,
|
|
1200
|
+
payload,
|
|
1201
|
+
...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/agent-protocol/codec.ts
|
|
1206
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
1207
|
+
// node → agent
|
|
1208
|
+
"TASK",
|
|
1209
|
+
"HELLO_ACK",
|
|
1210
|
+
"CONTEXT",
|
|
1211
|
+
"INSTRUCTION",
|
|
1212
|
+
"RESPONSE",
|
|
1213
|
+
"CANCEL",
|
|
1214
|
+
// agent → node
|
|
1215
|
+
"HELLO",
|
|
1216
|
+
"TASK_REQUEST",
|
|
1217
|
+
"TASK_ACCEPTED",
|
|
1218
|
+
"TASK_STARTED",
|
|
1219
|
+
"STATUS",
|
|
1220
|
+
"QUESTION",
|
|
1221
|
+
"REPORT",
|
|
1222
|
+
"ARTIFACT",
|
|
1223
|
+
"COMPLETION",
|
|
1224
|
+
"ERROR",
|
|
1225
|
+
"CANCELLED"
|
|
1226
|
+
]);
|
|
1227
|
+
var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
|
|
1228
|
+
function validateEnvelope(value, line) {
|
|
1229
|
+
const sample = line?.slice(0, 200);
|
|
1230
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1231
|
+
return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
|
|
1232
|
+
}
|
|
1233
|
+
const raw = value;
|
|
1234
|
+
const protocol = raw.protocol;
|
|
1235
|
+
if (protocol === void 0) {
|
|
1236
|
+
const type = raw.type;
|
|
1237
|
+
if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
|
|
1238
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
ok: true,
|
|
1242
|
+
legacy: true,
|
|
1243
|
+
envelope: {
|
|
1244
|
+
protocol: "omninode-agent-protocol/1",
|
|
1245
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
|
|
1246
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1247
|
+
type,
|
|
1248
|
+
payload: raw.payload
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
|
|
1253
|
+
return {
|
|
1254
|
+
ok: false,
|
|
1255
|
+
error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (raw.type === void 0) {
|
|
1259
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
|
|
1260
|
+
}
|
|
1261
|
+
if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
|
|
1262
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
|
|
1263
|
+
}
|
|
1264
|
+
if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
|
|
1265
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
|
|
1266
|
+
}
|
|
1267
|
+
if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
|
|
1268
|
+
return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
ok: true,
|
|
1272
|
+
legacy: protocol !== PROTOCOL_V2,
|
|
1273
|
+
envelope: {
|
|
1274
|
+
protocol,
|
|
1275
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
|
|
1276
|
+
...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
|
|
1277
|
+
...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
|
|
1278
|
+
...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
|
|
1279
|
+
...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
|
|
1280
|
+
...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
|
|
1281
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1282
|
+
type: raw.type,
|
|
1283
|
+
payload: raw.payload,
|
|
1284
|
+
...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1289
|
+
const trimmed = line.trim();
|
|
1290
|
+
if (trimmed.length === 0) {
|
|
1291
|
+
return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
|
|
1292
|
+
}
|
|
1293
|
+
if (trimmed.length > maxBytes) {
|
|
1294
|
+
return {
|
|
1295
|
+
ok: false,
|
|
1296
|
+
error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
let value;
|
|
1300
|
+
try {
|
|
1301
|
+
value = JSON.parse(trimmed);
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
return {
|
|
1304
|
+
ok: false,
|
|
1305
|
+
error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
return validateEnvelope(value, trimmed);
|
|
1309
|
+
}
|
|
1310
|
+
function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1311
|
+
const line = JSON.stringify(envelope2);
|
|
1312
|
+
if (line.length > maxBytes) {
|
|
1313
|
+
throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
|
|
1314
|
+
}
|
|
1315
|
+
return line;
|
|
1316
|
+
}
|
|
1317
|
+
var ProtocolDecoder = class {
|
|
1318
|
+
buffer = "";
|
|
1319
|
+
maxBytes;
|
|
1320
|
+
constructor(maxBytes = MAX_MESSAGE_BYTES) {
|
|
1321
|
+
this.maxBytes = maxBytes;
|
|
1322
|
+
}
|
|
1323
|
+
push(chunk) {
|
|
1324
|
+
this.buffer += chunk;
|
|
1325
|
+
const results = [];
|
|
1326
|
+
let index = this.buffer.indexOf("\n");
|
|
1327
|
+
while (index >= 0) {
|
|
1328
|
+
const line = this.buffer.slice(0, index);
|
|
1329
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
1330
|
+
if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
|
|
1331
|
+
index = this.buffer.indexOf("\n");
|
|
1332
|
+
}
|
|
1333
|
+
if (this.buffer.length > this.maxBytes) {
|
|
1334
|
+
results.push({
|
|
1335
|
+
ok: false,
|
|
1336
|
+
error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
|
|
1337
|
+
});
|
|
1338
|
+
this.buffer = "";
|
|
1339
|
+
}
|
|
1340
|
+
return results;
|
|
1341
|
+
}
|
|
1342
|
+
/** Flush any trailing line when the process exits. */
|
|
1343
|
+
flush() {
|
|
1344
|
+
if (this.buffer.trim().length === 0) {
|
|
1345
|
+
this.buffer = "";
|
|
1346
|
+
return [];
|
|
1347
|
+
}
|
|
1348
|
+
const result = decodeMessage(this.buffer, this.maxBytes);
|
|
1349
|
+
this.buffer = "";
|
|
1350
|
+
return [result];
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// src/agent-protocol/handshake.ts
|
|
1355
|
+
function parseHello(envelopeValue) {
|
|
1356
|
+
if (envelopeValue.type !== "HELLO") {
|
|
1357
|
+
return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
|
|
1358
|
+
}
|
|
1359
|
+
const payload = envelopeValue.payload;
|
|
1360
|
+
if (payload === null || typeof payload !== "object") {
|
|
1361
|
+
return { ok: false, reason: "HELLO payload must be an object" };
|
|
1362
|
+
}
|
|
1363
|
+
const p = payload;
|
|
1364
|
+
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
1365
|
+
return { ok: false, reason: "HELLO payload is missing 'name'" };
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
ok: true,
|
|
1369
|
+
descriptor: {
|
|
1370
|
+
protocol: envelopeValue.protocol,
|
|
1371
|
+
name: p.name,
|
|
1372
|
+
...typeof p.version === "string" ? { version: p.version } : {},
|
|
1373
|
+
capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
|
|
1374
|
+
supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function buildHelloAck(descriptor) {
|
|
1379
|
+
return envelope("HELLO_ACK", {
|
|
1380
|
+
ok: true,
|
|
1381
|
+
protocol: PROTOCOL_V2,
|
|
1382
|
+
agent: descriptor.name,
|
|
1383
|
+
...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
|
|
1384
|
+
supportedProtocols: [PROTOCOL_V2]
|
|
1385
|
+
}, { agentId: descriptor.name });
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/agent-protocol/session.ts
|
|
1389
|
+
var ProtocolSession = class {
|
|
1390
|
+
decoder;
|
|
1391
|
+
ids;
|
|
1392
|
+
responder;
|
|
1393
|
+
violations = [];
|
|
1394
|
+
questions = [];
|
|
1395
|
+
artifacts = [];
|
|
1396
|
+
reports = [];
|
|
1397
|
+
descriptor;
|
|
1398
|
+
completionSummary;
|
|
1399
|
+
errorMessage;
|
|
1400
|
+
constructor(ids, options = {}) {
|
|
1401
|
+
this.ids = ids;
|
|
1402
|
+
this.responder = options.responder;
|
|
1403
|
+
this.decoder = new ProtocolDecoder(options.maxBytes);
|
|
1404
|
+
}
|
|
1405
|
+
/** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
|
|
1406
|
+
taskMessages(objective, role, context, instruction) {
|
|
1407
|
+
const ids = this.ids;
|
|
1408
|
+
const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
|
|
1409
|
+
if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
|
|
1410
|
+
if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
|
|
1411
|
+
if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
|
|
1412
|
+
return messages.map((m) => encodeMessage(m));
|
|
1413
|
+
}
|
|
1414
|
+
helloAck(descriptor) {
|
|
1415
|
+
return encodeMessage(buildHelloAck(descriptor));
|
|
1416
|
+
}
|
|
1417
|
+
/** Consumes decoded stdout, answering questions as they arrive. */
|
|
1418
|
+
async handle(decoded) {
|
|
1419
|
+
if (!decoded.ok) {
|
|
1420
|
+
this.violations.push(decoded.error);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const message = decoded.envelope;
|
|
1424
|
+
if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
|
|
1425
|
+
if (message.type === "HELLO") {
|
|
1426
|
+
const hello = parseHello(message);
|
|
1427
|
+
if (hello.ok) this.descriptor = hello.descriptor;
|
|
1428
|
+
else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (message.type === "QUESTION") {
|
|
1432
|
+
await this.answerQuestion(message);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (message.type === "REPORT") {
|
|
1436
|
+
this.reports.push(message.payload);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (message.type === "ARTIFACT") {
|
|
1440
|
+
this.artifacts.push(message.payload);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (message.type === "COMPLETION") {
|
|
1444
|
+
const payload = message.payload;
|
|
1445
|
+
this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (message.type === "ERROR") {
|
|
1449
|
+
const payload = message.payload;
|
|
1450
|
+
const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
|
|
1451
|
+
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1452
|
+
this.errorMessage = `Agent error${code}: ${detail}`;
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
this.violations.push({
|
|
1459
|
+
code: "UNKNOWN_TYPE",
|
|
1460
|
+
message: `unexpected message type ${message.type} from agent`
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
async answerQuestion(message) {
|
|
1464
|
+
const payload = message.payload ?? {};
|
|
1465
|
+
const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
|
|
1466
|
+
const question = typeof payload.question === "string" ? payload.question : "";
|
|
1467
|
+
const expected = typeof payload.expected === "string" ? payload.expected : "text";
|
|
1468
|
+
let answer;
|
|
1469
|
+
if (this.responder) {
|
|
1470
|
+
try {
|
|
1471
|
+
answer = await this.responder({ questionId, question, expected });
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
this.violations.push({
|
|
1474
|
+
code: "INVALID_PAYLOAD",
|
|
1475
|
+
message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
|
|
1480
|
+
this.pendingAnswers.push(
|
|
1481
|
+
encodeMessage(
|
|
1482
|
+
envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
|
|
1483
|
+
...this.ids,
|
|
1484
|
+
requestId: message.requestId ?? this.ids.requestId
|
|
1485
|
+
})
|
|
1486
|
+
)
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
pendingAnswers = [];
|
|
1490
|
+
/** True once any inbound message carried the v2 protocol field. */
|
|
1491
|
+
sawV2 = false;
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/agents/cwd.ts
|
|
1495
|
+
import { statSync } from "fs";
|
|
1496
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1497
|
+
function validateWorkingDirectory(cwd, options = {}) {
|
|
1498
|
+
if (!cwd) return void 0;
|
|
1499
|
+
const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
|
|
1500
|
+
let stats;
|
|
1501
|
+
try {
|
|
1502
|
+
stats = statSync(normalized);
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
throw new AgentError(
|
|
1505
|
+
"AGENT_NOT_FOUND",
|
|
1506
|
+
`Working directory "${normalized}" does not exist or is not accessible.`,
|
|
1507
|
+
{ cause: error, details: { cwd: normalized } }
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
if (!stats.isDirectory()) {
|
|
1511
|
+
throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
|
|
1512
|
+
details: { cwd: normalized }
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
statSync(resolve(normalized, "."));
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
throw new AgentError(
|
|
1519
|
+
"AGENT_NOT_FOUND",
|
|
1520
|
+
`Working directory "${normalized}" is not accessible.`,
|
|
1521
|
+
{ cause: error }
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
1525
|
+
const rel = relative(projectRoot, normalized);
|
|
1526
|
+
const outsideProject = rel.startsWith("..") || isAbsolute(rel);
|
|
1527
|
+
if (outsideProject && options.allowExternal !== true) {
|
|
1528
|
+
throw new AgentError(
|
|
1529
|
+
"AGENT_NOT_FOUND",
|
|
1530
|
+
`Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
|
|
1531
|
+
{ details: { cwd: normalized, projectRoot } }
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
return { cwd: normalized, outsideProject };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/agents/env.ts
|
|
1538
|
+
var BASELINE_KEYS = ["PATH", "HOME"];
|
|
1539
|
+
function resolveEnvPolicy(config) {
|
|
1540
|
+
if (config.envPolicy !== void 0) return config.envPolicy;
|
|
1541
|
+
if (config.inheritEnv === false) return "explicit";
|
|
1542
|
+
return "inherit";
|
|
1543
|
+
}
|
|
902
1544
|
function buildChildEnv(config) {
|
|
903
1545
|
const configured = config.env ?? {};
|
|
904
|
-
|
|
1546
|
+
const policy = resolveEnvPolicy(config);
|
|
1547
|
+
if (policy === "inherit") {
|
|
905
1548
|
return { ...process.env, ...configured };
|
|
906
1549
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
1550
|
+
const base = {};
|
|
1551
|
+
for (const key of BASELINE_KEYS) {
|
|
1552
|
+
const value = process.env[key];
|
|
1553
|
+
if (value !== void 0) base[key] = value;
|
|
1554
|
+
}
|
|
1555
|
+
switch (policy) {
|
|
1556
|
+
case "explicit":
|
|
1557
|
+
return { ...base, ...configured };
|
|
1558
|
+
case "allowlist": {
|
|
1559
|
+
const allowed = { ...base };
|
|
1560
|
+
for (const key of config.envAllowlist ?? []) {
|
|
1561
|
+
const value = process.env[key];
|
|
1562
|
+
if (value !== void 0) allowed[key] = value;
|
|
1563
|
+
}
|
|
1564
|
+
return { ...allowed, ...configured };
|
|
1565
|
+
}
|
|
1566
|
+
case "denylist": {
|
|
1567
|
+
const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
|
|
1568
|
+
const filtered = {};
|
|
1569
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1570
|
+
if (denied.has(key.toUpperCase())) continue;
|
|
1571
|
+
filtered[key] = value;
|
|
1572
|
+
}
|
|
1573
|
+
return { ...filtered, ...configured };
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
912
1576
|
}
|
|
1577
|
+
|
|
1578
|
+
// src/agents/process/index.ts
|
|
1579
|
+
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
1580
|
+
var KILL_GRACE_MS = 5e3;
|
|
1581
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
913
1582
|
function composeTaskText(input) {
|
|
914
1583
|
const sections = [`# Objective
|
|
915
1584
|
${input.objective}`];
|
|
@@ -930,18 +1599,19 @@ ${input.instruction}`);
|
|
|
930
1599
|
return sections.join("\n\n");
|
|
931
1600
|
}
|
|
932
1601
|
function buildProtocolMessages(input, agentName) {
|
|
933
|
-
const
|
|
1602
|
+
const session = new ProtocolSession({
|
|
1603
|
+
requestId: `req-${input.taskId}`,
|
|
934
1604
|
taskId: input.taskId,
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1605
|
+
agentId: agentName,
|
|
1606
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1607
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1608
|
+
});
|
|
1609
|
+
return session.taskMessages(
|
|
1610
|
+
input.objective,
|
|
1611
|
+
input.role,
|
|
1612
|
+
input.context,
|
|
1613
|
+
input.instruction
|
|
1614
|
+
);
|
|
945
1615
|
}
|
|
946
1616
|
var ProcessAgent = class {
|
|
947
1617
|
config;
|
|
@@ -970,40 +1640,73 @@ var ProcessAgent = class {
|
|
|
970
1640
|
}
|
|
971
1641
|
const mode = this.config.inputMode ?? "stdin";
|
|
972
1642
|
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
973
|
-
return new Promise((
|
|
1643
|
+
return new Promise((resolve2, reject) => {
|
|
1644
|
+
let cwd;
|
|
1645
|
+
try {
|
|
1646
|
+
cwd = validateWorkingDirectory(this.config.cwd, {
|
|
1647
|
+
allowExternal: this.config.allowExternalCwd
|
|
1648
|
+
})?.cwd;
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
reject(error);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
974
1653
|
const child = spawn(
|
|
975
1654
|
command,
|
|
976
1655
|
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
977
1656
|
{
|
|
978
|
-
cwd:
|
|
1657
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
979
1658
|
env: buildChildEnv(this.config),
|
|
980
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1659
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1660
|
+
// Own process group so the whole tree can be terminated together
|
|
1661
|
+
// (roadmap §8: no orphaned children).
|
|
1662
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
981
1663
|
}
|
|
982
1664
|
);
|
|
983
1665
|
this.running.set(input.taskId, child);
|
|
1666
|
+
GLOBAL_RUNNING.add(child);
|
|
1667
|
+
installSignalForwarding();
|
|
984
1668
|
let stdout = "";
|
|
985
1669
|
let stderr = "";
|
|
986
1670
|
let timedOut = false;
|
|
1671
|
+
let outputLimitExceeded = false;
|
|
987
1672
|
let settled = false;
|
|
1673
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
988
1674
|
const timer = setTimeout(() => {
|
|
989
1675
|
timedOut = true;
|
|
990
|
-
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
991
|
-
child
|
|
992
|
-
setTimeout(() => child
|
|
1676
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
|
|
1677
|
+
killTree(child, "SIGTERM");
|
|
1678
|
+
setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
|
|
993
1679
|
}, timeoutMs);
|
|
994
1680
|
const settle = (finish) => {
|
|
995
1681
|
if (settled) return;
|
|
996
1682
|
settled = true;
|
|
997
1683
|
clearTimeout(timer);
|
|
998
1684
|
this.running.delete(input.taskId);
|
|
1685
|
+
GLOBAL_RUNNING.delete(child);
|
|
999
1686
|
finish();
|
|
1000
1687
|
};
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1688
|
+
const collect = (into, chunk) => {
|
|
1689
|
+
if (outputLimitExceeded) return;
|
|
1690
|
+
const text = sanitizeChunk(chunk);
|
|
1691
|
+
if (into === "stdout") {
|
|
1692
|
+
if (stdout.length + text.length > maxOutputBytes) {
|
|
1693
|
+
stdout = (stdout + text).slice(0, maxOutputBytes);
|
|
1694
|
+
outputLimitExceeded = true;
|
|
1695
|
+
this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
|
|
1696
|
+
killTree(child, "SIGKILL");
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
stdout += text;
|
|
1700
|
+
} else {
|
|
1701
|
+
if (stderr.length + text.length > maxOutputBytes) {
|
|
1702
|
+
stderr = (stderr + text).slice(0, maxOutputBytes);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
stderr += text;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
1709
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
1007
1710
|
child.stdin?.on("error", () => {
|
|
1008
1711
|
});
|
|
1009
1712
|
if (mode !== "arg") {
|
|
@@ -1037,7 +1740,19 @@ var ProcessAgent = class {
|
|
|
1037
1740
|
new AgentError(
|
|
1038
1741
|
"AGENT_TIMEOUT",
|
|
1039
1742
|
`Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
|
|
1040
|
-
{
|
|
1743
|
+
{
|
|
1744
|
+
details: {
|
|
1745
|
+
operation: "agent.run",
|
|
1746
|
+
agent: this.config.name,
|
|
1747
|
+
durationMs: timeoutMs,
|
|
1748
|
+
// The process was started and may have performed side
|
|
1749
|
+
// effects before being killed — the outcome is unknown,
|
|
1750
|
+
// not failed (roadmap §6.3).
|
|
1751
|
+
dispatched: true,
|
|
1752
|
+
stdout: stdout.slice(0, 4e3),
|
|
1753
|
+
stderr: stderr.slice(0, 2e3)
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1041
1756
|
)
|
|
1042
1757
|
);
|
|
1043
1758
|
return;
|
|
@@ -1045,7 +1760,7 @@ var ProcessAgent = class {
|
|
|
1045
1760
|
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
1046
1761
|
if (mode === "protocol") {
|
|
1047
1762
|
try {
|
|
1048
|
-
|
|
1763
|
+
resolve2(
|
|
1049
1764
|
interpretProtocolRun({
|
|
1050
1765
|
input,
|
|
1051
1766
|
agentName: this.config.name,
|
|
@@ -1060,14 +1775,14 @@ var ProcessAgent = class {
|
|
|
1060
1775
|
return;
|
|
1061
1776
|
}
|
|
1062
1777
|
const trimmed = stdout.trim();
|
|
1063
|
-
|
|
1778
|
+
resolve2({
|
|
1064
1779
|
taskId: input.taskId,
|
|
1065
|
-
status: code === 0 ? "completed" : "failed",
|
|
1780
|
+
status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
|
|
1066
1781
|
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
1067
1782
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1068
1783
|
exitCode: code ?? void 0,
|
|
1069
1784
|
logs,
|
|
1070
|
-
error:
|
|
1785
|
+
...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
|
|
1071
1786
|
});
|
|
1072
1787
|
});
|
|
1073
1788
|
});
|
|
@@ -1076,67 +1791,117 @@ var ProcessAgent = class {
|
|
|
1076
1791
|
/** Ask the agent to stop current work (SIGTERM). */
|
|
1077
1792
|
async cancel(taskId) {
|
|
1078
1793
|
const child = this.running.get(taskId);
|
|
1079
|
-
if (child) child
|
|
1794
|
+
if (child) killTree(child, "SIGTERM");
|
|
1080
1795
|
}
|
|
1081
1796
|
runningTaskIds() {
|
|
1082
1797
|
return [...this.running.keys()];
|
|
1083
1798
|
}
|
|
1084
1799
|
};
|
|
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
|
-
}
|
|
1800
|
+
function interpretProtocolRun({
|
|
1801
|
+
input,
|
|
1802
|
+
agentName,
|
|
1803
|
+
stdout,
|
|
1804
|
+
exitCode,
|
|
1805
|
+
logs,
|
|
1806
|
+
responder
|
|
1807
|
+
}) {
|
|
1808
|
+
const session = new ProtocolSession(
|
|
1809
|
+
{
|
|
1810
|
+
requestId: `req-${input.taskId}`,
|
|
1811
|
+
taskId: input.taskId,
|
|
1812
|
+
agentId: agentName,
|
|
1813
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1814
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1815
|
+
},
|
|
1816
|
+
{ ...responder !== void 0 ? { responder } : {} }
|
|
1817
|
+
);
|
|
1818
|
+
for (const result of session.decoder.push(stdout)) {
|
|
1819
|
+
session.handle(result);
|
|
1108
1820
|
}
|
|
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
|
-
);
|
|
1821
|
+
for (const result of session.decoder.flush()) {
|
|
1822
|
+
session.handle(result);
|
|
1114
1823
|
}
|
|
1824
|
+
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
1825
|
const trimmed = stdout.trim();
|
|
1826
|
+
const protocolBlock = {
|
|
1827
|
+
protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
|
|
1828
|
+
legacy: !session.sawV2 && session.descriptor === void 0,
|
|
1829
|
+
...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
|
|
1830
|
+
questions: session.questions,
|
|
1831
|
+
violations: session.violations.map((v) => `${v.code}: ${v.message}`)
|
|
1832
|
+
};
|
|
1833
|
+
if (session.errorMessage !== void 0) {
|
|
1834
|
+
return {
|
|
1835
|
+
taskId: input.taskId,
|
|
1836
|
+
status: "failed",
|
|
1837
|
+
error: session.errorMessage,
|
|
1838
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1839
|
+
exitCode: exitCode ?? void 0,
|
|
1840
|
+
logs,
|
|
1841
|
+
protocol: protocolBlock,
|
|
1842
|
+
...reports.length > 0 ? { reports } : {}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
|
|
1846
|
+
return {
|
|
1847
|
+
taskId: input.taskId,
|
|
1848
|
+
status: "failed",
|
|
1849
|
+
error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
|
|
1850
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1851
|
+
exitCode: exitCode ?? void 0,
|
|
1852
|
+
logs,
|
|
1853
|
+
protocol: protocolBlock
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const completed = exitCode === 0 && session.completionSummary !== void 0;
|
|
1116
1857
|
return {
|
|
1117
1858
|
taskId: input.taskId,
|
|
1118
|
-
status:
|
|
1119
|
-
summary:
|
|
1859
|
+
status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
|
|
1860
|
+
summary: session.completionSummary ?? reports[0]?.summary,
|
|
1120
1861
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1121
1862
|
exitCode: exitCode ?? void 0,
|
|
1122
1863
|
logs,
|
|
1123
|
-
|
|
1124
|
-
|
|
1864
|
+
protocol: protocolBlock,
|
|
1865
|
+
...reports.length > 0 ? { reports } : {},
|
|
1866
|
+
...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
|
|
1125
1867
|
};
|
|
1126
1868
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1869
|
+
var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
|
|
1870
|
+
var signalForwardingInstalled = false;
|
|
1871
|
+
function installSignalForwarding() {
|
|
1872
|
+
if (signalForwardingInstalled) return;
|
|
1873
|
+
signalForwardingInstalled = true;
|
|
1874
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1875
|
+
process.on(signal, () => {
|
|
1876
|
+
for (const child of GLOBAL_RUNNING) {
|
|
1877
|
+
killTree(child, "SIGTERM");
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function killTree(child, signal) {
|
|
1883
|
+
if (child.pid === void 0) return;
|
|
1884
|
+
if (process.platform !== "win32") {
|
|
1885
|
+
try {
|
|
1886
|
+
process.kill(-child.pid, signal);
|
|
1887
|
+
return;
|
|
1888
|
+
} catch {
|
|
1136
1889
|
}
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
child.kill(signal);
|
|
1137
1893
|
} catch {
|
|
1138
1894
|
}
|
|
1139
|
-
|
|
1895
|
+
}
|
|
1896
|
+
function describeExit(code, signal) {
|
|
1897
|
+
if (signal !== null) {
|
|
1898
|
+
return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
|
|
1899
|
+
}
|
|
1900
|
+
if (code === null) return "Process terminated without an exit code.";
|
|
1901
|
+
return `Process exited with code ${code}.`;
|
|
1902
|
+
}
|
|
1903
|
+
function sanitizeChunk(chunk) {
|
|
1904
|
+
return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
|
|
1140
1905
|
}
|
|
1141
1906
|
function normalizeReport(payload, taskId, agentName, index) {
|
|
1142
1907
|
if (payload === null || typeof payload !== "object") return void 0;
|
|
@@ -1157,6 +1922,40 @@ function firstLine(text, maxLength = 200) {
|
|
|
1157
1922
|
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
1158
1923
|
}
|
|
1159
1924
|
|
|
1925
|
+
// src/agents/adapters.ts
|
|
1926
|
+
var AgentAdapterRegistry = class {
|
|
1927
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1928
|
+
register(adapter) {
|
|
1929
|
+
this.adapters.set(adapter.integration, adapter);
|
|
1930
|
+
}
|
|
1931
|
+
get(integration) {
|
|
1932
|
+
return this.adapters.get(integration);
|
|
1933
|
+
}
|
|
1934
|
+
list() {
|
|
1935
|
+
return [...this.adapters.values()];
|
|
1936
|
+
}
|
|
1937
|
+
create(config) {
|
|
1938
|
+
const adapter = this.get(config.integration);
|
|
1939
|
+
if (!adapter) {
|
|
1940
|
+
throw new AgentError(
|
|
1941
|
+
"AGENT_NOT_FOUND",
|
|
1942
|
+
`No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
return adapter.create(config);
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
var ProcessAgentAdapter = class {
|
|
1949
|
+
constructor(factory) {
|
|
1950
|
+
this.factory = factory;
|
|
1951
|
+
}
|
|
1952
|
+
factory;
|
|
1953
|
+
integration = "process";
|
|
1954
|
+
create(config) {
|
|
1955
|
+
return this.factory(config);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1160
1959
|
// src/agents/registry.ts
|
|
1161
1960
|
var AgentRegistry = class {
|
|
1162
1961
|
agents = /* @__PURE__ */ new Map();
|
|
@@ -1172,14 +1971,15 @@ var AgentRegistry = class {
|
|
|
1172
1971
|
};
|
|
1173
1972
|
|
|
1174
1973
|
// src/agents/factory.ts
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1974
|
+
var defaultAgentAdapters = new AgentAdapterRegistry();
|
|
1975
|
+
defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
|
|
1976
|
+
function createAgent(config, adapters = defaultAgentAdapters) {
|
|
1977
|
+
const registered = adapters.get(config.integration);
|
|
1978
|
+
if (registered) return registered.create(config);
|
|
1179
1979
|
if (config.integration === "native") {
|
|
1180
1980
|
throw new OmniNodeError(
|
|
1181
1981
|
"NOT_IMPLEMENTED",
|
|
1182
|
-
`
|
|
1982
|
+
`No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
|
|
1183
1983
|
);
|
|
1184
1984
|
}
|
|
1185
1985
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
@@ -1222,7 +2022,7 @@ function registerAgentCommands(program) {
|
|
|
1222
2022
|
"--input-mode <mode>",
|
|
1223
2023
|
"How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
|
|
1224
2024
|
"stdin"
|
|
1225
|
-
).option(
|
|
2025
|
+
).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
2026
|
"--timeout-ms <ms>",
|
|
1227
2027
|
"Per-task timeout in milliseconds.",
|
|
1228
2028
|
(value) => Number(value)
|
|
@@ -1237,7 +2037,12 @@ async function addAgent(options) {
|
|
|
1237
2037
|
...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
|
|
1238
2038
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
1239
2039
|
input_mode: options.inputMode,
|
|
1240
|
-
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
|
|
2040
|
+
...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
|
|
2041
|
+
...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
|
|
2042
|
+
...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
|
|
2043
|
+
...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
|
|
2044
|
+
...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
|
|
2045
|
+
...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
|
|
1241
2046
|
});
|
|
1242
2047
|
if (!candidate.success) {
|
|
1243
2048
|
const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
@@ -1397,12 +2202,18 @@ import { randomBytes } from "crypto";
|
|
|
1397
2202
|
var ALLOWED_TRANSITIONS = {
|
|
1398
2203
|
created: ["queued", "running", "cancelled"],
|
|
1399
2204
|
queued: ["running", "cancelled"],
|
|
1400
|
-
running: ["completed", "failed", "cancelled"],
|
|
2205
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
1401
2206
|
waiting: ["running", "cancelled"],
|
|
1402
2207
|
completed: [],
|
|
1403
|
-
failed:
|
|
1404
|
-
|
|
2208
|
+
// failed → running is the only re-entry: bounded automatic retry (§6.4).
|
|
2209
|
+
failed: ["running"],
|
|
2210
|
+
timed_out: [],
|
|
2211
|
+
cancelled: [],
|
|
2212
|
+
unknown: [],
|
|
2213
|
+
partially_completed: []
|
|
1405
2214
|
};
|
|
2215
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
2216
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
1406
2217
|
var TaskEngine = class {
|
|
1407
2218
|
constructor(options) {
|
|
1408
2219
|
this.options = options;
|
|
@@ -1434,6 +2245,7 @@ var TaskEngine = class {
|
|
|
1434
2245
|
id: generateTaskId(),
|
|
1435
2246
|
objective: input.objective.trim(),
|
|
1436
2247
|
status: "created",
|
|
2248
|
+
attempt: 0,
|
|
1437
2249
|
createdAt: now,
|
|
1438
2250
|
updatedAt: now,
|
|
1439
2251
|
...input.project !== void 0 ? { project: input.project } : {},
|
|
@@ -1450,8 +2262,10 @@ var TaskEngine = class {
|
|
|
1450
2262
|
const task = await this.mustGet(id);
|
|
1451
2263
|
return this.transition(task, "queued");
|
|
1452
2264
|
}
|
|
1453
|
-
/** Runs the task through its assigned agent and records the result.
|
|
1454
|
-
|
|
2265
|
+
/** Runs the task through its assigned agent and records the result.
|
|
2266
|
+
* `maxAttempts` enables bounded automatic retries of *retryable* errors
|
|
2267
|
+
* with linear backoff; unknown/timed_out outcomes are never auto-retried. */
|
|
2268
|
+
async run(id, options = {}) {
|
|
1455
2269
|
const task = await this.mustGet(id);
|
|
1456
2270
|
if (task.status !== "created" && task.status !== "queued") {
|
|
1457
2271
|
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
@@ -1464,49 +2278,89 @@ var TaskEngine = class {
|
|
|
1464
2278
|
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
1465
2279
|
}
|
|
1466
2280
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1467
|
-
const running = await this.transition(task, "running");
|
|
1468
2281
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1469
|
-
let background =
|
|
2282
|
+
let background = task.context?.background;
|
|
1470
2283
|
if (this.options.memory) {
|
|
1471
2284
|
const memoryContext = await this.options.memory.gatherContext({
|
|
1472
|
-
objective:
|
|
1473
|
-
...
|
|
1474
|
-
...
|
|
2285
|
+
objective: task.objective,
|
|
2286
|
+
...task.role ? { role: task.role } : {},
|
|
2287
|
+
...task.project ? { project: task.project } : {}
|
|
1475
2288
|
});
|
|
1476
2289
|
if (memoryContext.length > 0) {
|
|
1477
2290
|
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1478
2291
|
}
|
|
1479
2292
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2293
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
|
|
2294
|
+
const backoffMs = options.retryBackoffMs ?? 0;
|
|
2295
|
+
let current = task;
|
|
2296
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
2297
|
+
const attempt = current.attempt + 1;
|
|
2298
|
+
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2299
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2300
|
+
current = await this.transition(current, "running", { attempt });
|
|
2301
|
+
let output;
|
|
2302
|
+
let thrown;
|
|
2303
|
+
try {
|
|
2304
|
+
output = await agent.run({
|
|
2305
|
+
taskId: current.id,
|
|
2306
|
+
objective: current.objective,
|
|
2307
|
+
...role ? { role } : {},
|
|
2308
|
+
...background !== void 0 ? { context: background } : {}
|
|
2309
|
+
});
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
thrown = error;
|
|
2312
|
+
}
|
|
2313
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2314
|
+
if (thrown !== void 0 || output === void 0) {
|
|
2315
|
+
const outcome = classifyThrown(thrown);
|
|
2316
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2317
|
+
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2318
|
+
current = await this.transition(current, outcome, {
|
|
2319
|
+
attempt,
|
|
2320
|
+
lastError: message,
|
|
2321
|
+
execution: {
|
|
2322
|
+
executionId,
|
|
2323
|
+
attempt,
|
|
2324
|
+
startedAt,
|
|
2325
|
+
finishedAt,
|
|
2326
|
+
outcome,
|
|
2327
|
+
error: message
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2331
|
+
if (retryable) {
|
|
2332
|
+
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2336
|
+
return current;
|
|
2337
|
+
}
|
|
2338
|
+
const currentState = await this.options.store.get(current.id);
|
|
2339
|
+
if (currentState?.status === "cancelled") {
|
|
2340
|
+
current = currentState;
|
|
2341
|
+
break;
|
|
2342
|
+
}
|
|
2343
|
+
const result = {
|
|
2344
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2345
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2346
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2347
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2348
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
2349
|
+
finishedAt
|
|
2350
|
+
};
|
|
2351
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2352
|
+
current = await this.transition(current, finalStatus, {
|
|
2353
|
+
attempt,
|
|
2354
|
+
result,
|
|
2355
|
+
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
1493
2356
|
});
|
|
2357
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2358
|
+
break;
|
|
1494
2359
|
}
|
|
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
2360
|
if (this.options.memory) {
|
|
1507
|
-
await this.options.memory.recordTaskOutcome(
|
|
2361
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
1508
2362
|
}
|
|
1509
|
-
return
|
|
2363
|
+
return current;
|
|
1510
2364
|
}
|
|
1511
2365
|
async cancel(id) {
|
|
1512
2366
|
const task = await this.mustGet(id);
|
|
@@ -1519,13 +2373,14 @@ var TaskEngine = class {
|
|
|
1519
2373
|
}
|
|
1520
2374
|
return this.transition(task, "cancelled");
|
|
1521
2375
|
}
|
|
1522
|
-
/** Error recovery
|
|
2376
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
1523
2377
|
async retry(id) {
|
|
1524
2378
|
const task = await this.mustGet(id);
|
|
1525
|
-
|
|
2379
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
2380
|
+
if (!retryableStates.includes(task.status)) {
|
|
1526
2381
|
throw new TaskError(
|
|
1527
2382
|
"TASK_INVALID",
|
|
1528
|
-
`Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
2383
|
+
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1529
2384
|
);
|
|
1530
2385
|
}
|
|
1531
2386
|
const { result: _discarded, ...rest } = task;
|
|
@@ -1557,10 +2412,14 @@ var TaskEngine = class {
|
|
|
1557
2412
|
if (!allowed.includes(to)) {
|
|
1558
2413
|
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
1559
2414
|
}
|
|
2415
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
1560
2416
|
const updated = {
|
|
1561
2417
|
...task,
|
|
1562
2418
|
status: to,
|
|
1563
2419
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2420
|
+
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2421
|
+
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2422
|
+
...executions !== void 0 ? { executions } : {},
|
|
1564
2423
|
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1565
2424
|
};
|
|
1566
2425
|
await this.options.store.save(updated);
|
|
@@ -1581,6 +2440,19 @@ var TaskEngine = class {
|
|
|
1581
2440
|
function generateTaskId() {
|
|
1582
2441
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1583
2442
|
}
|
|
2443
|
+
function errorCode(error) {
|
|
2444
|
+
return error?.code ?? "";
|
|
2445
|
+
}
|
|
2446
|
+
function classifyThrown(error) {
|
|
2447
|
+
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
2448
|
+
const dispatched = error.details?.dispatched;
|
|
2449
|
+
return dispatched === false ? "timed_out" : "unknown";
|
|
2450
|
+
}
|
|
2451
|
+
return "failed";
|
|
2452
|
+
}
|
|
2453
|
+
function sleep(ms) {
|
|
2454
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2455
|
+
}
|
|
1584
2456
|
|
|
1585
2457
|
// src/audit/index.ts
|
|
1586
2458
|
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -1623,6 +2495,23 @@ var FileAuditLog = class {
|
|
|
1623
2495
|
}
|
|
1624
2496
|
};
|
|
1625
2497
|
|
|
2498
|
+
// src/memory/base.ts
|
|
2499
|
+
function defaultSearch(provider, text, options = {}) {
|
|
2500
|
+
return provider.retrieve({
|
|
2501
|
+
text,
|
|
2502
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2503
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
function defaultMetadata(name, options = {}) {
|
|
2507
|
+
return {
|
|
2508
|
+
name,
|
|
2509
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2510
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
2511
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1626
2515
|
// src/memory/local.ts
|
|
1627
2516
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1628
2517
|
"the",
|
|
@@ -1685,9 +2574,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1685
2574
|
constructor(directory) {
|
|
1686
2575
|
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
1687
2576
|
}
|
|
1688
|
-
|
|
2577
|
+
metadata() {
|
|
2578
|
+
return defaultMetadata(this.name, {
|
|
2579
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2580
|
+
relevanceRanking: true,
|
|
2581
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
search(text, options = {}) {
|
|
2585
|
+
return defaultSearch(this, text, options);
|
|
2586
|
+
}
|
|
2587
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2588
|
+
query(query) {
|
|
2589
|
+
return this.retrieve(query);
|
|
2590
|
+
}
|
|
2591
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2592
|
+
write(entry2) {
|
|
2593
|
+
return this.store(entry2);
|
|
2594
|
+
}
|
|
2595
|
+
async retrieve(query) {
|
|
1689
2596
|
const entries = await this.readAll();
|
|
1690
|
-
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter(
|
|
2597
|
+
const filtered = entries.filter((entry2) => query.scope ? entry2.scope === query.scope : true).filter((entry2) => query.category ? entry2.category === query.category : true).filter(
|
|
1691
2598
|
(entry2) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry2.tags ?? []).includes(tag)) : true
|
|
1692
2599
|
);
|
|
1693
2600
|
if (!query.text || query.text.trim().length === 0) {
|
|
@@ -1696,7 +2603,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1696
2603
|
const queryKeywords = keywords(query.text);
|
|
1697
2604
|
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
2605
|
}
|
|
1699
|
-
|
|
2606
|
+
store(entry2) {
|
|
1700
2607
|
return this.mutate((entries) => {
|
|
1701
2608
|
const index = entries.findIndex((e) => e.key === entry2.key);
|
|
1702
2609
|
const stamped = {
|
|
@@ -1727,11 +2634,40 @@ var RememberaMemoryProvider = class {
|
|
|
1727
2634
|
this.env = config.env ?? process.env;
|
|
1728
2635
|
this.log = log.child({ component: "remembera" });
|
|
1729
2636
|
}
|
|
2637
|
+
get authenticationConfigured() {
|
|
2638
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
2639
|
+
}
|
|
1730
2640
|
headers() {
|
|
1731
2641
|
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1732
2642
|
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1733
2643
|
}
|
|
1734
|
-
|
|
2644
|
+
/** Search delegates to the gateway's relevance query. */
|
|
2645
|
+
search(text, options = {}) {
|
|
2646
|
+
return defaultSearch(
|
|
2647
|
+
this,
|
|
2648
|
+
text,
|
|
2649
|
+
{
|
|
2650
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2651
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2652
|
+
}
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
metadata() {
|
|
2656
|
+
return defaultMetadata(this.name, {
|
|
2657
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2658
|
+
relevanceRanking: true,
|
|
2659
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2663
|
+
query(query) {
|
|
2664
|
+
return this.retrieve(query);
|
|
2665
|
+
}
|
|
2666
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2667
|
+
write(entry2) {
|
|
2668
|
+
return this.store(entry2);
|
|
2669
|
+
}
|
|
2670
|
+
async retrieve(query) {
|
|
1735
2671
|
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1736
2672
|
if (status !== 200) {
|
|
1737
2673
|
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
@@ -1742,7 +2678,7 @@ var RememberaMemoryProvider = class {
|
|
|
1742
2678
|
}
|
|
1743
2679
|
return entries.map((entry2) => this.toEntry(entry2)).filter((entry2) => entry2 !== void 0);
|
|
1744
2680
|
}
|
|
1745
|
-
async
|
|
2681
|
+
async store(entry2) {
|
|
1746
2682
|
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry2);
|
|
1747
2683
|
if (status !== 200 && status !== 201) {
|
|
1748
2684
|
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
@@ -1777,6 +2713,7 @@ var RememberaMemoryProvider = class {
|
|
|
1777
2713
|
content: entry2.content,
|
|
1778
2714
|
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1779
2715
|
...Array.isArray(entry2.tags) ? { tags: entry2.tags.filter((t) => typeof t === "string") } : {},
|
|
2716
|
+
...typeof entry2.category === "string" ? { category: entry2.category } : {},
|
|
1780
2717
|
...typeof entry2.createdAt === "string" ? { createdAt: entry2.createdAt } : {},
|
|
1781
2718
|
...typeof entry2.updatedAt === "string" ? { updatedAt: entry2.updatedAt } : {}
|
|
1782
2719
|
};
|
|
@@ -1807,57 +2744,54 @@ function createMemoryProvider(config) {
|
|
|
1807
2744
|
}
|
|
1808
2745
|
|
|
1809
2746
|
// src/memory/service.ts
|
|
2747
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1810
2748
|
var MAX_CONTEXT_CHARS = 4e3;
|
|
1811
2749
|
var MemoryService = class {
|
|
1812
|
-
constructor(provider, log = logger) {
|
|
2750
|
+
constructor(provider, log = logger, options = {}) {
|
|
1813
2751
|
this.provider = provider;
|
|
1814
2752
|
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2753
|
+
this.required = options.required ?? false;
|
|
2754
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1815
2755
|
}
|
|
1816
2756
|
provider;
|
|
1817
2757
|
log;
|
|
2758
|
+
required;
|
|
2759
|
+
maxContextChars;
|
|
1818
2760
|
get providerRef() {
|
|
1819
2761
|
return this.provider;
|
|
1820
2762
|
}
|
|
1821
2763
|
get providerName() {
|
|
1822
2764
|
return this.provider.name;
|
|
1823
2765
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
2766
|
+
get isRequired() {
|
|
2767
|
+
return this.required;
|
|
2768
|
+
}
|
|
2769
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
2770
|
+
async contextFor(query) {
|
|
2771
|
+
let entries;
|
|
1826
2772
|
try {
|
|
1827
|
-
|
|
2773
|
+
entries = await this.provider.retrieve({
|
|
1828
2774
|
text: query.objective,
|
|
1829
2775
|
...query.role ? { tags: [query.role] } : {},
|
|
1830
|
-
limit:
|
|
2776
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1831
2777
|
});
|
|
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
2778
|
} catch (error) {
|
|
1848
|
-
this.
|
|
1849
|
-
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1850
|
-
);
|
|
1851
|
-
return "";
|
|
2779
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
1852
2780
|
}
|
|
2781
|
+
return this.format(entries);
|
|
2782
|
+
}
|
|
2783
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2784
|
+
async gatherContext(query) {
|
|
2785
|
+
return (await this.contextFor(query)).text;
|
|
1853
2786
|
}
|
|
1854
|
-
/** Records a finished task's outcome —
|
|
2787
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1855
2788
|
async recordTaskOutcome(task) {
|
|
1856
2789
|
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1857
2790
|
try {
|
|
1858
|
-
await this.provider.
|
|
2791
|
+
await this.provider.store({
|
|
1859
2792
|
key: `task:${task.id}`,
|
|
1860
2793
|
scope: "task",
|
|
2794
|
+
category: "tasks",
|
|
1861
2795
|
content: `Objective: ${task.objective}
|
|
1862
2796
|
Status: ${task.status}
|
|
1863
2797
|
Outcome: ${outcome}`,
|
|
@@ -1868,18 +2802,53 @@ Outcome: ${outcome}`,
|
|
|
1868
2802
|
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1869
2803
|
);
|
|
1870
2804
|
for (const { report, finding } of significant) {
|
|
1871
|
-
await this.provider.
|
|
2805
|
+
await this.provider.store({
|
|
1872
2806
|
key: `finding:${report.id}:${finding.id}`,
|
|
1873
2807
|
scope: "project",
|
|
2808
|
+
category: "problems",
|
|
1874
2809
|
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1875
2810
|
tags: ["known-problem", report.agent]
|
|
1876
2811
|
});
|
|
1877
2812
|
}
|
|
1878
2813
|
} catch (error) {
|
|
1879
|
-
this.
|
|
1880
|
-
|
|
2814
|
+
this.handleFailure("store", error, void 0);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
format(entries) {
|
|
2818
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2819
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2820
|
+
for (const entry2 of entries) {
|
|
2821
|
+
byScope.set(entry2.scope, [...byScope.get(entry2.scope) ?? [], entry2]);
|
|
2822
|
+
}
|
|
2823
|
+
const parts = [];
|
|
2824
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2825
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
2826
|
+
if (scopeEntries.length === 0) continue;
|
|
2827
|
+
const lines = scopeEntries.map((entry2) => {
|
|
2828
|
+
const category = entry2.category ? `[${entry2.category}] ` : "";
|
|
2829
|
+
return `- ${category}${entry2.content}`;
|
|
2830
|
+
}).join("\n");
|
|
2831
|
+
parts.push(`### Relevant memory (${scope})
|
|
2832
|
+
${lines}`);
|
|
2833
|
+
}
|
|
2834
|
+
let text = parts.join("\n\n");
|
|
2835
|
+
const truncated = text.length > this.maxContextChars;
|
|
2836
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2837
|
+
return { text, entries, truncated };
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
2841
|
+
*/
|
|
2842
|
+
handleFailure(operation, error, fallback) {
|
|
2843
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2844
|
+
if (this.required) {
|
|
2845
|
+
throw new MemoryError(
|
|
2846
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2847
|
+
{ cause: error }
|
|
1881
2848
|
);
|
|
1882
2849
|
}
|
|
2850
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
2851
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
1883
2852
|
}
|
|
1884
2853
|
};
|
|
1885
2854
|
|
|
@@ -1913,7 +2882,11 @@ function createTaskEngine(config, log = logger, directory) {
|
|
|
1913
2882
|
agents,
|
|
1914
2883
|
roles,
|
|
1915
2884
|
store: new FileTaskStore(directory),
|
|
1916
|
-
...config.project.memory !== void 0 ? {
|
|
2885
|
+
...config.project.memory !== void 0 ? {
|
|
2886
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
2887
|
+
required: config.project.memory.required
|
|
2888
|
+
})
|
|
2889
|
+
} : {},
|
|
1917
2890
|
audit: new FileAuditLog(),
|
|
1918
2891
|
log
|
|
1919
2892
|
});
|
|
@@ -1980,7 +2953,7 @@ function registerTaskCommands(program) {
|
|
|
1980
2953
|
const cancelled = await engine.cancel(taskId);
|
|
1981
2954
|
console.log(`Task ${cancelled.id} is now "${cancelled.status}".`);
|
|
1982
2955
|
});
|
|
1983
|
-
task.command("retry <task-id>").description("Reset a failed
|
|
2956
|
+
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
2957
|
const config = loadConfig();
|
|
1985
2958
|
const engine = createTaskEngine(config);
|
|
1986
2959
|
const reset = await engine.retry(taskId);
|
|
@@ -2003,6 +2976,15 @@ function printOutcome(task, options = {}) {
|
|
|
2003
2976
|
if (task.context?.files) console.log(` files: ${task.context.files.join(", ")}`);
|
|
2004
2977
|
console.log(` objective: ${task.objective}`);
|
|
2005
2978
|
}
|
|
2979
|
+
if (options.verbose && task.attempt > 0) {
|
|
2980
|
+
console.log(` attempt: ${task.attempt}`);
|
|
2981
|
+
}
|
|
2982
|
+
if (task.lastError) console.log(` error: ${task.lastError}`);
|
|
2983
|
+
if (options.verbose && task.executions && task.executions.length > 0) {
|
|
2984
|
+
console.log(
|
|
2985
|
+
` executions: ${task.executions.map((e) => `#${e.attempt}:${e.outcome}`).join(", ")}`
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2006
2988
|
const result = task.result;
|
|
2007
2989
|
if (!result) return;
|
|
2008
2990
|
if (result.summary) console.log(` summary: ${result.summary}`);
|
|
@@ -2018,6 +3000,87 @@ function truncate2(text, max) {
|
|
|
2018
3000
|
|
|
2019
3001
|
// src/reports/aggregate.ts
|
|
2020
3002
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3003
|
+
|
|
3004
|
+
// src/reports/schema.ts
|
|
3005
|
+
import { z as z2 } from "zod";
|
|
3006
|
+
var evidenceSchema = z2.union([
|
|
3007
|
+
z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
|
|
3008
|
+
z2.object({
|
|
3009
|
+
kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
|
|
3010
|
+
ref: z2.string().optional(),
|
|
3011
|
+
line: z2.number().int().optional(),
|
|
3012
|
+
excerpt: z2.string().optional(),
|
|
3013
|
+
agent: z2.string().optional()
|
|
3014
|
+
})
|
|
3015
|
+
]);
|
|
3016
|
+
var findingSchema = z2.object({
|
|
3017
|
+
id: z2.string().min(1),
|
|
3018
|
+
title: z2.string().min(1),
|
|
3019
|
+
detail: z2.string().default(""),
|
|
3020
|
+
description: z2.string().optional(),
|
|
3021
|
+
severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
|
|
3022
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
3023
|
+
severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
|
|
3024
|
+
confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
|
|
3025
|
+
recommendations: z2.array(z2.string()).optional(),
|
|
3026
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
3027
|
+
location: z2.string().optional(),
|
|
3028
|
+
tags: z2.array(z2.string()).optional(),
|
|
3029
|
+
source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
|
|
3030
|
+
recommendation: z2.string().optional()
|
|
3031
|
+
});
|
|
3032
|
+
var artifactSchema = z2.object({
|
|
3033
|
+
kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
|
|
3034
|
+
ref: z2.string().min(1),
|
|
3035
|
+
description: z2.string().optional(),
|
|
3036
|
+
mediaType: z2.string().optional(),
|
|
3037
|
+
bytes: z2.number().int().optional()
|
|
3038
|
+
});
|
|
3039
|
+
var reportSchema = z2.object({
|
|
3040
|
+
id: z2.string().min(1),
|
|
3041
|
+
taskId: z2.string().min(1),
|
|
3042
|
+
agent: z2.string().min(1),
|
|
3043
|
+
agentId: z2.string().optional(),
|
|
3044
|
+
pipelineRunId: z2.string().optional(),
|
|
3045
|
+
model: z2.string().optional(),
|
|
3046
|
+
summary: z2.string(),
|
|
3047
|
+
findings: z2.array(findingSchema),
|
|
3048
|
+
recommendations: z2.array(z2.string()),
|
|
3049
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
3050
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
3051
|
+
artifacts: z2.array(artifactSchema).optional(),
|
|
3052
|
+
createdAt: z2.string().min(1),
|
|
3053
|
+
metadata: z2.record(z2.unknown()).optional()
|
|
3054
|
+
});
|
|
3055
|
+
function validateReport(value) {
|
|
3056
|
+
const parsed = reportSchema.safeParse(value);
|
|
3057
|
+
if (!parsed.success) {
|
|
3058
|
+
const issues = parsed.error.issues.map((issue) => ({
|
|
3059
|
+
path: issue.path.join(".") || "(root)",
|
|
3060
|
+
message: issue.message
|
|
3061
|
+
}));
|
|
3062
|
+
return {
|
|
3063
|
+
valid: false,
|
|
3064
|
+
issues,
|
|
3065
|
+
reason: `report failed schema validation (${issues.length} issue(s))`
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
const report = parsed.data;
|
|
3069
|
+
report.findings = report.findings.map((finding) => ({
|
|
3070
|
+
...finding,
|
|
3071
|
+
description: finding.description ?? finding.detail,
|
|
3072
|
+
...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
|
|
3073
|
+
}));
|
|
3074
|
+
return { valid: true, report };
|
|
3075
|
+
}
|
|
3076
|
+
function normalizeEvidence(input) {
|
|
3077
|
+
if (input === void 0) return void 0;
|
|
3078
|
+
return input.map(
|
|
3079
|
+
(item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
// src/reports/aggregate.ts
|
|
2021
3084
|
var SEVERITY_RANK = {
|
|
2022
3085
|
info: 0,
|
|
2023
3086
|
low: 1,
|
|
@@ -2039,7 +3102,12 @@ function aggregateReports(reports, options = {}) {
|
|
|
2039
3102
|
}
|
|
2040
3103
|
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
2041
3104
|
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2042
|
-
const
|
|
3105
|
+
const conflicts = detectConflicts(findingGroups);
|
|
3106
|
+
const agreements = findingGroups.filter(
|
|
3107
|
+
(group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
|
|
3108
|
+
);
|
|
3109
|
+
const disputed = conflicts.length;
|
|
3110
|
+
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
3111
|
return {
|
|
2044
3112
|
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
2045
3113
|
title: options.objective !== void 0 ? truncate3(options.objective, 120) : "Combined report",
|
|
@@ -2048,10 +3116,57 @@ function aggregateReports(reports, options = {}) {
|
|
|
2048
3116
|
taskIds: options.taskIds ?? [],
|
|
2049
3117
|
summary,
|
|
2050
3118
|
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
3119
|
+
agreements,
|
|
3120
|
+
conflicts,
|
|
2051
3121
|
recommendations: recommendationGroups,
|
|
2052
3122
|
sources,
|
|
2053
3123
|
reportIds: reports.map((r) => r.id),
|
|
2054
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3124
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3125
|
+
metadata: {
|
|
3126
|
+
agents: sources.length,
|
|
3127
|
+
reports: reports.length,
|
|
3128
|
+
rawFindings: totalRawFindings,
|
|
3129
|
+
findingGroups: findingGroups.length,
|
|
3130
|
+
agreements: agreements.length,
|
|
3131
|
+
conflicts: conflicts.length
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
function rankConfidence(confidence) {
|
|
3136
|
+
return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
|
|
3137
|
+
}
|
|
3138
|
+
function detectConflicts(groups) {
|
|
3139
|
+
const conflicts = [];
|
|
3140
|
+
for (const group of groups) {
|
|
3141
|
+
if (group.sources.length < 2) continue;
|
|
3142
|
+
const severities = new Set(Object.values(group.severityBySource ?? {}));
|
|
3143
|
+
if (severities.size > 1) {
|
|
3144
|
+
conflicts.push({
|
|
3145
|
+
type: "severity",
|
|
3146
|
+
findingTitle: group.title,
|
|
3147
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
3148
|
+
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.`
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
|
|
3152
|
+
if (recommendations.length > 1) {
|
|
3153
|
+
conflicts.push({
|
|
3154
|
+
type: "recommendation",
|
|
3155
|
+
findingTitle: group.title,
|
|
3156
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
3157
|
+
note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
return conflicts;
|
|
3162
|
+
}
|
|
3163
|
+
function position(source, group) {
|
|
3164
|
+
return {
|
|
3165
|
+
agent: source.agent,
|
|
3166
|
+
reportId: source.reportId,
|
|
3167
|
+
...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
|
|
3168
|
+
...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
|
|
3169
|
+
...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
|
|
2055
3170
|
};
|
|
2056
3171
|
}
|
|
2057
3172
|
function rank(finding) {
|
|
@@ -2064,10 +3179,22 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
2064
3179
|
group.occurrences += 1;
|
|
2065
3180
|
group.details.push(finding.detail);
|
|
2066
3181
|
group.sources.push(source);
|
|
2067
|
-
group.evidence.push(...finding.evidence ?? []);
|
|
3182
|
+
group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
|
|
2068
3183
|
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
2069
3184
|
group.severity = finding.severity;
|
|
2070
3185
|
}
|
|
3186
|
+
if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
|
|
3187
|
+
group.confidence = finding.confidence;
|
|
3188
|
+
}
|
|
3189
|
+
if (finding.severity) {
|
|
3190
|
+
group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
|
|
3191
|
+
}
|
|
3192
|
+
if (finding.confidence) {
|
|
3193
|
+
group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
|
|
3194
|
+
}
|
|
3195
|
+
if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
|
|
3196
|
+
group.recommendations = [...group.recommendations ?? [], finding.recommendation];
|
|
3197
|
+
}
|
|
2071
3198
|
return;
|
|
2072
3199
|
}
|
|
2073
3200
|
groups.push({
|
|
@@ -2076,7 +3203,11 @@ function mergeFinding(groups, finding, report, threshold) {
|
|
|
2076
3203
|
sources: [source],
|
|
2077
3204
|
occurrences: 1,
|
|
2078
3205
|
...finding.severity ? { severity: finding.severity } : {},
|
|
2079
|
-
|
|
3206
|
+
...finding.confidence ? { confidence: finding.confidence } : {},
|
|
3207
|
+
...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
|
|
3208
|
+
...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
|
|
3209
|
+
...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
|
|
3210
|
+
evidence: [...normalizeEvidence(finding.evidence) ?? []]
|
|
2080
3211
|
});
|
|
2081
3212
|
}
|
|
2082
3213
|
function mergeRecommendation(groups, text, report, threshold) {
|
|
@@ -2158,6 +3289,7 @@ function extractReportFromText(raw, meta) {
|
|
|
2158
3289
|
id: `${meta.taskId}-f-${index + 1}`,
|
|
2159
3290
|
title: truncate4(text, 120),
|
|
2160
3291
|
detail: text,
|
|
3292
|
+
description: text,
|
|
2161
3293
|
severity: guessSeverity(text)
|
|
2162
3294
|
}));
|
|
2163
3295
|
return {
|
|
@@ -2168,7 +3300,8 @@ function extractReportFromText(raw, meta) {
|
|
|
2168
3300
|
summary,
|
|
2169
3301
|
findings,
|
|
2170
3302
|
recommendations: recommendationTexts.map((t) => truncate4(t, 200)),
|
|
2171
|
-
|
|
3303
|
+
...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
|
|
3304
|
+
metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
|
|
2172
3305
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2173
3306
|
};
|
|
2174
3307
|
}
|
|
@@ -2184,27 +3317,46 @@ var ReportService = class {
|
|
|
2184
3317
|
}
|
|
2185
3318
|
store;
|
|
2186
3319
|
log;
|
|
3320
|
+
/** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
|
|
3321
|
+
lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
|
|
2187
3322
|
get storeRef() {
|
|
2188
3323
|
return this.store;
|
|
2189
3324
|
}
|
|
2190
|
-
/** Normalizes finished tasks into reports (§17 — normalizer)
|
|
3325
|
+
/** Normalizes finished tasks into reports (§17 — normalizer), validating each
|
|
3326
|
+
* one so malformed reports never reach aggregation (§13). */
|
|
2191
3327
|
async collectFromTasks(tasks) {
|
|
2192
|
-
const
|
|
3328
|
+
const candidates = [];
|
|
2193
3329
|
for (const task of tasks) {
|
|
2194
3330
|
const structured = task.result?.reports ?? [];
|
|
2195
3331
|
if (structured.length > 0) {
|
|
2196
|
-
|
|
3332
|
+
candidates.push(...structured);
|
|
2197
3333
|
continue;
|
|
2198
3334
|
}
|
|
2199
3335
|
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
2200
3336
|
if (text.trim().length === 0) continue;
|
|
2201
|
-
|
|
3337
|
+
candidates.push(
|
|
2202
3338
|
extractReportFromText(text, {
|
|
2203
3339
|
taskId: task.id,
|
|
2204
3340
|
agent: task.agent ?? "unknown"
|
|
2205
3341
|
})
|
|
2206
3342
|
);
|
|
2207
3343
|
}
|
|
3344
|
+
const reports = [];
|
|
3345
|
+
const rejected = [];
|
|
3346
|
+
for (const candidate of candidates) {
|
|
3347
|
+
const result = validateReport(candidate);
|
|
3348
|
+
if (result.valid) {
|
|
3349
|
+
reports.push(result.report);
|
|
3350
|
+
} else {
|
|
3351
|
+
rejected.push({
|
|
3352
|
+
key: candidate?.id ?? "(unknown)",
|
|
3353
|
+
reason: result.reason,
|
|
3354
|
+
issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
|
|
3355
|
+
});
|
|
3356
|
+
this.log.warn(`Rejected malformed report: ${result.reason}`);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
|
|
2208
3360
|
return reports;
|
|
2209
3361
|
}
|
|
2210
3362
|
async saveReports(reports) {
|
|
@@ -2293,8 +3445,9 @@ function registerReportCommands(program) {
|
|
|
2293
3445
|
return;
|
|
2294
3446
|
}
|
|
2295
3447
|
for (const c of combined) {
|
|
3448
|
+
const conflicts = c.conflicts.length > 0 ? ` ${c.conflicts.length} CONFLICT(S)` : "";
|
|
2296
3449
|
console.log(
|
|
2297
|
-
`${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
|
|
3450
|
+
`${c.id} ${c.findings.length} finding(s) from ${c.sources.length} source(s)${conflicts}${c.pipelineRunId ? ` run=${c.pipelineRunId}` : ""}`
|
|
2298
3451
|
);
|
|
2299
3452
|
}
|
|
2300
3453
|
});
|
|
@@ -2331,6 +3484,21 @@ function registerReportCommands(program) {
|
|
|
2331
3484
|
console.log(` reports: ${combined.reportIds.length}`);
|
|
2332
3485
|
if (combined.pipelineRunId) console.log(` run: ${combined.pipelineRunId}`);
|
|
2333
3486
|
console.log(` summary: ${combined.summary}`);
|
|
3487
|
+
if (combined.conflicts.length > 0) {
|
|
3488
|
+
console.log(` CONFLICTS (${combined.conflicts.length}) \u2014 both positions preserved:`);
|
|
3489
|
+
for (const conflict of combined.conflicts) {
|
|
3490
|
+
console.log(` ! ${conflict.findingTitle} [${conflict.type}]`);
|
|
3491
|
+
for (const position2 of conflict.positions) {
|
|
3492
|
+
console.log(
|
|
3493
|
+
` ${position2.agent}: severity=${position2.severity ?? "unstated"}${position2.recommendation ? ` fix="${position2.recommendation}"` : ""}`
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
console.log(` ${conflict.note}`);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
if (combined.agreements.length > 0) {
|
|
3500
|
+
console.log(` agreements: ${combined.agreements.length} (multi-agent consensus)`);
|
|
3501
|
+
}
|
|
2334
3502
|
if (combined.findings.length > 0) {
|
|
2335
3503
|
console.log(" findings (grouped, duplicates merged):");
|
|
2336
3504
|
for (const finding of combined.findings) {
|
|
@@ -2367,19 +3535,32 @@ var HeuristicPlanner = class {
|
|
|
2367
3535
|
name = "heuristic";
|
|
2368
3536
|
async plan(request) {
|
|
2369
3537
|
const steps = [];
|
|
2370
|
-
const findings = request.reports.flatMap(
|
|
2371
|
-
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
2372
|
-
);
|
|
2373
3538
|
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
2374
|
-
for (const
|
|
3539
|
+
for (const text of recommendations) {
|
|
2375
3540
|
steps.push({
|
|
2376
3541
|
id: `step-${steps.length + 1}`,
|
|
2377
3542
|
title: truncate5(text, 120),
|
|
2378
3543
|
order: steps.length + 1,
|
|
2379
|
-
...
|
|
3544
|
+
...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
2380
3545
|
});
|
|
2381
3546
|
}
|
|
2382
|
-
const
|
|
3547
|
+
const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
|
|
3548
|
+
finding: {
|
|
3549
|
+
id: `agg-${slug(finding.title)}`,
|
|
3550
|
+
title: finding.title,
|
|
3551
|
+
detail: finding.details[0] ?? finding.title,
|
|
3552
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
3553
|
+
...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
|
|
3554
|
+
},
|
|
3555
|
+
agent: finding.sources.map((s) => s.agent).join("+")
|
|
3556
|
+
})) : request.reports.flatMap(
|
|
3557
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
3558
|
+
);
|
|
3559
|
+
const significant = contributed.filter(
|
|
3560
|
+
({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
|
|
3561
|
+
).sort(
|
|
3562
|
+
(a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
|
|
3563
|
+
);
|
|
2383
3564
|
for (const { finding } of significant) {
|
|
2384
3565
|
steps.push(remediationStep(finding, steps.length + 1));
|
|
2385
3566
|
}
|
|
@@ -2392,12 +3573,20 @@ var HeuristicPlanner = class {
|
|
|
2392
3573
|
});
|
|
2393
3574
|
}
|
|
2394
3575
|
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
3576
|
+
const conflicts = request.aggregated?.conflicts ?? [];
|
|
3577
|
+
if (conflicts.length > 0) {
|
|
3578
|
+
risks.push(
|
|
3579
|
+
`${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
2395
3582
|
return {
|
|
2396
3583
|
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
2397
3584
|
objective: request.objective,
|
|
3585
|
+
goal: request.aggregated?.title ?? truncate5(request.objective, 120),
|
|
2398
3586
|
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
2399
3587
|
steps,
|
|
2400
3588
|
...risks.length > 0 ? { risks } : {},
|
|
3589
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
2401
3590
|
generatedBy: "heuristic",
|
|
2402
3591
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2403
3592
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
@@ -2412,24 +3601,76 @@ function remediationStep(finding, order) {
|
|
|
2412
3601
|
description: finding.detail,
|
|
2413
3602
|
order,
|
|
2414
3603
|
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
2415
|
-
sourceFindings: [finding.id]
|
|
3604
|
+
sourceFindings: [finding.id],
|
|
3605
|
+
...finding.recommendation ? { recommendation: finding.recommendation } : {}
|
|
2416
3606
|
};
|
|
2417
3607
|
}
|
|
2418
3608
|
function dedupe(texts) {
|
|
2419
3609
|
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
2420
3610
|
}
|
|
3611
|
+
function slug(text) {
|
|
3612
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3613
|
+
}
|
|
2421
3614
|
function truncate5(text, max) {
|
|
2422
3615
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2423
3616
|
}
|
|
2424
3617
|
|
|
2425
3618
|
// src/planner/model.ts
|
|
2426
3619
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
2427
|
-
|
|
3620
|
+
|
|
3621
|
+
// src/planner/schema.ts
|
|
3622
|
+
import { z as z3 } from "zod";
|
|
3623
|
+
var planStepSchema = z3.object({
|
|
3624
|
+
id: z3.string().min(1).optional(),
|
|
3625
|
+
title: z3.string().min(1),
|
|
3626
|
+
description: z3.string().optional(),
|
|
3627
|
+
order: z3.number().int().optional(),
|
|
3628
|
+
targets: z3.array(z3.string()).optional(),
|
|
3629
|
+
acceptance_criteria: z3.array(z3.string()).optional(),
|
|
3630
|
+
depends_on: z3.array(z3.string()).optional(),
|
|
3631
|
+
source_findings: z3.array(z3.string()).optional()
|
|
3632
|
+
});
|
|
3633
|
+
var planJsonSchema = z3.object({
|
|
3634
|
+
goal: z3.string().min(1),
|
|
3635
|
+
summary: z3.string().min(1),
|
|
3636
|
+
steps: z3.array(planStepSchema).min(1),
|
|
3637
|
+
risks: z3.array(z3.string()).optional(),
|
|
3638
|
+
notes: z3.array(z3.string()).optional()
|
|
3639
|
+
});
|
|
3640
|
+
function validatePlan(plan) {
|
|
3641
|
+
const issues = [];
|
|
3642
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
3643
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
3644
|
+
if (stepIds.has(step.id)) {
|
|
3645
|
+
issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
|
|
3646
|
+
}
|
|
3647
|
+
stepIds.add(step.id);
|
|
3648
|
+
if (!step.title || step.title.trim().length === 0) {
|
|
3649
|
+
issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
|
|
3650
|
+
}
|
|
3651
|
+
for (const dep of step.dependsOn ?? []) {
|
|
3652
|
+
if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
|
|
3653
|
+
issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (plan.steps.length === 0) {
|
|
3658
|
+
issues.push({ path: "steps", message: "a plan needs at least one step" });
|
|
3659
|
+
}
|
|
3660
|
+
if (!plan.summary || plan.summary.trim().length === 0) {
|
|
3661
|
+
issues.push({ path: "summary", message: "summary must not be empty" });
|
|
3662
|
+
}
|
|
3663
|
+
return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
|
|
3664
|
+
}
|
|
2428
3665
|
|
|
2429
3666
|
// src/planner/context.ts
|
|
2430
3667
|
function buildPlannerContext(request) {
|
|
2431
|
-
const parts = [`#
|
|
3668
|
+
const parts = [`# Task
|
|
2432
3669
|
${request.objective}`];
|
|
3670
|
+
if (request.projectContext) {
|
|
3671
|
+
parts.push(`# Project context
|
|
3672
|
+
${request.projectContext}`);
|
|
3673
|
+
}
|
|
2433
3674
|
if (request.role) {
|
|
2434
3675
|
const roleLines = [`# Role
|
|
2435
3676
|
${request.role.name} (${request.role.id})`];
|
|
@@ -2449,7 +3690,7 @@ ${request.role.name} (${request.role.id})`];
|
|
|
2449
3690
|
)
|
|
2450
3691
|
);
|
|
2451
3692
|
parts.push(
|
|
2452
|
-
`#
|
|
3693
|
+
`# Agent reports (${request.reports.length} by ${agents})
|
|
2453
3694
|
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
2454
3695
|
);
|
|
2455
3696
|
const recommendationLines = request.reports.flatMap(
|
|
@@ -2459,9 +3700,20 @@ ${request.role.name} (${request.role.id})`];
|
|
|
2459
3700
|
parts.push(`# Recommendations
|
|
2460
3701
|
${recommendationLines.join("\n")}`);
|
|
2461
3702
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
3703
|
+
}
|
|
3704
|
+
if (request.aggregated) {
|
|
3705
|
+
const aggregatedLines = [
|
|
3706
|
+
`- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
|
|
3707
|
+
...request.aggregated.findings.map(
|
|
3708
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
|
|
3709
|
+
),
|
|
3710
|
+
// Conflicts stay visible to the planner — it must plan around disagreement.
|
|
3711
|
+
...request.aggregated.conflicts.map(
|
|
3712
|
+
(conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
|
|
3713
|
+
)
|
|
3714
|
+
];
|
|
3715
|
+
parts.push(`# Aggregated findings
|
|
3716
|
+
${aggregatedLines.join("\n")}`);
|
|
2465
3717
|
}
|
|
2466
3718
|
if (request.memory && request.memory.length > 0) {
|
|
2467
3719
|
parts.push(
|
|
@@ -2469,7 +3721,11 @@ ${summaryLines.join("\n")}`);
|
|
|
2469
3721
|
` + request.memory.map((entry2) => `- [${entry2.scope}] ${entry2.content}`).join("\n")
|
|
2470
3722
|
);
|
|
2471
3723
|
}
|
|
2472
|
-
if (request.
|
|
3724
|
+
if (request.constraints && request.constraints.length > 0) {
|
|
3725
|
+
parts.push(`# Constraints
|
|
3726
|
+
${request.constraints.map((c) => `- ${c}`).join("\n")}`);
|
|
3727
|
+
}
|
|
3728
|
+
if (request.context) {
|
|
2473
3729
|
parts.push(`# Research context
|
|
2474
3730
|
${request.context}`);
|
|
2475
3731
|
}
|
|
@@ -2477,22 +3733,6 @@ ${request.context}`);
|
|
|
2477
3733
|
}
|
|
2478
3734
|
|
|
2479
3735
|
// src/planner/model.ts
|
|
2480
|
-
var planStepJsonSchema = z2.object({
|
|
2481
|
-
id: z2.string().optional(),
|
|
2482
|
-
title: z2.string().min(1),
|
|
2483
|
-
description: z2.string().optional(),
|
|
2484
|
-
order: z2.number().int().optional(),
|
|
2485
|
-
targets: z2.array(z2.string()).optional(),
|
|
2486
|
-
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
2487
|
-
depends_on: z2.array(z2.string()).optional(),
|
|
2488
|
-
source_findings: z2.array(z2.string()).optional()
|
|
2489
|
-
}).strict();
|
|
2490
|
-
var planJsonSchema = z2.object({
|
|
2491
|
-
summary: z2.string().min(1),
|
|
2492
|
-
steps: z2.array(planStepJsonSchema).min(1),
|
|
2493
|
-
risks: z2.array(z2.string()).optional(),
|
|
2494
|
-
notes: z2.array(z2.string()).optional()
|
|
2495
|
-
}).strict();
|
|
2496
3736
|
var ModelPlanner = class {
|
|
2497
3737
|
constructor(options) {
|
|
2498
3738
|
this.options = options;
|
|
@@ -2506,7 +3746,7 @@ var ModelPlanner = class {
|
|
|
2506
3746
|
const messages = [
|
|
2507
3747
|
{
|
|
2508
3748
|
role: "system",
|
|
2509
|
-
content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
|
|
3749
|
+
content: 'You are the OmniNode planner. Given the objective and the combined research context, produce a final analysis and a prioritized, actionable implementation plan. Respond with JSON ONLY (no prose, no code fences) matching exactly this shape: {"goal": string, "summary": string, "steps": [{"id": string, "title": string, "description": string, "order": number, "targets": string[], "acceptance_criteria": string[], "source_findings": string[]}], "risks": string[], "notes": string[]}. ' + (this.options.instruction ?? "")
|
|
2510
3750
|
},
|
|
2511
3751
|
{ role: "user", content: context }
|
|
2512
3752
|
];
|
|
@@ -2517,60 +3757,86 @@ var ModelPlanner = class {
|
|
|
2517
3757
|
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
2518
3758
|
}
|
|
2519
3759
|
const parsed = parsePlanJson(responseText);
|
|
2520
|
-
if (!parsed) {
|
|
2521
|
-
return this.degrade(request,
|
|
3760
|
+
if (!parsed.ok) {
|
|
3761
|
+
return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
|
|
2522
3762
|
}
|
|
2523
3763
|
const plan = {
|
|
2524
3764
|
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
2525
3765
|
objective: request.objective,
|
|
3766
|
+
goal: parsed.goal,
|
|
2526
3767
|
summary: parsed.summary,
|
|
2527
3768
|
steps: parsed.steps,
|
|
2528
3769
|
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
2529
3770
|
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
3771
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
2530
3772
|
generatedBy: `model:${this.options.model}`,
|
|
2531
3773
|
model: this.options.model,
|
|
2532
3774
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
2533
3775
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
2534
3776
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2535
3777
|
};
|
|
2536
|
-
|
|
3778
|
+
const validation = validatePlan(plan);
|
|
3779
|
+
if (!validation.valid) {
|
|
3780
|
+
return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
|
|
3781
|
+
}
|
|
3782
|
+
return validation.plan;
|
|
2537
3783
|
}
|
|
2538
|
-
async degrade(request, reason) {
|
|
3784
|
+
async degrade(request, reason, issues = []) {
|
|
2539
3785
|
if (this.options.fallback) {
|
|
2540
3786
|
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
2541
3787
|
return this.options.fallback.plan(request);
|
|
2542
3788
|
}
|
|
2543
|
-
throw new PlannerError("
|
|
3789
|
+
throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
|
|
3790
|
+
details: { reason, issues }
|
|
3791
|
+
});
|
|
2544
3792
|
}
|
|
2545
3793
|
};
|
|
2546
3794
|
function parsePlanJson(text) {
|
|
2547
3795
|
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
2548
3796
|
const start = unfenced.indexOf("{");
|
|
2549
3797
|
const end = unfenced.lastIndexOf("}");
|
|
2550
|
-
if (start < 0 || end <= start)
|
|
3798
|
+
if (start < 0 || end <= start) {
|
|
3799
|
+
return { ok: false, reason: "planner output contained no JSON object", issues: [] };
|
|
3800
|
+
}
|
|
3801
|
+
let json;
|
|
2551
3802
|
try {
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
if (!parsed.success) return void 0;
|
|
2555
|
-
const steps = parsed.data.steps.map((step, index) => ({
|
|
2556
|
-
id: step.id ?? `step-${index + 1}`,
|
|
2557
|
-
title: step.title,
|
|
2558
|
-
description: step.description,
|
|
2559
|
-
order: step.order ?? index + 1,
|
|
2560
|
-
targets: step.targets,
|
|
2561
|
-
acceptanceCriteria: step.acceptance_criteria,
|
|
2562
|
-
dependsOn: step.depends_on,
|
|
2563
|
-
sourceFindings: step.source_findings
|
|
2564
|
-
}));
|
|
3803
|
+
json = JSON.parse(unfenced.slice(start, end + 1));
|
|
3804
|
+
} catch (error) {
|
|
2565
3805
|
return {
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
3806
|
+
ok: false,
|
|
3807
|
+
reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
3808
|
+
issues: []
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
3812
|
+
if (!parsed.success) {
|
|
3813
|
+
return {
|
|
3814
|
+
ok: false,
|
|
3815
|
+
reason: "planner output did not match the plan schema",
|
|
3816
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
3817
|
+
path: issue.path.join(".") || "(root)",
|
|
3818
|
+
message: issue.message
|
|
3819
|
+
}))
|
|
2570
3820
|
};
|
|
2571
|
-
} catch {
|
|
2572
|
-
return void 0;
|
|
2573
3821
|
}
|
|
3822
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
3823
|
+
id: step.id ?? `step-${index + 1}`,
|
|
3824
|
+
title: step.title,
|
|
3825
|
+
description: step.description,
|
|
3826
|
+
order: step.order ?? index + 1,
|
|
3827
|
+
targets: step.targets,
|
|
3828
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
3829
|
+
dependsOn: step.depends_on,
|
|
3830
|
+
sourceFindings: step.source_findings
|
|
3831
|
+
})).sort((a, b) => a.order - b.order);
|
|
3832
|
+
return {
|
|
3833
|
+
ok: true,
|
|
3834
|
+
goal: parsed.data.goal,
|
|
3835
|
+
summary: parsed.data.summary,
|
|
3836
|
+
steps,
|
|
3837
|
+
risks: parsed.data.risks,
|
|
3838
|
+
notes: parsed.data.notes
|
|
3839
|
+
};
|
|
2574
3840
|
}
|
|
2575
3841
|
function describe(error) {
|
|
2576
3842
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -2584,9 +3850,23 @@ var PipelineEngine = class {
|
|
|
2584
3850
|
}
|
|
2585
3851
|
options;
|
|
2586
3852
|
log;
|
|
3853
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3854
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
2587
3855
|
get store() {
|
|
2588
3856
|
return this.options.store;
|
|
2589
3857
|
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Saves the run while preserving fields that may be set by another process
|
|
3860
|
+
* (e.g. a cancellation request) — an in-memory snapshot must never clobber
|
|
3861
|
+
* externally requested cancellation (§9.2).
|
|
3862
|
+
*/
|
|
3863
|
+
async persist(run2) {
|
|
3864
|
+
const current = await this.options.store.get(run2.id);
|
|
3865
|
+
await this.options.store.save({
|
|
3866
|
+
...run2,
|
|
3867
|
+
...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
2590
3870
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
2591
3871
|
validate(def) {
|
|
2592
3872
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -2686,18 +3966,44 @@ var PipelineEngine = class {
|
|
|
2686
3966
|
);
|
|
2687
3967
|
}
|
|
2688
3968
|
}
|
|
3969
|
+
if (step.model) {
|
|
3970
|
+
const separator = step.model.indexOf(":");
|
|
3971
|
+
if (separator <= 0 || separator === step.model.length - 1) {
|
|
3972
|
+
throw new PipelineError(
|
|
3973
|
+
"PIPELINE_INVALID",
|
|
3974
|
+
`Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
const providerName = step.model.slice(0, separator);
|
|
3978
|
+
if (!this.options.providers.some((p) => p.name === providerName)) {
|
|
3979
|
+
throw new PipelineError(
|
|
3980
|
+
"PIPELINE_INVALID",
|
|
3981
|
+
`Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
|
|
3982
|
+
);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
2689
3985
|
}
|
|
2690
3986
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2691
3987
|
const run2 = {
|
|
2692
3988
|
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2693
3989
|
pipelineId: def.id,
|
|
3990
|
+
attempt: 1,
|
|
2694
3991
|
objective,
|
|
2695
3992
|
status: "running",
|
|
2696
3993
|
startedAt: now,
|
|
2697
3994
|
stepRuns: []
|
|
2698
3995
|
};
|
|
2699
3996
|
await this.options.store.save(run2);
|
|
2700
|
-
|
|
3997
|
+
const liveTaskIds = /* @__PURE__ */ new Set();
|
|
3998
|
+
this.activeRuns.set(run2.id, {
|
|
3999
|
+
taskIds: liveTaskIds,
|
|
4000
|
+
cancelTasks: async () => {
|
|
4001
|
+
for (const taskId of liveTaskIds) {
|
|
4002
|
+
await this.options.tasks.cancel(taskId).catch(() => void 0);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
});
|
|
4006
|
+
this.log.info(`Running pipeline ${def.id} as ${run2.id} (attempt ${run2.attempt}).`);
|
|
2701
4007
|
await this.options.audit?.record({
|
|
2702
4008
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2703
4009
|
action: "pipeline.run.started",
|
|
@@ -2709,7 +4015,22 @@ var PipelineEngine = class {
|
|
|
2709
4015
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
2710
4016
|
const contextParts = [];
|
|
2711
4017
|
const allReports = [];
|
|
4018
|
+
let cancelled = false;
|
|
2712
4019
|
while (pending.size > 0) {
|
|
4020
|
+
const persisted = await this.options.store.get(run2.id);
|
|
4021
|
+
if (persisted?.cancellationRequested === true) {
|
|
4022
|
+
cancelled = true;
|
|
4023
|
+
run2.cancellationRequested = true;
|
|
4024
|
+
run2.cancellationRequestedAt = persisted.cancellationRequestedAt;
|
|
4025
|
+
await this.activeRuns.get(run2.id)?.cancelTasks();
|
|
4026
|
+
for (const step of def.steps.filter((s) => pending.has(s.id))) {
|
|
4027
|
+
const stepRun = { stepId: step.id, status: "cancelled" };
|
|
4028
|
+
run2.stepRuns.push(stepRun);
|
|
4029
|
+
options.onStep?.(stepRun);
|
|
4030
|
+
}
|
|
4031
|
+
pending.clear();
|
|
4032
|
+
break;
|
|
4033
|
+
}
|
|
2713
4034
|
const ready = def.steps.filter(
|
|
2714
4035
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
2715
4036
|
);
|
|
@@ -2726,7 +4047,8 @@ var PipelineEngine = class {
|
|
|
2726
4047
|
...role ? { role } : {},
|
|
2727
4048
|
combinedContext: contextParts.join("\n\n"),
|
|
2728
4049
|
reports: [...allReports],
|
|
2729
|
-
pipelineRunId: run2.id
|
|
4050
|
+
pipelineRunId: run2.id,
|
|
4051
|
+
runId: run2.id
|
|
2730
4052
|
};
|
|
2731
4053
|
return this.executeStep(step, ctx);
|
|
2732
4054
|
})
|
|
@@ -2743,7 +4065,9 @@ ${contribution}`);
|
|
|
2743
4065
|
}
|
|
2744
4066
|
const stepRun = {
|
|
2745
4067
|
stepId: step.id,
|
|
2746
|
-
|
|
4068
|
+
attempt: outcome.attempt ?? 1,
|
|
4069
|
+
...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
|
|
4070
|
+
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
2747
4071
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2748
4072
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
2749
4073
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
@@ -2752,10 +4076,12 @@ ${contribution}`);
|
|
|
2752
4076
|
options.onStep?.(stepRun);
|
|
2753
4077
|
});
|
|
2754
4078
|
run2.reports = [...allReports];
|
|
2755
|
-
await this.
|
|
4079
|
+
await this.persist(run2);
|
|
2756
4080
|
}
|
|
2757
|
-
const
|
|
2758
|
-
const
|
|
4081
|
+
const allOutcomes = [...outcomes.values()];
|
|
4082
|
+
const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
|
|
4083
|
+
const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
|
|
4084
|
+
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
|
|
2759
4085
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2760
4086
|
let resultSummary;
|
|
2761
4087
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -2783,16 +4109,19 @@ ${contribution}`);
|
|
|
2783
4109
|
run2.reports = collected;
|
|
2784
4110
|
}
|
|
2785
4111
|
}
|
|
4112
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4113
|
+
this.activeRuns.delete(run2.id);
|
|
2786
4114
|
const finished = {
|
|
2787
4115
|
...run2,
|
|
2788
4116
|
status: finalStatus,
|
|
2789
|
-
finishedAt
|
|
4117
|
+
finishedAt,
|
|
4118
|
+
attempts: [...run2.attempts ?? [], { attempt: run2.attempt, startedAt: run2.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
2790
4119
|
...run2.reports !== void 0 && run2.reports.length > 0 ? { reports: run2.reports } : {},
|
|
2791
4120
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2792
4121
|
...planId !== void 0 ? { planId } : {},
|
|
2793
4122
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
2794
4123
|
};
|
|
2795
|
-
await this.
|
|
4124
|
+
await this.persist(finished);
|
|
2796
4125
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
2797
4126
|
await this.options.audit?.record({
|
|
2798
4127
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2802,6 +4131,23 @@ ${contribution}`);
|
|
|
2802
4131
|
});
|
|
2803
4132
|
return finished;
|
|
2804
4133
|
}
|
|
4134
|
+
/** First-class cancellation (§9.2): requests cancellation and terminates
|
|
4135
|
+
* tasks that are running in this process immediately. */
|
|
4136
|
+
async cancel(runId) {
|
|
4137
|
+
const run2 = await this.options.store.get(runId);
|
|
4138
|
+
if (!run2) {
|
|
4139
|
+
throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
|
|
4140
|
+
}
|
|
4141
|
+
await this.options.store.save({
|
|
4142
|
+
...run2,
|
|
4143
|
+
cancellationRequested: true,
|
|
4144
|
+
cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4145
|
+
});
|
|
4146
|
+
const active = this.activeRuns.get(runId);
|
|
4147
|
+
if (active) {
|
|
4148
|
+
await active.cancelTasks();
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
2805
4151
|
async get(runId) {
|
|
2806
4152
|
return this.options.store.get(runId);
|
|
2807
4153
|
}
|
|
@@ -2825,9 +4171,16 @@ ${contribution}`);
|
|
|
2825
4171
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
2826
4172
|
}
|
|
2827
4173
|
last = await this.executeStepOnce(step, ctx);
|
|
2828
|
-
if (last.status !== "failed") return last;
|
|
4174
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
2829
4175
|
}
|
|
2830
|
-
return last;
|
|
4176
|
+
return { ...last, attempt: retries + 1 };
|
|
4177
|
+
}
|
|
4178
|
+
/** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
|
|
4179
|
+
trackLiveTasks(runId, taskIds) {
|
|
4180
|
+
if (!runId) return;
|
|
4181
|
+
const active = this.activeRuns.get(runId);
|
|
4182
|
+
if (!active) return;
|
|
4183
|
+
for (const id of taskIds) active.taskIds.add(id);
|
|
2831
4184
|
}
|
|
2832
4185
|
async executeStepOnce(step, ctx) {
|
|
2833
4186
|
switch (step.kind) {
|
|
@@ -2860,7 +4213,10 @@ ${contribution}`);
|
|
|
2860
4213
|
})
|
|
2861
4214
|
)
|
|
2862
4215
|
);
|
|
4216
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
2863
4217
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
4218
|
+
const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
|
|
4219
|
+
const completedTasks = results.filter((r) => r.status === "completed");
|
|
2864
4220
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2865
4221
|
const lines = results.map((result, index) => {
|
|
2866
4222
|
const agentName = agentNames[index] ?? "unknown";
|
|
@@ -2870,16 +4226,25 @@ ${contribution}`);
|
|
|
2870
4226
|
const reports = results.flatMap((r) => r.result?.reports ?? []);
|
|
2871
4227
|
const summary = lines.join("\n");
|
|
2872
4228
|
if (failedTasks.length > 0) {
|
|
2873
|
-
const firstError = failedTasks[0]?.result?.error ?? "unknown
|
|
4229
|
+
const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
|
|
4230
|
+
const states = failedTasks.map((r) => r.status).join(", ");
|
|
2874
4231
|
return {
|
|
2875
|
-
|
|
4232
|
+
// Some agents succeeded → the step produced usable but incomplete
|
|
4233
|
+
// intelligence: partial, not failed (roadmap §9.3).
|
|
4234
|
+
status: completedTasks.length > 0 ? "partial" : "failed",
|
|
2876
4235
|
summary,
|
|
2877
4236
|
taskIds: created.map((t) => t.id),
|
|
2878
|
-
error: `${failedTasks.length}/${results.length} task(s)
|
|
4237
|
+
error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
|
|
2879
4238
|
reports
|
|
2880
4239
|
};
|
|
2881
4240
|
}
|
|
2882
|
-
return {
|
|
4241
|
+
return {
|
|
4242
|
+
status: "completed",
|
|
4243
|
+
summary,
|
|
4244
|
+
taskIds: created.map((t) => t.id),
|
|
4245
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
4246
|
+
reports
|
|
4247
|
+
};
|
|
2883
4248
|
}
|
|
2884
4249
|
async runSingle(step, ctx, agentName) {
|
|
2885
4250
|
const task = await this.options.tasks.create({
|
|
@@ -2888,10 +4253,13 @@ ${contribution}`);
|
|
|
2888
4253
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
2889
4254
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2890
4255
|
});
|
|
4256
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
2891
4257
|
const result = await this.options.tasks.run(task.id);
|
|
2892
4258
|
if (result.status !== "completed") {
|
|
2893
4259
|
return {
|
|
2894
|
-
|
|
4260
|
+
// Unknown/timed_out outcomes are not failures — the work may have
|
|
4261
|
+
// happened remotely; the step is incomplete, not broken.
|
|
4262
|
+
status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
|
|
2895
4263
|
summary: result.result?.summary ?? "",
|
|
2896
4264
|
taskIds: [task.id],
|
|
2897
4265
|
error: result.result?.error ?? `task ${task.id} did not complete`,
|
|
@@ -2906,11 +4274,14 @@ ${contribution}`);
|
|
|
2906
4274
|
};
|
|
2907
4275
|
}
|
|
2908
4276
|
async runPlan(step, ctx) {
|
|
4277
|
+
const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports) : void 0;
|
|
2909
4278
|
const request = {
|
|
2910
4279
|
objective: ctx.objective,
|
|
2911
4280
|
reports: ctx.reports,
|
|
4281
|
+
...aggregated !== void 0 ? { aggregated } : {},
|
|
2912
4282
|
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2913
4283
|
...ctx.role ? { role: ctx.role } : {},
|
|
4284
|
+
...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
|
|
2914
4285
|
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2915
4286
|
};
|
|
2916
4287
|
let planner;
|
|
@@ -3078,7 +4449,11 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
3078
4449
|
agents,
|
|
3079
4450
|
roles,
|
|
3080
4451
|
store: new FileTaskStore(),
|
|
3081
|
-
...config.project.memory !== void 0 ? {
|
|
4452
|
+
...config.project.memory !== void 0 ? {
|
|
4453
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
4454
|
+
required: config.project.memory.required
|
|
4455
|
+
})
|
|
4456
|
+
} : {},
|
|
3082
4457
|
audit: new FileAuditLog(),
|
|
3083
4458
|
log
|
|
3084
4459
|
});
|
|
@@ -3136,6 +4511,24 @@ function registerPipelineCommands(program) {
|
|
|
3136
4511
|
}
|
|
3137
4512
|
await runPipelineAction(def.id, objective ?? previous.objective);
|
|
3138
4513
|
});
|
|
4514
|
+
pipeline.command("cancel <run-id>").description("Request cancellation of a pipeline run (stops scheduling and cancels running tasks).").action(async (runId) => {
|
|
4515
|
+
const config = loadConfig();
|
|
4516
|
+
const engine = buildPipelineEngine(config);
|
|
4517
|
+
await engine.cancel(runId);
|
|
4518
|
+
console.log(`Cancellation requested for run ${runId}.`);
|
|
4519
|
+
console.log("A run executing in another process stops at its next check point.");
|
|
4520
|
+
});
|
|
4521
|
+
pipeline.command("validate <id>").description("Validate a pipeline definition before running it (\xA79.5).").action(async (id) => {
|
|
4522
|
+
const config = loadConfig();
|
|
4523
|
+
const def = findPipeline(config, id);
|
|
4524
|
+
const engine = buildPipelineEngine(config);
|
|
4525
|
+
engine.validate(def);
|
|
4526
|
+
console.log(`Pipeline "${def.id}" is valid (${def.steps.length} step(s)).`);
|
|
4527
|
+
for (const step of def.steps) {
|
|
4528
|
+
const deps = step.dependsOn?.join(", ") ?? "(previous step)";
|
|
4529
|
+
console.log(` ${step.id} kind=${step.kind} depends_on=${deps}`);
|
|
4530
|
+
}
|
|
4531
|
+
});
|
|
3139
4532
|
pipeline.command("runs <id>").description("List recorded runs of a pipeline.").action(async (id) => {
|
|
3140
4533
|
const config = loadConfig();
|
|
3141
4534
|
const engine = buildPipelineEngine(config);
|
|
@@ -3145,7 +4538,8 @@ function registerPipelineCommands(program) {
|
|
|
3145
4538
|
return;
|
|
3146
4539
|
}
|
|
3147
4540
|
for (const run2 of runs) {
|
|
3148
|
-
|
|
4541
|
+
const note = isInterrupted(run2) ? " (interrupted \u2014 process gone before finish)" : "";
|
|
4542
|
+
console.log(`${run2.id} ${run2.status.padEnd(10)} ${run2.startedAt ?? "?"}${note}`);
|
|
3149
4543
|
}
|
|
3150
4544
|
});
|
|
3151
4545
|
pipeline.command("status <run-id>").description("Show the recorded state of a pipeline run.").action(async (runId) => {
|
|
@@ -3163,6 +4557,13 @@ function registerPipelineCommands(program) {
|
|
|
3163
4557
|
console.log(` status: ${run2.status}`);
|
|
3164
4558
|
console.log(` started: ${run2.startedAt ?? "?"}`);
|
|
3165
4559
|
if (run2.finishedAt) console.log(` finished: ${run2.finishedAt}`);
|
|
4560
|
+
console.log(` attempt: ${run2.attempt}`);
|
|
4561
|
+
if (run2.cancellationRequested) {
|
|
4562
|
+
console.log(` cancellation requested at ${run2.cancellationRequestedAt ?? "?"}`);
|
|
4563
|
+
}
|
|
4564
|
+
if (isInterrupted(run2)) {
|
|
4565
|
+
console.log(" note: this run looks interrupted (no finishedAt) \u2014 retry with `pipeline retry`");
|
|
4566
|
+
}
|
|
3166
4567
|
if (run2.resultSummary) console.log(` result: ${run2.resultSummary}`);
|
|
3167
4568
|
for (const stepRun of run2.stepRuns) {
|
|
3168
4569
|
console.log(` [${stepRun.stepId}] ${stepRun.status}`);
|
|
@@ -3204,6 +4605,9 @@ async function runPipelineAction(id, objective) {
|
|
|
3204
4605
|
function registerRunAlias(program) {
|
|
3205
4606
|
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
4607
|
}
|
|
4608
|
+
function isInterrupted(run2) {
|
|
4609
|
+
return run2.status === "running" && run2.finishedAt === void 0;
|
|
4610
|
+
}
|
|
3207
4611
|
function findPipeline(config, id) {
|
|
3208
4612
|
const def = config.project.pipelines.find((p) => p.id === id);
|
|
3209
4613
|
if (!def) {
|