@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/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// src/types/task.ts
|
|
2
|
+
var TERMINAL_TASK_STATUSES = [
|
|
3
|
+
"completed",
|
|
4
|
+
"failed",
|
|
5
|
+
"timed_out",
|
|
6
|
+
"cancelled",
|
|
7
|
+
"partially_completed"
|
|
8
|
+
];
|
|
9
|
+
|
|
1
10
|
// src/errors/index.ts
|
|
2
11
|
var OmniNodeError = class extends Error {
|
|
3
12
|
code;
|
|
@@ -116,6 +125,7 @@ var providerConfigSchema = z.object({
|
|
|
116
125
|
type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
|
|
117
126
|
base_url: z.string().url(),
|
|
118
127
|
api_key_env_var: z.string().min(1).optional(),
|
|
128
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
119
129
|
headers: z.record(z.string()).optional(),
|
|
120
130
|
enabled: z.boolean().optional(),
|
|
121
131
|
metadata: z.record(z.unknown()).optional()
|
|
@@ -131,6 +141,11 @@ var agentConfigSchema = z.object({
|
|
|
131
141
|
input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
|
|
132
142
|
timeout_ms: z.number().int().positive().optional(),
|
|
133
143
|
inherit_env: z.boolean().optional(),
|
|
144
|
+
env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
|
|
145
|
+
env_allowlist: z.array(z.string()).optional(),
|
|
146
|
+
env_denylist: z.array(z.string()).optional(),
|
|
147
|
+
max_output_bytes: z.number().int().positive().optional(),
|
|
148
|
+
allow_external_cwd: z.boolean().optional(),
|
|
134
149
|
metadata: z.record(z.unknown()).optional()
|
|
135
150
|
}).strict();
|
|
136
151
|
var roleConfigSchema = z.object({
|
|
@@ -151,7 +166,9 @@ var pipelineStepConfigSchema = z.object({
|
|
|
151
166
|
model: z.string().optional(),
|
|
152
167
|
depends_on: z.array(z.string()).optional(),
|
|
153
168
|
retries: z.number().int().min(0).max(10).optional(),
|
|
154
|
-
condition: z.enum(["always", "on-success", "on-failure"]).optional()
|
|
169
|
+
condition: z.enum(["always", "on-success", "on-failure"]).optional(),
|
|
170
|
+
/** Constraints passed to the planner for plan steps (§15). */
|
|
171
|
+
constraints: z.array(z.string()).optional()
|
|
155
172
|
}).strict();
|
|
156
173
|
var pipelineConfigSchema = z.object({
|
|
157
174
|
id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
|
|
@@ -175,7 +192,9 @@ var projectConfigSchema = z.object({
|
|
|
175
192
|
memory: z.object({
|
|
176
193
|
provider: z.string().min(1).default("local"),
|
|
177
194
|
base_url: z.string().url().optional(),
|
|
178
|
-
api_key_env_var: z.string().min(1).optional()
|
|
195
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
196
|
+
/** When true, memory failures fail tasks instead of being tolerated (§12). */
|
|
197
|
+
required: z.boolean().optional()
|
|
179
198
|
}).strict().optional()
|
|
180
199
|
}).strict();
|
|
181
200
|
var appConfigSchema = z.object({
|
|
@@ -257,7 +276,8 @@ function toAppConfig(yaml) {
|
|
|
257
276
|
memory: {
|
|
258
277
|
provider: yaml.project.memory.provider,
|
|
259
278
|
baseUrl: yaml.project.memory.base_url,
|
|
260
|
-
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
279
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var,
|
|
280
|
+
required: yaml.project.memory.required
|
|
261
281
|
}
|
|
262
282
|
} : {}
|
|
263
283
|
},
|
|
@@ -278,7 +298,8 @@ function toPipelineDefinition(raw) {
|
|
|
278
298
|
model: step.model,
|
|
279
299
|
dependsOn: step.depends_on,
|
|
280
300
|
retries: step.retries,
|
|
281
|
-
condition: step.condition
|
|
301
|
+
condition: step.condition,
|
|
302
|
+
constraints: step.constraints
|
|
282
303
|
}))
|
|
283
304
|
};
|
|
284
305
|
}
|
|
@@ -288,6 +309,7 @@ function toProviderConfig(raw) {
|
|
|
288
309
|
type: raw.type,
|
|
289
310
|
baseUrl: raw.base_url,
|
|
290
311
|
apiKeyEnvVar: raw.api_key_env_var,
|
|
312
|
+
timeoutMs: raw.timeout_ms,
|
|
291
313
|
headers: raw.headers,
|
|
292
314
|
enabled: raw.enabled,
|
|
293
315
|
metadata: raw.metadata
|
|
@@ -305,6 +327,11 @@ function toAgentConfig(raw) {
|
|
|
305
327
|
inputMode: raw.input_mode,
|
|
306
328
|
timeoutMs: raw.timeout_ms,
|
|
307
329
|
inheritEnv: raw.inherit_env,
|
|
330
|
+
envPolicy: raw.env_policy,
|
|
331
|
+
envAllowlist: raw.env_allowlist,
|
|
332
|
+
envDenylist: raw.env_denylist,
|
|
333
|
+
maxOutputBytes: raw.max_output_bytes,
|
|
334
|
+
allowExternalCwd: raw.allow_external_cwd,
|
|
308
335
|
metadata: raw.metadata
|
|
309
336
|
};
|
|
310
337
|
}
|
|
@@ -375,57 +402,200 @@ function authHeaders(config, env = process.env) {
|
|
|
375
402
|
}
|
|
376
403
|
|
|
377
404
|
// src/providers/http.ts
|
|
405
|
+
var HttpResponseError = class extends Error {
|
|
406
|
+
constructor(message, status, cause) {
|
|
407
|
+
super(message, { cause });
|
|
408
|
+
this.status = status;
|
|
409
|
+
this.name = "HttpResponseError";
|
|
410
|
+
}
|
|
411
|
+
status;
|
|
412
|
+
};
|
|
378
413
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
379
414
|
async function requestJson(url, options = {}) {
|
|
380
415
|
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
392
|
-
});
|
|
393
|
-
} catch (error) {
|
|
394
|
-
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
395
|
-
throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
|
|
396
|
-
cause: error
|
|
397
|
-
});
|
|
398
|
-
}
|
|
416
|
+
const response = await fetch(url, {
|
|
417
|
+
method,
|
|
418
|
+
headers: {
|
|
419
|
+
accept: "application/json",
|
|
420
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
421
|
+
...headers
|
|
422
|
+
},
|
|
423
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
424
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
425
|
+
});
|
|
399
426
|
let parsed;
|
|
400
427
|
try {
|
|
401
428
|
parsed = await response.json();
|
|
402
429
|
} catch (error) {
|
|
403
|
-
throw new
|
|
404
|
-
cause: error,
|
|
405
|
-
details: { status: response.status }
|
|
406
|
-
});
|
|
430
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
407
431
|
}
|
|
408
432
|
return { status: response.status, body: parsed };
|
|
409
433
|
}
|
|
410
434
|
|
|
435
|
+
// src/providers/errors.ts
|
|
436
|
+
function kindToCode(kind) {
|
|
437
|
+
switch (kind) {
|
|
438
|
+
case "AUTHENTICATION_ERROR":
|
|
439
|
+
return "PROVIDER_AUTH_FAILED";
|
|
440
|
+
case "MODEL_NOT_FOUND":
|
|
441
|
+
return "MODEL_NOT_FOUND";
|
|
442
|
+
default:
|
|
443
|
+
return "PROVIDER_UNAVAILABLE";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function isRetryable(kind) {
|
|
447
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
448
|
+
}
|
|
449
|
+
function kindFromStatus(status) {
|
|
450
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
451
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
452
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
453
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
454
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
455
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
456
|
+
return "UNKNOWN_ERROR";
|
|
457
|
+
}
|
|
458
|
+
function extractMessage(body) {
|
|
459
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
460
|
+
const root = body;
|
|
461
|
+
const error = root.error ?? root;
|
|
462
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
463
|
+
return typeof message === "string" ? message : void 0;
|
|
464
|
+
}
|
|
465
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
466
|
+
if (!value) return void 0;
|
|
467
|
+
const seconds = Number(value);
|
|
468
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
469
|
+
const date = Date.parse(value);
|
|
470
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
471
|
+
}
|
|
472
|
+
function isTimeoutError(error) {
|
|
473
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
474
|
+
}
|
|
475
|
+
function isNetworkError(error) {
|
|
476
|
+
return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
|
|
477
|
+
}
|
|
478
|
+
function fallbackMessage(kind, context, status) {
|
|
479
|
+
const { provider, operation } = context;
|
|
480
|
+
switch (kind) {
|
|
481
|
+
case "AUTHENTICATION_ERROR":
|
|
482
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
483
|
+
case "RATE_LIMIT_ERROR":
|
|
484
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
485
|
+
case "MODEL_NOT_FOUND":
|
|
486
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
487
|
+
case "TIMEOUT":
|
|
488
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
489
|
+
case "SERVER_ERROR":
|
|
490
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
491
|
+
case "INVALID_REQUEST":
|
|
492
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
493
|
+
default:
|
|
494
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function providerHttpError(context) {
|
|
498
|
+
const status = context.status ?? 0;
|
|
499
|
+
const kind = kindFromStatus(status);
|
|
500
|
+
const detail = extractMessage(context.body);
|
|
501
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
502
|
+
return new ProviderError(
|
|
503
|
+
context.code ?? kindToCode(kind),
|
|
504
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
505
|
+
{
|
|
506
|
+
cause: context.cause,
|
|
507
|
+
details: {
|
|
508
|
+
kind,
|
|
509
|
+
provider: context.provider,
|
|
510
|
+
operation: context.operation,
|
|
511
|
+
status,
|
|
512
|
+
retryable: isRetryable(kind),
|
|
513
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
514
|
+
...detail ? { providerMessage: detail } : {}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
function providerTransportError(context) {
|
|
520
|
+
const cause = context.cause;
|
|
521
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
522
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
523
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
524
|
+
cause: context.cause,
|
|
525
|
+
details: {
|
|
526
|
+
kind,
|
|
527
|
+
provider: context.provider,
|
|
528
|
+
operation: context.operation,
|
|
529
|
+
retryable: isRetryable(kind)
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
411
534
|
// src/providers/openai-compatible/index.ts
|
|
535
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
412
536
|
var OpenAICompatibleProvider = class {
|
|
413
537
|
config;
|
|
538
|
+
providerId;
|
|
539
|
+
authentication;
|
|
414
540
|
log;
|
|
415
|
-
|
|
541
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
542
|
+
discovered;
|
|
543
|
+
env;
|
|
544
|
+
constructor(config, log = logger, env = process.env) {
|
|
416
545
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
546
|
+
this.env = env;
|
|
547
|
+
this.providerId = config.providerId ?? config.name;
|
|
548
|
+
this.authentication = {
|
|
549
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
550
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
551
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
552
|
+
};
|
|
417
553
|
this.log = log.child({ provider: config.name });
|
|
418
554
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
555
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
556
|
+
get requestTimeoutMs() {
|
|
557
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
558
|
+
}
|
|
559
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
560
|
+
async connect() {
|
|
561
|
+
authHeaders(this.config, this.env);
|
|
562
|
+
const models = await this.listModels();
|
|
563
|
+
this.discovered = models;
|
|
564
|
+
return models;
|
|
565
|
+
}
|
|
566
|
+
async getModel(modelId) {
|
|
567
|
+
const models = this.discovered ?? await this.connect();
|
|
568
|
+
return models.find((m) => m.id === modelId);
|
|
569
|
+
}
|
|
570
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
571
|
+
async rawModelsResponse() {
|
|
572
|
+
const response = await this.modelsRequest();
|
|
573
|
+
return response.body;
|
|
574
|
+
}
|
|
575
|
+
async modelsRequest() {
|
|
576
|
+
let response;
|
|
577
|
+
try {
|
|
578
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
579
|
+
headers: authHeaders(this.config, this.env),
|
|
580
|
+
timeoutMs: this.requestTimeoutMs
|
|
581
|
+
});
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error instanceof ProviderError) throw error;
|
|
584
|
+
throw providerTransportError({ provider: this.config.name, operation: "listModels", cause: error });
|
|
425
585
|
}
|
|
586
|
+
const { status, body } = response;
|
|
426
587
|
if (status !== 200) {
|
|
427
|
-
throw
|
|
588
|
+
throw providerHttpError({
|
|
589
|
+
provider: this.config.name,
|
|
590
|
+
operation: "listModels",
|
|
591
|
+
status,
|
|
592
|
+
body
|
|
593
|
+
});
|
|
428
594
|
}
|
|
595
|
+
return response;
|
|
596
|
+
}
|
|
597
|
+
async listModels() {
|
|
598
|
+
const body = await this.rawModelsResponse();
|
|
429
599
|
const entries = extractModelEntries(body, this.config);
|
|
430
600
|
const models = [];
|
|
431
601
|
let skipped = 0;
|
|
@@ -434,22 +604,44 @@ var OpenAICompatibleProvider = class {
|
|
|
434
604
|
skipped += 1;
|
|
435
605
|
continue;
|
|
436
606
|
}
|
|
437
|
-
models.push(
|
|
438
|
-
provider: this.config.name,
|
|
439
|
-
id: entry.id,
|
|
440
|
-
displayName: entry.id,
|
|
441
|
-
status: "available",
|
|
442
|
-
metadata: {
|
|
443
|
-
...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
|
|
444
|
-
...typeof entry.created === "number" ? { createdAt: entry.created } : {}
|
|
445
|
-
}
|
|
446
|
-
});
|
|
607
|
+
models.push(this.toModelInfo(entry));
|
|
447
608
|
}
|
|
448
609
|
if (skipped > 0) {
|
|
449
610
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
450
611
|
}
|
|
451
612
|
return models;
|
|
452
613
|
}
|
|
614
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
615
|
+
toModelInfo(entry) {
|
|
616
|
+
const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
617
|
+
const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
|
|
618
|
+
const inputTypes = normalizeModalities(entry.input_modalities);
|
|
619
|
+
const outputTypes = normalizeModalities(entry.output_modalities);
|
|
620
|
+
return {
|
|
621
|
+
provider: this.config.name,
|
|
622
|
+
id: entry.id,
|
|
623
|
+
displayName: entry.id,
|
|
624
|
+
...typeof entry.name === "string" ? { name: entry.name } : {},
|
|
625
|
+
status: "available",
|
|
626
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
627
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
628
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
629
|
+
...parameters !== void 0 ? {
|
|
630
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
631
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
632
|
+
supportsStreaming: parameters.includes("stream")
|
|
633
|
+
} : {},
|
|
634
|
+
metadata: {
|
|
635
|
+
...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
|
|
636
|
+
...typeof entry.created === "number" ? { createdAt: entry.created } : {},
|
|
637
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/** Streaming where the gateway supports it (§11). */
|
|
642
|
+
stream(request, onChunk) {
|
|
643
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
644
|
+
}
|
|
453
645
|
async healthCheck() {
|
|
454
646
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
455
647
|
try {
|
|
@@ -479,27 +671,35 @@ var OpenAICompatibleProvider = class {
|
|
|
479
671
|
}
|
|
480
672
|
}
|
|
481
673
|
async chat(request) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
);
|
|
674
|
+
let response;
|
|
675
|
+
try {
|
|
676
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: authHeaders(this.config, this.env),
|
|
679
|
+
timeoutMs: this.requestTimeoutMs,
|
|
680
|
+
body: {
|
|
681
|
+
model: request.model,
|
|
682
|
+
messages: request.messages,
|
|
683
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
684
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
685
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (error instanceof ProviderError) throw error;
|
|
690
|
+
throw providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
500
691
|
}
|
|
692
|
+
const { status, body } = response;
|
|
501
693
|
if (status !== 200) {
|
|
502
|
-
throw
|
|
694
|
+
throw providerHttpError({
|
|
695
|
+
provider: this.config.name,
|
|
696
|
+
operation: "chat",
|
|
697
|
+
status,
|
|
698
|
+
body,
|
|
699
|
+
...status === 404 ? {
|
|
700
|
+
code: "MODEL_NOT_FOUND"
|
|
701
|
+
} : {}
|
|
702
|
+
});
|
|
503
703
|
}
|
|
504
704
|
const payload = body;
|
|
505
705
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -521,6 +721,81 @@ var OpenAICompatibleProvider = class {
|
|
|
521
721
|
};
|
|
522
722
|
}
|
|
523
723
|
};
|
|
724
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
725
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
726
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
727
|
+
let response;
|
|
728
|
+
try {
|
|
729
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
730
|
+
method: "POST",
|
|
731
|
+
headers: {
|
|
732
|
+
accept: "text/event-stream",
|
|
733
|
+
"content-type": "application/json",
|
|
734
|
+
...authHeaders(config, env)
|
|
735
|
+
},
|
|
736
|
+
body: JSON.stringify({
|
|
737
|
+
model: request.model,
|
|
738
|
+
messages: request.messages,
|
|
739
|
+
stream: true,
|
|
740
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
741
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
742
|
+
}),
|
|
743
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
744
|
+
});
|
|
745
|
+
} catch (error) {
|
|
746
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
747
|
+
}
|
|
748
|
+
if (!response.ok) {
|
|
749
|
+
const body = await response.json().catch(() => void 0);
|
|
750
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
751
|
+
}
|
|
752
|
+
if (!response.body) {
|
|
753
|
+
throw providerHttpError({
|
|
754
|
+
provider: config.name,
|
|
755
|
+
operation: "stream",
|
|
756
|
+
status: 500,
|
|
757
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
let content = "";
|
|
761
|
+
let model = request.model;
|
|
762
|
+
let finishReason;
|
|
763
|
+
const decoder = new TextDecoder();
|
|
764
|
+
let buffer = "";
|
|
765
|
+
for await (const piece of response.body) {
|
|
766
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
767
|
+
let newline = buffer.indexOf("\n");
|
|
768
|
+
while (newline >= 0) {
|
|
769
|
+
const line = buffer.slice(0, newline).trim();
|
|
770
|
+
buffer = buffer.slice(newline + 1);
|
|
771
|
+
newline = buffer.indexOf("\n");
|
|
772
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
773
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
774
|
+
if (data === "[DONE]") continue;
|
|
775
|
+
let event;
|
|
776
|
+
try {
|
|
777
|
+
event = JSON.parse(data);
|
|
778
|
+
} catch {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (typeof event.model === "string") model = event.model;
|
|
782
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
783
|
+
const first = choices[0];
|
|
784
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
785
|
+
if (delta.length > 0) content += delta;
|
|
786
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
787
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
791
|
+
}
|
|
792
|
+
function normalizeModalities(value) {
|
|
793
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
794
|
+
if (!list) return void 0;
|
|
795
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
796
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
797
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
798
|
+
}
|
|
524
799
|
function normalizeBaseUrl(baseUrl) {
|
|
525
800
|
return baseUrl.replace(/\/+$/, "");
|
|
526
801
|
}
|
|
@@ -535,14 +810,6 @@ function extractModelEntries(body, config) {
|
|
|
535
810
|
{ details: { endpoint: "models" } }
|
|
536
811
|
);
|
|
537
812
|
}
|
|
538
|
-
function authFailed(config, status) {
|
|
539
|
-
const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
|
|
540
|
-
return new ProviderError(
|
|
541
|
-
"PROVIDER_AUTH_FAILED",
|
|
542
|
-
`Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
|
|
543
|
-
{ details: { status } }
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
813
|
function unavailable(config, message, status) {
|
|
547
814
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
548
815
|
details: { status }
|
|
@@ -555,11 +822,33 @@ function inferHealth(error) {
|
|
|
555
822
|
|
|
556
823
|
// src/providers/omnihilbras/index.ts
|
|
557
824
|
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
825
|
+
gateway;
|
|
826
|
+
async listModels() {
|
|
827
|
+
const raw = await this.rawModelsResponse();
|
|
828
|
+
await this.extractGatewayMetadata(raw);
|
|
829
|
+
const models = await super.listModels();
|
|
830
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
831
|
+
}
|
|
558
832
|
/**
|
|
559
833
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
560
834
|
* (use healthCheck() for a non-throwing probe).
|
|
561
835
|
*/
|
|
562
|
-
|
|
836
|
+
/**
|
|
837
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
838
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
839
|
+
*/
|
|
840
|
+
gatewayMetadata() {
|
|
841
|
+
return this.gateway;
|
|
842
|
+
}
|
|
843
|
+
async extractGatewayMetadata(body) {
|
|
844
|
+
if (body !== null && typeof body === "object") {
|
|
845
|
+
const gateway = body.gateway;
|
|
846
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
847
|
+
this.gateway = gateway;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async connectAndRegister(registry) {
|
|
563
852
|
const apiKey = resolveApiKey(this.config);
|
|
564
853
|
const discovered = await this.listModels();
|
|
565
854
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -664,19 +953,431 @@ var ModelRegistry = class {
|
|
|
664
953
|
|
|
665
954
|
// src/agents/process/index.ts
|
|
666
955
|
import { spawn } from "child_process";
|
|
667
|
-
|
|
668
|
-
|
|
956
|
+
|
|
957
|
+
// src/agent-protocol/version.ts
|
|
958
|
+
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
959
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
960
|
+
"omninode-agent-protocol/1",
|
|
961
|
+
PROTOCOL_V2
|
|
962
|
+
];
|
|
963
|
+
var MAX_MESSAGE_BYTES = 1048576;
|
|
964
|
+
function isSupportedProtocol(protocol) {
|
|
965
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/agent-protocol/types.ts
|
|
969
|
+
function newMessageId(prefix = "msg") {
|
|
970
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
971
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
972
|
+
}
|
|
973
|
+
function envelope(type, payload, ids = {}) {
|
|
974
|
+
return {
|
|
975
|
+
protocol: PROTOCOL_V2,
|
|
976
|
+
messageId: newMessageId(),
|
|
977
|
+
...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
|
|
978
|
+
...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
|
|
979
|
+
...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
|
|
980
|
+
...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
|
|
981
|
+
...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
|
|
982
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
983
|
+
type,
|
|
984
|
+
payload,
|
|
985
|
+
...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/agent-protocol/codec.ts
|
|
990
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
991
|
+
// node → agent
|
|
992
|
+
"TASK",
|
|
993
|
+
"HELLO_ACK",
|
|
994
|
+
"CONTEXT",
|
|
995
|
+
"INSTRUCTION",
|
|
996
|
+
"RESPONSE",
|
|
997
|
+
"CANCEL",
|
|
998
|
+
// agent → node
|
|
999
|
+
"HELLO",
|
|
1000
|
+
"TASK_REQUEST",
|
|
1001
|
+
"TASK_ACCEPTED",
|
|
1002
|
+
"TASK_STARTED",
|
|
1003
|
+
"STATUS",
|
|
1004
|
+
"QUESTION",
|
|
1005
|
+
"REPORT",
|
|
1006
|
+
"ARTIFACT",
|
|
1007
|
+
"COMPLETION",
|
|
1008
|
+
"ERROR",
|
|
1009
|
+
"CANCELLED"
|
|
1010
|
+
]);
|
|
1011
|
+
var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
|
|
1012
|
+
function validateEnvelope(value, line) {
|
|
1013
|
+
const sample = line?.slice(0, 200);
|
|
1014
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1015
|
+
return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
|
|
1016
|
+
}
|
|
1017
|
+
const raw = value;
|
|
1018
|
+
const protocol = raw.protocol;
|
|
1019
|
+
if (protocol === void 0) {
|
|
1020
|
+
const type = raw.type;
|
|
1021
|
+
if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
|
|
1022
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
ok: true,
|
|
1026
|
+
legacy: true,
|
|
1027
|
+
envelope: {
|
|
1028
|
+
protocol: "omninode-agent-protocol/1",
|
|
1029
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
|
|
1030
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1031
|
+
type,
|
|
1032
|
+
payload: raw.payload
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
|
|
1037
|
+
return {
|
|
1038
|
+
ok: false,
|
|
1039
|
+
error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
if (raw.type === void 0) {
|
|
1043
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
|
|
1046
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
|
|
1047
|
+
}
|
|
1048
|
+
if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
|
|
1049
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
|
|
1050
|
+
}
|
|
1051
|
+
if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
|
|
1052
|
+
return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
|
|
1053
|
+
}
|
|
1054
|
+
return {
|
|
1055
|
+
ok: true,
|
|
1056
|
+
legacy: protocol !== PROTOCOL_V2,
|
|
1057
|
+
envelope: {
|
|
1058
|
+
protocol,
|
|
1059
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
|
|
1060
|
+
...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
|
|
1061
|
+
...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
|
|
1062
|
+
...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
|
|
1063
|
+
...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
|
|
1064
|
+
...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
|
|
1065
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1066
|
+
type: raw.type,
|
|
1067
|
+
payload: raw.payload,
|
|
1068
|
+
...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1073
|
+
const trimmed = line.trim();
|
|
1074
|
+
if (trimmed.length === 0) {
|
|
1075
|
+
return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
|
|
1076
|
+
}
|
|
1077
|
+
if (trimmed.length > maxBytes) {
|
|
1078
|
+
return {
|
|
1079
|
+
ok: false,
|
|
1080
|
+
error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
let value;
|
|
1084
|
+
try {
|
|
1085
|
+
value = JSON.parse(trimmed);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
return {
|
|
1088
|
+
ok: false,
|
|
1089
|
+
error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
return validateEnvelope(value, trimmed);
|
|
1093
|
+
}
|
|
1094
|
+
function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1095
|
+
const line = JSON.stringify(envelope2);
|
|
1096
|
+
if (line.length > maxBytes) {
|
|
1097
|
+
throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
|
|
1098
|
+
}
|
|
1099
|
+
return line;
|
|
1100
|
+
}
|
|
1101
|
+
var ProtocolDecoder = class {
|
|
1102
|
+
buffer = "";
|
|
1103
|
+
maxBytes;
|
|
1104
|
+
constructor(maxBytes = MAX_MESSAGE_BYTES) {
|
|
1105
|
+
this.maxBytes = maxBytes;
|
|
1106
|
+
}
|
|
1107
|
+
push(chunk) {
|
|
1108
|
+
this.buffer += chunk;
|
|
1109
|
+
const results = [];
|
|
1110
|
+
let index = this.buffer.indexOf("\n");
|
|
1111
|
+
while (index >= 0) {
|
|
1112
|
+
const line = this.buffer.slice(0, index);
|
|
1113
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
1114
|
+
if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
|
|
1115
|
+
index = this.buffer.indexOf("\n");
|
|
1116
|
+
}
|
|
1117
|
+
if (this.buffer.length > this.maxBytes) {
|
|
1118
|
+
results.push({
|
|
1119
|
+
ok: false,
|
|
1120
|
+
error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
|
|
1121
|
+
});
|
|
1122
|
+
this.buffer = "";
|
|
1123
|
+
}
|
|
1124
|
+
return results;
|
|
1125
|
+
}
|
|
1126
|
+
/** Flush any trailing line when the process exits. */
|
|
1127
|
+
flush() {
|
|
1128
|
+
if (this.buffer.trim().length === 0) {
|
|
1129
|
+
this.buffer = "";
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
const result = decodeMessage(this.buffer, this.maxBytes);
|
|
1133
|
+
this.buffer = "";
|
|
1134
|
+
return [result];
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// src/agent-protocol/handshake.ts
|
|
1139
|
+
function parseHello(envelopeValue) {
|
|
1140
|
+
if (envelopeValue.type !== "HELLO") {
|
|
1141
|
+
return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
|
|
1142
|
+
}
|
|
1143
|
+
const payload = envelopeValue.payload;
|
|
1144
|
+
if (payload === null || typeof payload !== "object") {
|
|
1145
|
+
return { ok: false, reason: "HELLO payload must be an object" };
|
|
1146
|
+
}
|
|
1147
|
+
const p = payload;
|
|
1148
|
+
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
1149
|
+
return { ok: false, reason: "HELLO payload is missing 'name'" };
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
ok: true,
|
|
1153
|
+
descriptor: {
|
|
1154
|
+
protocol: envelopeValue.protocol,
|
|
1155
|
+
name: p.name,
|
|
1156
|
+
...typeof p.version === "string" ? { version: p.version } : {},
|
|
1157
|
+
capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
|
|
1158
|
+
supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function buildHello(descriptor) {
|
|
1163
|
+
return envelope("HELLO", descriptor, { agentId: descriptor.name });
|
|
1164
|
+
}
|
|
1165
|
+
function buildHelloAck(descriptor) {
|
|
1166
|
+
return envelope("HELLO_ACK", {
|
|
1167
|
+
ok: true,
|
|
1168
|
+
protocol: PROTOCOL_V2,
|
|
1169
|
+
agent: descriptor.name,
|
|
1170
|
+
...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
|
|
1171
|
+
supportedProtocols: [PROTOCOL_V2]
|
|
1172
|
+
}, { agentId: descriptor.name });
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/agent-protocol/session.ts
|
|
1176
|
+
var ProtocolSession = class {
|
|
1177
|
+
decoder;
|
|
1178
|
+
ids;
|
|
1179
|
+
responder;
|
|
1180
|
+
violations = [];
|
|
1181
|
+
questions = [];
|
|
1182
|
+
artifacts = [];
|
|
1183
|
+
reports = [];
|
|
1184
|
+
descriptor;
|
|
1185
|
+
completionSummary;
|
|
1186
|
+
errorMessage;
|
|
1187
|
+
constructor(ids, options = {}) {
|
|
1188
|
+
this.ids = ids;
|
|
1189
|
+
this.responder = options.responder;
|
|
1190
|
+
this.decoder = new ProtocolDecoder(options.maxBytes);
|
|
1191
|
+
}
|
|
1192
|
+
/** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
|
|
1193
|
+
taskMessages(objective, role, context, instruction) {
|
|
1194
|
+
const ids = this.ids;
|
|
1195
|
+
const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
|
|
1196
|
+
if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
|
|
1197
|
+
if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
|
|
1198
|
+
if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
|
|
1199
|
+
return messages.map((m) => encodeMessage(m));
|
|
1200
|
+
}
|
|
1201
|
+
helloAck(descriptor) {
|
|
1202
|
+
return encodeMessage(buildHelloAck(descriptor));
|
|
1203
|
+
}
|
|
1204
|
+
/** Consumes decoded stdout, answering questions as they arrive. */
|
|
1205
|
+
async handle(decoded) {
|
|
1206
|
+
if (!decoded.ok) {
|
|
1207
|
+
this.violations.push(decoded.error);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const message = decoded.envelope;
|
|
1211
|
+
if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
|
|
1212
|
+
if (message.type === "HELLO") {
|
|
1213
|
+
const hello = parseHello(message);
|
|
1214
|
+
if (hello.ok) this.descriptor = hello.descriptor;
|
|
1215
|
+
else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (message.type === "QUESTION") {
|
|
1219
|
+
await this.answerQuestion(message);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (message.type === "REPORT") {
|
|
1223
|
+
this.reports.push(message.payload);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (message.type === "ARTIFACT") {
|
|
1227
|
+
this.artifacts.push(message.payload);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (message.type === "COMPLETION") {
|
|
1231
|
+
const payload = message.payload;
|
|
1232
|
+
this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (message.type === "ERROR") {
|
|
1236
|
+
const payload = message.payload;
|
|
1237
|
+
const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
|
|
1238
|
+
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1239
|
+
this.errorMessage = `Agent error${code}: ${detail}`;
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
this.violations.push({
|
|
1246
|
+
code: "UNKNOWN_TYPE",
|
|
1247
|
+
message: `unexpected message type ${message.type} from agent`
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
async answerQuestion(message) {
|
|
1251
|
+
const payload = message.payload ?? {};
|
|
1252
|
+
const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
|
|
1253
|
+
const question = typeof payload.question === "string" ? payload.question : "";
|
|
1254
|
+
const expected = typeof payload.expected === "string" ? payload.expected : "text";
|
|
1255
|
+
let answer;
|
|
1256
|
+
if (this.responder) {
|
|
1257
|
+
try {
|
|
1258
|
+
answer = await this.responder({ questionId, question, expected });
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
this.violations.push({
|
|
1261
|
+
code: "INVALID_PAYLOAD",
|
|
1262
|
+
message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
|
|
1267
|
+
this.pendingAnswers.push(
|
|
1268
|
+
encodeMessage(
|
|
1269
|
+
envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
|
|
1270
|
+
...this.ids,
|
|
1271
|
+
requestId: message.requestId ?? this.ids.requestId
|
|
1272
|
+
})
|
|
1273
|
+
)
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
pendingAnswers = [];
|
|
1277
|
+
/** True once any inbound message carried the v2 protocol field. */
|
|
1278
|
+
sawV2 = false;
|
|
1279
|
+
};
|
|
1280
|
+
async function decodeStream(session, chunk) {
|
|
1281
|
+
const decoded = session.decoder.push(chunk);
|
|
1282
|
+
for (const result of decoded) {
|
|
1283
|
+
await session.handle(result);
|
|
1284
|
+
}
|
|
1285
|
+
return session.pendingAnswers.splice(0);
|
|
1286
|
+
}
|
|
1287
|
+
function requireCompletion(session) {
|
|
1288
|
+
if (session.errorMessage !== void 0) {
|
|
1289
|
+
throw new ProtocolError(session.errorMessage);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/agents/cwd.ts
|
|
1294
|
+
import { statSync } from "fs";
|
|
1295
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1296
|
+
function validateWorkingDirectory(cwd, options = {}) {
|
|
1297
|
+
if (!cwd) return void 0;
|
|
1298
|
+
const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
|
|
1299
|
+
let stats;
|
|
1300
|
+
try {
|
|
1301
|
+
stats = statSync(normalized);
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
throw new AgentError(
|
|
1304
|
+
"AGENT_NOT_FOUND",
|
|
1305
|
+
`Working directory "${normalized}" does not exist or is not accessible.`,
|
|
1306
|
+
{ cause: error, details: { cwd: normalized } }
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
if (!stats.isDirectory()) {
|
|
1310
|
+
throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
|
|
1311
|
+
details: { cwd: normalized }
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
statSync(resolve(normalized, "."));
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
throw new AgentError(
|
|
1318
|
+
"AGENT_NOT_FOUND",
|
|
1319
|
+
`Working directory "${normalized}" is not accessible.`,
|
|
1320
|
+
{ cause: error }
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
1324
|
+
const rel = relative(projectRoot, normalized);
|
|
1325
|
+
const outsideProject = rel.startsWith("..") || isAbsolute(rel);
|
|
1326
|
+
if (outsideProject && options.allowExternal !== true) {
|
|
1327
|
+
throw new AgentError(
|
|
1328
|
+
"AGENT_NOT_FOUND",
|
|
1329
|
+
`Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
|
|
1330
|
+
{ details: { cwd: normalized, projectRoot } }
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return { cwd: normalized, outsideProject };
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/agents/env.ts
|
|
1337
|
+
var BASELINE_KEYS = ["PATH", "HOME"];
|
|
1338
|
+
function resolveEnvPolicy(config) {
|
|
1339
|
+
if (config.envPolicy !== void 0) return config.envPolicy;
|
|
1340
|
+
if (config.inheritEnv === false) return "explicit";
|
|
1341
|
+
return "inherit";
|
|
1342
|
+
}
|
|
669
1343
|
function buildChildEnv(config) {
|
|
670
1344
|
const configured = config.env ?? {};
|
|
671
|
-
|
|
1345
|
+
const policy = resolveEnvPolicy(config);
|
|
1346
|
+
if (policy === "inherit") {
|
|
672
1347
|
return { ...process.env, ...configured };
|
|
673
1348
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
1349
|
+
const base = {};
|
|
1350
|
+
for (const key of BASELINE_KEYS) {
|
|
1351
|
+
const value = process.env[key];
|
|
1352
|
+
if (value !== void 0) base[key] = value;
|
|
1353
|
+
}
|
|
1354
|
+
switch (policy) {
|
|
1355
|
+
case "explicit":
|
|
1356
|
+
return { ...base, ...configured };
|
|
1357
|
+
case "allowlist": {
|
|
1358
|
+
const allowed = { ...base };
|
|
1359
|
+
for (const key of config.envAllowlist ?? []) {
|
|
1360
|
+
const value = process.env[key];
|
|
1361
|
+
if (value !== void 0) allowed[key] = value;
|
|
1362
|
+
}
|
|
1363
|
+
return { ...allowed, ...configured };
|
|
1364
|
+
}
|
|
1365
|
+
case "denylist": {
|
|
1366
|
+
const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
|
|
1367
|
+
const filtered = {};
|
|
1368
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1369
|
+
if (denied.has(key.toUpperCase())) continue;
|
|
1370
|
+
filtered[key] = value;
|
|
1371
|
+
}
|
|
1372
|
+
return { ...filtered, ...configured };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
679
1375
|
}
|
|
1376
|
+
|
|
1377
|
+
// src/agents/process/index.ts
|
|
1378
|
+
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
1379
|
+
var KILL_GRACE_MS = 5e3;
|
|
1380
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
680
1381
|
function composeTaskText(input) {
|
|
681
1382
|
const sections = [`# Objective
|
|
682
1383
|
${input.objective}`];
|
|
@@ -697,18 +1398,19 @@ ${input.instruction}`);
|
|
|
697
1398
|
return sections.join("\n\n");
|
|
698
1399
|
}
|
|
699
1400
|
function buildProtocolMessages(input, agentName) {
|
|
700
|
-
const
|
|
1401
|
+
const session = new ProtocolSession({
|
|
1402
|
+
requestId: `req-${input.taskId}`,
|
|
701
1403
|
taskId: input.taskId,
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1404
|
+
agentId: agentName,
|
|
1405
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1406
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1407
|
+
});
|
|
1408
|
+
return session.taskMessages(
|
|
1409
|
+
input.objective,
|
|
1410
|
+
input.role,
|
|
1411
|
+
input.context,
|
|
1412
|
+
input.instruction
|
|
1413
|
+
);
|
|
712
1414
|
}
|
|
713
1415
|
var ProcessAgent = class {
|
|
714
1416
|
config;
|
|
@@ -737,40 +1439,73 @@ var ProcessAgent = class {
|
|
|
737
1439
|
}
|
|
738
1440
|
const mode = this.config.inputMode ?? "stdin";
|
|
739
1441
|
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
740
|
-
return new Promise((
|
|
1442
|
+
return new Promise((resolve2, reject) => {
|
|
1443
|
+
let cwd;
|
|
1444
|
+
try {
|
|
1445
|
+
cwd = validateWorkingDirectory(this.config.cwd, {
|
|
1446
|
+
allowExternal: this.config.allowExternalCwd
|
|
1447
|
+
})?.cwd;
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
reject(error);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
741
1452
|
const child = spawn(
|
|
742
1453
|
command,
|
|
743
1454
|
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
744
1455
|
{
|
|
745
|
-
cwd:
|
|
1456
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
746
1457
|
env: buildChildEnv(this.config),
|
|
747
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1458
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1459
|
+
// Own process group so the whole tree can be terminated together
|
|
1460
|
+
// (roadmap §8: no orphaned children).
|
|
1461
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
748
1462
|
}
|
|
749
1463
|
);
|
|
750
1464
|
this.running.set(input.taskId, child);
|
|
1465
|
+
GLOBAL_RUNNING.add(child);
|
|
1466
|
+
installSignalForwarding();
|
|
751
1467
|
let stdout = "";
|
|
752
1468
|
let stderr = "";
|
|
753
1469
|
let timedOut = false;
|
|
1470
|
+
let outputLimitExceeded = false;
|
|
754
1471
|
let settled = false;
|
|
1472
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
755
1473
|
const timer = setTimeout(() => {
|
|
756
1474
|
timedOut = true;
|
|
757
|
-
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
758
|
-
child
|
|
759
|
-
setTimeout(() => child
|
|
1475
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
|
|
1476
|
+
killTree(child, "SIGTERM");
|
|
1477
|
+
setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
|
|
760
1478
|
}, timeoutMs);
|
|
761
1479
|
const settle = (finish) => {
|
|
762
1480
|
if (settled) return;
|
|
763
1481
|
settled = true;
|
|
764
1482
|
clearTimeout(timer);
|
|
765
1483
|
this.running.delete(input.taskId);
|
|
1484
|
+
GLOBAL_RUNNING.delete(child);
|
|
766
1485
|
finish();
|
|
767
1486
|
};
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1487
|
+
const collect = (into, chunk) => {
|
|
1488
|
+
if (outputLimitExceeded) return;
|
|
1489
|
+
const text = sanitizeChunk(chunk);
|
|
1490
|
+
if (into === "stdout") {
|
|
1491
|
+
if (stdout.length + text.length > maxOutputBytes) {
|
|
1492
|
+
stdout = (stdout + text).slice(0, maxOutputBytes);
|
|
1493
|
+
outputLimitExceeded = true;
|
|
1494
|
+
this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
|
|
1495
|
+
killTree(child, "SIGKILL");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
stdout += text;
|
|
1499
|
+
} else {
|
|
1500
|
+
if (stderr.length + text.length > maxOutputBytes) {
|
|
1501
|
+
stderr = (stderr + text).slice(0, maxOutputBytes);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
stderr += text;
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
1508
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
774
1509
|
child.stdin?.on("error", () => {
|
|
775
1510
|
});
|
|
776
1511
|
if (mode !== "arg") {
|
|
@@ -804,7 +1539,19 @@ var ProcessAgent = class {
|
|
|
804
1539
|
new AgentError(
|
|
805
1540
|
"AGENT_TIMEOUT",
|
|
806
1541
|
`Agent "${this.config.name}" timed out after ${timeoutMs}ms and was killed.`,
|
|
807
|
-
{
|
|
1542
|
+
{
|
|
1543
|
+
details: {
|
|
1544
|
+
operation: "agent.run",
|
|
1545
|
+
agent: this.config.name,
|
|
1546
|
+
durationMs: timeoutMs,
|
|
1547
|
+
// The process was started and may have performed side
|
|
1548
|
+
// effects before being killed — the outcome is unknown,
|
|
1549
|
+
// not failed (roadmap §6.3).
|
|
1550
|
+
dispatched: true,
|
|
1551
|
+
stdout: stdout.slice(0, 4e3),
|
|
1552
|
+
stderr: stderr.slice(0, 2e3)
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
808
1555
|
)
|
|
809
1556
|
);
|
|
810
1557
|
return;
|
|
@@ -812,7 +1559,7 @@ var ProcessAgent = class {
|
|
|
812
1559
|
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
813
1560
|
if (mode === "protocol") {
|
|
814
1561
|
try {
|
|
815
|
-
|
|
1562
|
+
resolve2(
|
|
816
1563
|
interpretProtocolRun({
|
|
817
1564
|
input,
|
|
818
1565
|
agentName: this.config.name,
|
|
@@ -827,14 +1574,14 @@ var ProcessAgent = class {
|
|
|
827
1574
|
return;
|
|
828
1575
|
}
|
|
829
1576
|
const trimmed = stdout.trim();
|
|
830
|
-
|
|
1577
|
+
resolve2({
|
|
831
1578
|
taskId: input.taskId,
|
|
832
|
-
status: code === 0 ? "completed" : "failed",
|
|
1579
|
+
status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
|
|
833
1580
|
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
834
1581
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
835
1582
|
exitCode: code ?? void 0,
|
|
836
1583
|
logs,
|
|
837
|
-
error:
|
|
1584
|
+
...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
|
|
838
1585
|
});
|
|
839
1586
|
});
|
|
840
1587
|
});
|
|
@@ -843,67 +1590,117 @@ var ProcessAgent = class {
|
|
|
843
1590
|
/** Ask the agent to stop current work (SIGTERM). */
|
|
844
1591
|
async cancel(taskId) {
|
|
845
1592
|
const child = this.running.get(taskId);
|
|
846
|
-
if (child) child
|
|
1593
|
+
if (child) killTree(child, "SIGTERM");
|
|
847
1594
|
}
|
|
848
1595
|
runningTaskIds() {
|
|
849
1596
|
return [...this.running.keys()];
|
|
850
1597
|
}
|
|
851
1598
|
};
|
|
852
|
-
function interpretProtocolRun({
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
873
|
-
error = `Agent error${code}: ${detail}`;
|
|
874
|
-
}
|
|
1599
|
+
function interpretProtocolRun({
|
|
1600
|
+
input,
|
|
1601
|
+
agentName,
|
|
1602
|
+
stdout,
|
|
1603
|
+
exitCode,
|
|
1604
|
+
logs,
|
|
1605
|
+
responder
|
|
1606
|
+
}) {
|
|
1607
|
+
const session = new ProtocolSession(
|
|
1608
|
+
{
|
|
1609
|
+
requestId: `req-${input.taskId}`,
|
|
1610
|
+
taskId: input.taskId,
|
|
1611
|
+
agentId: agentName,
|
|
1612
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1613
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1614
|
+
},
|
|
1615
|
+
{ ...responder !== void 0 ? { responder } : {} }
|
|
1616
|
+
);
|
|
1617
|
+
for (const result of session.decoder.push(stdout)) {
|
|
1618
|
+
session.handle(result);
|
|
875
1619
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
`Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
|
|
879
|
-
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
|
|
880
|
-
);
|
|
1620
|
+
for (const result of session.decoder.flush()) {
|
|
1621
|
+
session.handle(result);
|
|
881
1622
|
}
|
|
1623
|
+
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);
|
|
882
1624
|
const trimmed = stdout.trim();
|
|
1625
|
+
const protocolBlock = {
|
|
1626
|
+
protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
|
|
1627
|
+
legacy: !session.sawV2 && session.descriptor === void 0,
|
|
1628
|
+
...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
|
|
1629
|
+
questions: session.questions,
|
|
1630
|
+
violations: session.violations.map((v) => `${v.code}: ${v.message}`)
|
|
1631
|
+
};
|
|
1632
|
+
if (session.errorMessage !== void 0) {
|
|
1633
|
+
return {
|
|
1634
|
+
taskId: input.taskId,
|
|
1635
|
+
status: "failed",
|
|
1636
|
+
error: session.errorMessage,
|
|
1637
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1638
|
+
exitCode: exitCode ?? void 0,
|
|
1639
|
+
logs,
|
|
1640
|
+
protocol: protocolBlock,
|
|
1641
|
+
...reports.length > 0 ? { reports } : {}
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
|
|
1645
|
+
return {
|
|
1646
|
+
taskId: input.taskId,
|
|
1647
|
+
status: "failed",
|
|
1648
|
+
error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
|
|
1649
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1650
|
+
exitCode: exitCode ?? void 0,
|
|
1651
|
+
logs,
|
|
1652
|
+
protocol: protocolBlock
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
const completed = exitCode === 0 && session.completionSummary !== void 0;
|
|
883
1656
|
return {
|
|
884
1657
|
taskId: input.taskId,
|
|
885
|
-
status:
|
|
886
|
-
summary:
|
|
1658
|
+
status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
|
|
1659
|
+
summary: session.completionSummary ?? reports[0]?.summary,
|
|
887
1660
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
888
1661
|
exitCode: exitCode ?? void 0,
|
|
889
1662
|
logs,
|
|
890
|
-
|
|
891
|
-
|
|
1663
|
+
protocol: protocolBlock,
|
|
1664
|
+
...reports.length > 0 ? { reports } : {},
|
|
1665
|
+
...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
|
|
892
1666
|
};
|
|
893
1667
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1668
|
+
var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
|
|
1669
|
+
var signalForwardingInstalled = false;
|
|
1670
|
+
function installSignalForwarding() {
|
|
1671
|
+
if (signalForwardingInstalled) return;
|
|
1672
|
+
signalForwardingInstalled = true;
|
|
1673
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1674
|
+
process.on(signal, () => {
|
|
1675
|
+
for (const child of GLOBAL_RUNNING) {
|
|
1676
|
+
killTree(child, "SIGTERM");
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function killTree(child, signal) {
|
|
1682
|
+
if (child.pid === void 0) return;
|
|
1683
|
+
if (process.platform !== "win32") {
|
|
1684
|
+
try {
|
|
1685
|
+
process.kill(-child.pid, signal);
|
|
1686
|
+
return;
|
|
1687
|
+
} catch {
|
|
903
1688
|
}
|
|
1689
|
+
}
|
|
1690
|
+
try {
|
|
1691
|
+
child.kill(signal);
|
|
904
1692
|
} catch {
|
|
905
1693
|
}
|
|
906
|
-
|
|
1694
|
+
}
|
|
1695
|
+
function describeExit(code, signal) {
|
|
1696
|
+
if (signal !== null) {
|
|
1697
|
+
return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
|
|
1698
|
+
}
|
|
1699
|
+
if (code === null) return "Process terminated without an exit code.";
|
|
1700
|
+
return `Process exited with code ${code}.`;
|
|
1701
|
+
}
|
|
1702
|
+
function sanitizeChunk(chunk) {
|
|
1703
|
+
return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
|
|
907
1704
|
}
|
|
908
1705
|
function normalizeReport(payload, taskId, agentName, index) {
|
|
909
1706
|
if (payload === null || typeof payload !== "object") return void 0;
|
|
@@ -924,6 +1721,40 @@ function firstLine(text, maxLength = 200) {
|
|
|
924
1721
|
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
925
1722
|
}
|
|
926
1723
|
|
|
1724
|
+
// src/agents/adapters.ts
|
|
1725
|
+
var AgentAdapterRegistry = class {
|
|
1726
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1727
|
+
register(adapter) {
|
|
1728
|
+
this.adapters.set(adapter.integration, adapter);
|
|
1729
|
+
}
|
|
1730
|
+
get(integration) {
|
|
1731
|
+
return this.adapters.get(integration);
|
|
1732
|
+
}
|
|
1733
|
+
list() {
|
|
1734
|
+
return [...this.adapters.values()];
|
|
1735
|
+
}
|
|
1736
|
+
create(config) {
|
|
1737
|
+
const adapter = this.get(config.integration);
|
|
1738
|
+
if (!adapter) {
|
|
1739
|
+
throw new AgentError(
|
|
1740
|
+
"AGENT_NOT_FOUND",
|
|
1741
|
+
`No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
return adapter.create(config);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
var ProcessAgentAdapter = class {
|
|
1748
|
+
constructor(factory) {
|
|
1749
|
+
this.factory = factory;
|
|
1750
|
+
}
|
|
1751
|
+
factory;
|
|
1752
|
+
integration = "process";
|
|
1753
|
+
create(config) {
|
|
1754
|
+
return this.factory(config);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
|
|
927
1758
|
// src/agents/registry.ts
|
|
928
1759
|
var AgentRegistry = class {
|
|
929
1760
|
agents = /* @__PURE__ */ new Map();
|
|
@@ -939,14 +1770,15 @@ var AgentRegistry = class {
|
|
|
939
1770
|
};
|
|
940
1771
|
|
|
941
1772
|
// src/agents/factory.ts
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1773
|
+
var defaultAgentAdapters = new AgentAdapterRegistry();
|
|
1774
|
+
defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
|
|
1775
|
+
function createAgent(config, adapters = defaultAgentAdapters) {
|
|
1776
|
+
const registered = adapters.get(config.integration);
|
|
1777
|
+
if (registered) return registered.create(config);
|
|
946
1778
|
if (config.integration === "native") {
|
|
947
1779
|
throw new OmniNodeError(
|
|
948
1780
|
"NOT_IMPLEMENTED",
|
|
949
|
-
`
|
|
1781
|
+
`No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
|
|
950
1782
|
);
|
|
951
1783
|
}
|
|
952
1784
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
@@ -1082,12 +1914,18 @@ import { randomBytes } from "crypto";
|
|
|
1082
1914
|
var ALLOWED_TRANSITIONS = {
|
|
1083
1915
|
created: ["queued", "running", "cancelled"],
|
|
1084
1916
|
queued: ["running", "cancelled"],
|
|
1085
|
-
running: ["completed", "failed", "cancelled"],
|
|
1917
|
+
running: ["completed", "failed", "timed_out", "unknown", "cancelled", "partially_completed"],
|
|
1086
1918
|
waiting: ["running", "cancelled"],
|
|
1087
1919
|
completed: [],
|
|
1088
|
-
failed:
|
|
1089
|
-
|
|
1920
|
+
// failed → running is the only re-entry: bounded automatic retry (§6.4).
|
|
1921
|
+
failed: ["running"],
|
|
1922
|
+
timed_out: [],
|
|
1923
|
+
cancelled: [],
|
|
1924
|
+
unknown: [],
|
|
1925
|
+
partially_completed: []
|
|
1090
1926
|
};
|
|
1927
|
+
var RETRYABLE_ERROR_CODES = ["AGENT_FAILED"];
|
|
1928
|
+
var EXECUTION_HISTORY_LIMIT = 10;
|
|
1091
1929
|
var TaskEngine = class {
|
|
1092
1930
|
constructor(options) {
|
|
1093
1931
|
this.options = options;
|
|
@@ -1119,6 +1957,7 @@ var TaskEngine = class {
|
|
|
1119
1957
|
id: generateTaskId(),
|
|
1120
1958
|
objective: input.objective.trim(),
|
|
1121
1959
|
status: "created",
|
|
1960
|
+
attempt: 0,
|
|
1122
1961
|
createdAt: now,
|
|
1123
1962
|
updatedAt: now,
|
|
1124
1963
|
...input.project !== void 0 ? { project: input.project } : {},
|
|
@@ -1135,8 +1974,10 @@ var TaskEngine = class {
|
|
|
1135
1974
|
const task = await this.mustGet(id);
|
|
1136
1975
|
return this.transition(task, "queued");
|
|
1137
1976
|
}
|
|
1138
|
-
/** Runs the task through its assigned agent and records the result.
|
|
1139
|
-
|
|
1977
|
+
/** Runs the task through its assigned agent and records the result.
|
|
1978
|
+
* `maxAttempts` enables bounded automatic retries of *retryable* errors
|
|
1979
|
+
* with linear backoff; unknown/timed_out outcomes are never auto-retried. */
|
|
1980
|
+
async run(id, options = {}) {
|
|
1140
1981
|
const task = await this.mustGet(id);
|
|
1141
1982
|
if (task.status !== "created" && task.status !== "queued") {
|
|
1142
1983
|
throw new TaskError("TASK_INVALID", `Task ${id} is "${task.status}" and cannot be run.`);
|
|
@@ -1149,49 +1990,89 @@ var TaskEngine = class {
|
|
|
1149
1990
|
throw new TaskError("TASK_INVALID", `Agent "${task.agent}" is not registered anymore.`);
|
|
1150
1991
|
}
|
|
1151
1992
|
const role = task.role ? this.options.roles.get(task.role) : void 0;
|
|
1152
|
-
const running = await this.transition(task, "running");
|
|
1153
1993
|
this.log.info(`Running task ${task.id} on agent "${task.agent}".`);
|
|
1154
|
-
let background =
|
|
1994
|
+
let background = task.context?.background;
|
|
1155
1995
|
if (this.options.memory) {
|
|
1156
1996
|
const memoryContext = await this.options.memory.gatherContext({
|
|
1157
|
-
objective:
|
|
1158
|
-
...
|
|
1159
|
-
...
|
|
1997
|
+
objective: task.objective,
|
|
1998
|
+
...task.role ? { role: task.role } : {},
|
|
1999
|
+
...task.project ? { project: task.project } : {}
|
|
1160
2000
|
});
|
|
1161
2001
|
if (memoryContext.length > 0) {
|
|
1162
2002
|
background = [memoryContext, background].filter((part) => part && part.length > 0).join("\n\n");
|
|
1163
2003
|
}
|
|
1164
2004
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
2005
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 1);
|
|
2006
|
+
const backoffMs = options.retryBackoffMs ?? 0;
|
|
2007
|
+
let current = task;
|
|
2008
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
2009
|
+
const attempt = current.attempt + 1;
|
|
2010
|
+
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
2011
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2012
|
+
current = await this.transition(current, "running", { attempt });
|
|
2013
|
+
let output;
|
|
2014
|
+
let thrown;
|
|
2015
|
+
try {
|
|
2016
|
+
output = await agent.run({
|
|
2017
|
+
taskId: current.id,
|
|
2018
|
+
objective: current.objective,
|
|
2019
|
+
...role ? { role } : {},
|
|
2020
|
+
...background !== void 0 ? { context: background } : {}
|
|
2021
|
+
});
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
thrown = error;
|
|
2024
|
+
}
|
|
2025
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2026
|
+
if (thrown !== void 0 || output === void 0) {
|
|
2027
|
+
const outcome = classifyThrown(thrown);
|
|
2028
|
+
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2029
|
+
const retryable = outcome === "failed" && RETRYABLE_ERROR_CODES.includes(errorCode(thrown)) && i < maxAttempts - 1;
|
|
2030
|
+
current = await this.transition(current, outcome, {
|
|
2031
|
+
attempt,
|
|
2032
|
+
lastError: message,
|
|
2033
|
+
execution: {
|
|
2034
|
+
executionId,
|
|
2035
|
+
attempt,
|
|
2036
|
+
startedAt,
|
|
2037
|
+
finishedAt,
|
|
2038
|
+
outcome,
|
|
2039
|
+
error: message
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
this.log.warn(`Task ${current.id} attempt ${attempt} \u2192 ${outcome}: ${message}`);
|
|
2043
|
+
if (retryable) {
|
|
2044
|
+
if (backoffMs > 0) await sleep(backoffMs * attempt);
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
2048
|
+
return current;
|
|
2049
|
+
}
|
|
2050
|
+
const currentState = await this.options.store.get(current.id);
|
|
2051
|
+
if (currentState?.status === "cancelled") {
|
|
2052
|
+
current = currentState;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
const result = {
|
|
2056
|
+
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
2057
|
+
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2058
|
+
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
2059
|
+
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
2060
|
+
...output.error !== void 0 ? { error: output.error } : {},
|
|
2061
|
+
finishedAt
|
|
2062
|
+
};
|
|
2063
|
+
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
2064
|
+
current = await this.transition(current, finalStatus, {
|
|
2065
|
+
attempt,
|
|
2066
|
+
result,
|
|
2067
|
+
execution: { executionId, attempt, startedAt, finishedAt, outcome: finalStatus }
|
|
1178
2068
|
});
|
|
2069
|
+
this.log.info(`Task ${current.id} finished: ${finalStatus}.`);
|
|
2070
|
+
break;
|
|
1179
2071
|
}
|
|
1180
|
-
const result = {
|
|
1181
|
-
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1182
|
-
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
1183
|
-
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1184
|
-
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1185
|
-
...output.error !== void 0 ? { error: output.error } : {},
|
|
1186
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1187
|
-
};
|
|
1188
|
-
const finalStatus = output.status === "completed" ? "completed" : "failed";
|
|
1189
|
-
const finished = await this.transition(running, finalStatus, { result });
|
|
1190
|
-
this.log.info(`Task ${task.id} finished: ${finalStatus}.`);
|
|
1191
2072
|
if (this.options.memory) {
|
|
1192
|
-
await this.options.memory.recordTaskOutcome(
|
|
2073
|
+
await this.options.memory.recordTaskOutcome(current);
|
|
1193
2074
|
}
|
|
1194
|
-
return
|
|
2075
|
+
return current;
|
|
1195
2076
|
}
|
|
1196
2077
|
async cancel(id) {
|
|
1197
2078
|
const task = await this.mustGet(id);
|
|
@@ -1204,13 +2085,14 @@ var TaskEngine = class {
|
|
|
1204
2085
|
}
|
|
1205
2086
|
return this.transition(task, "cancelled");
|
|
1206
2087
|
}
|
|
1207
|
-
/** Error recovery
|
|
2088
|
+
/** Error recovery: reset a failed/cancelled/unknown/timed_out task so it can run again. */
|
|
1208
2089
|
async retry(id) {
|
|
1209
2090
|
const task = await this.mustGet(id);
|
|
1210
|
-
|
|
2091
|
+
const retryableStates = ["failed", "cancelled", "unknown", "timed_out"];
|
|
2092
|
+
if (!retryableStates.includes(task.status)) {
|
|
1211
2093
|
throw new TaskError(
|
|
1212
2094
|
"TASK_INVALID",
|
|
1213
|
-
`Only failed or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
2095
|
+
`Only failed, timed out, unknown or cancelled tasks can be retried \u2014 task ${id} is "${task.status}".`
|
|
1214
2096
|
);
|
|
1215
2097
|
}
|
|
1216
2098
|
const { result: _discarded, ...rest } = task;
|
|
@@ -1242,10 +2124,14 @@ var TaskEngine = class {
|
|
|
1242
2124
|
if (!allowed.includes(to)) {
|
|
1243
2125
|
throw new TaskError("TASK_INVALID", `Task ${task.id} cannot move from "${task.status}" to "${to}".`);
|
|
1244
2126
|
}
|
|
2127
|
+
const executions = extra.execution ? [...task.executions ?? [], extra.execution].slice(-EXECUTION_HISTORY_LIMIT) : task.executions;
|
|
1245
2128
|
const updated = {
|
|
1246
2129
|
...task,
|
|
1247
2130
|
status: to,
|
|
1248
2131
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2132
|
+
...extra.attempt !== void 0 ? { attempt: extra.attempt } : {},
|
|
2133
|
+
...extra.lastError !== void 0 ? { lastError: extra.lastError } : {},
|
|
2134
|
+
...executions !== void 0 ? { executions } : {},
|
|
1249
2135
|
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1250
2136
|
};
|
|
1251
2137
|
await this.options.store.save(updated);
|
|
@@ -1266,6 +2152,19 @@ var TaskEngine = class {
|
|
|
1266
2152
|
function generateTaskId() {
|
|
1267
2153
|
return `task-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1268
2154
|
}
|
|
2155
|
+
function errorCode(error) {
|
|
2156
|
+
return error?.code ?? "";
|
|
2157
|
+
}
|
|
2158
|
+
function classifyThrown(error) {
|
|
2159
|
+
if (error instanceof AgentError && error.code === "AGENT_TIMEOUT") {
|
|
2160
|
+
const dispatched = error.details?.dispatched;
|
|
2161
|
+
return dispatched === false ? "timed_out" : "unknown";
|
|
2162
|
+
}
|
|
2163
|
+
return "failed";
|
|
2164
|
+
}
|
|
2165
|
+
function sleep(ms) {
|
|
2166
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2167
|
+
}
|
|
1269
2168
|
|
|
1270
2169
|
// src/audit/index.ts
|
|
1271
2170
|
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -1308,6 +2207,23 @@ var FileAuditLog = class {
|
|
|
1308
2207
|
}
|
|
1309
2208
|
};
|
|
1310
2209
|
|
|
2210
|
+
// src/memory/base.ts
|
|
2211
|
+
function defaultSearch(provider, text, options = {}) {
|
|
2212
|
+
return provider.retrieve({
|
|
2213
|
+
text,
|
|
2214
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2215
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
function defaultMetadata(name, options = {}) {
|
|
2219
|
+
return {
|
|
2220
|
+
name,
|
|
2221
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2222
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
2223
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
|
|
1311
2227
|
// src/memory/local.ts
|
|
1312
2228
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1313
2229
|
"the",
|
|
@@ -1370,9 +2286,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1370
2286
|
constructor(directory) {
|
|
1371
2287
|
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
1372
2288
|
}
|
|
1373
|
-
|
|
2289
|
+
metadata() {
|
|
2290
|
+
return defaultMetadata(this.name, {
|
|
2291
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2292
|
+
relevanceRanking: true,
|
|
2293
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
search(text, options = {}) {
|
|
2297
|
+
return defaultSearch(this, text, options);
|
|
2298
|
+
}
|
|
2299
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2300
|
+
query(query) {
|
|
2301
|
+
return this.retrieve(query);
|
|
2302
|
+
}
|
|
2303
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2304
|
+
write(entry) {
|
|
2305
|
+
return this.store(entry);
|
|
2306
|
+
}
|
|
2307
|
+
async retrieve(query) {
|
|
1374
2308
|
const entries = await this.readAll();
|
|
1375
|
-
const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
|
|
2309
|
+
const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter((entry) => query.category ? entry.category === query.category : true).filter(
|
|
1376
2310
|
(entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
|
|
1377
2311
|
);
|
|
1378
2312
|
if (!query.text || query.text.trim().length === 0) {
|
|
@@ -1381,7 +2315,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1381
2315
|
const queryKeywords = keywords(query.text);
|
|
1382
2316
|
return filtered.map((entry) => ({ entry, score: relevanceScore(queryKeywords, entry) })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).slice(0, query.limit ?? 10).map(({ entry }) => entry);
|
|
1383
2317
|
}
|
|
1384
|
-
|
|
2318
|
+
store(entry) {
|
|
1385
2319
|
return this.mutate((entries) => {
|
|
1386
2320
|
const index = entries.findIndex((e) => e.key === entry.key);
|
|
1387
2321
|
const stamped = {
|
|
@@ -1412,11 +2346,40 @@ var RememberaMemoryProvider = class {
|
|
|
1412
2346
|
this.env = config.env ?? process.env;
|
|
1413
2347
|
this.log = log.child({ component: "remembera" });
|
|
1414
2348
|
}
|
|
2349
|
+
get authenticationConfigured() {
|
|
2350
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
2351
|
+
}
|
|
1415
2352
|
headers() {
|
|
1416
2353
|
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1417
2354
|
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1418
2355
|
}
|
|
1419
|
-
|
|
2356
|
+
/** Search delegates to the gateway's relevance query. */
|
|
2357
|
+
search(text, options = {}) {
|
|
2358
|
+
return defaultSearch(
|
|
2359
|
+
this,
|
|
2360
|
+
text,
|
|
2361
|
+
{
|
|
2362
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2363
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2364
|
+
}
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
metadata() {
|
|
2368
|
+
return defaultMetadata(this.name, {
|
|
2369
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2370
|
+
relevanceRanking: true,
|
|
2371
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2375
|
+
query(query) {
|
|
2376
|
+
return this.retrieve(query);
|
|
2377
|
+
}
|
|
2378
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2379
|
+
write(entry) {
|
|
2380
|
+
return this.store(entry);
|
|
2381
|
+
}
|
|
2382
|
+
async retrieve(query) {
|
|
1420
2383
|
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1421
2384
|
if (status !== 200) {
|
|
1422
2385
|
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
@@ -1427,7 +2390,7 @@ var RememberaMemoryProvider = class {
|
|
|
1427
2390
|
}
|
|
1428
2391
|
return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
|
|
1429
2392
|
}
|
|
1430
|
-
async
|
|
2393
|
+
async store(entry) {
|
|
1431
2394
|
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
|
|
1432
2395
|
if (status !== 200 && status !== 201) {
|
|
1433
2396
|
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
@@ -1462,6 +2425,7 @@ var RememberaMemoryProvider = class {
|
|
|
1462
2425
|
content: entry.content,
|
|
1463
2426
|
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1464
2427
|
...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
|
|
2428
|
+
...typeof entry.category === "string" ? { category: entry.category } : {},
|
|
1465
2429
|
...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
|
|
1466
2430
|
...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
|
|
1467
2431
|
};
|
|
@@ -1492,102 +2456,387 @@ function createMemoryProvider(config) {
|
|
|
1492
2456
|
}
|
|
1493
2457
|
|
|
1494
2458
|
// src/memory/service.ts
|
|
2459
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1495
2460
|
var MAX_CONTEXT_CHARS = 4e3;
|
|
1496
2461
|
var MemoryService = class {
|
|
1497
|
-
constructor(provider, log = logger) {
|
|
2462
|
+
constructor(provider, log = logger, options = {}) {
|
|
1498
2463
|
this.provider = provider;
|
|
1499
2464
|
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2465
|
+
this.required = options.required ?? false;
|
|
2466
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1500
2467
|
}
|
|
1501
2468
|
provider;
|
|
1502
2469
|
log;
|
|
2470
|
+
required;
|
|
2471
|
+
maxContextChars;
|
|
1503
2472
|
get providerRef() {
|
|
1504
2473
|
return this.provider;
|
|
1505
2474
|
}
|
|
1506
2475
|
get providerName() {
|
|
1507
2476
|
return this.provider.name;
|
|
1508
2477
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
2478
|
+
get isRequired() {
|
|
2479
|
+
return this.required;
|
|
2480
|
+
}
|
|
2481
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
2482
|
+
async contextFor(query) {
|
|
2483
|
+
let entries;
|
|
1511
2484
|
try {
|
|
1512
|
-
|
|
2485
|
+
entries = await this.provider.retrieve({
|
|
1513
2486
|
text: query.objective,
|
|
1514
2487
|
...query.role ? { tags: [query.role] } : {},
|
|
1515
|
-
limit:
|
|
2488
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1516
2489
|
});
|
|
1517
|
-
if (entries.length === 0) return "";
|
|
1518
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
1519
|
-
for (const entry of entries) {
|
|
1520
|
-
byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
|
|
1521
|
-
}
|
|
1522
|
-
const parts = [];
|
|
1523
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1524
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
1525
|
-
if (scopeEntries.length === 0) continue;
|
|
1526
|
-
const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
|
|
1527
|
-
parts.push(`### Relevant memory (${scope})
|
|
1528
|
-
${lines}`);
|
|
1529
|
-
}
|
|
1530
|
-
const context = parts.join("\n\n");
|
|
1531
|
-
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
1532
2490
|
} catch (error) {
|
|
1533
|
-
this.
|
|
1534
|
-
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1535
|
-
);
|
|
1536
|
-
return "";
|
|
2491
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
1537
2492
|
}
|
|
2493
|
+
return this.format(entries);
|
|
1538
2494
|
}
|
|
1539
|
-
/**
|
|
2495
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2496
|
+
async gatherContext(query) {
|
|
2497
|
+
return (await this.contextFor(query)).text;
|
|
2498
|
+
}
|
|
2499
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1540
2500
|
async recordTaskOutcome(task) {
|
|
1541
2501
|
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1542
2502
|
try {
|
|
1543
|
-
await this.provider.
|
|
2503
|
+
await this.provider.store({
|
|
1544
2504
|
key: `task:${task.id}`,
|
|
1545
2505
|
scope: "task",
|
|
2506
|
+
category: "tasks",
|
|
1546
2507
|
content: `Objective: ${task.objective}
|
|
1547
2508
|
Status: ${task.status}
|
|
1548
2509
|
Outcome: ${outcome}`,
|
|
1549
2510
|
tags: ["task", ...task.agent ? [task.agent] : [], ...task.role ? [task.role] : []],
|
|
1550
2511
|
createdAt: task.createdAt
|
|
1551
2512
|
});
|
|
1552
|
-
const significant = (task.result?.reports ?? []).flatMap(
|
|
1553
|
-
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1554
|
-
);
|
|
1555
|
-
for (const { report, finding } of significant) {
|
|
1556
|
-
await this.provider.
|
|
1557
|
-
key: `finding:${report.id}:${finding.id}`,
|
|
1558
|
-
scope: "project",
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
2513
|
+
const significant = (task.result?.reports ?? []).flatMap(
|
|
2514
|
+
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
2515
|
+
);
|
|
2516
|
+
for (const { report, finding } of significant) {
|
|
2517
|
+
await this.provider.store({
|
|
2518
|
+
key: `finding:${report.id}:${finding.id}`,
|
|
2519
|
+
scope: "project",
|
|
2520
|
+
category: "problems",
|
|
2521
|
+
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
2522
|
+
tags: ["known-problem", report.agent]
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
} catch (error) {
|
|
2526
|
+
this.handleFailure("store", error, void 0);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
format(entries) {
|
|
2530
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2531
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2532
|
+
for (const entry of entries) {
|
|
2533
|
+
byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
|
|
2534
|
+
}
|
|
2535
|
+
const parts = [];
|
|
2536
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2537
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
2538
|
+
if (scopeEntries.length === 0) continue;
|
|
2539
|
+
const lines = scopeEntries.map((entry) => {
|
|
2540
|
+
const category = entry.category ? `[${entry.category}] ` : "";
|
|
2541
|
+
return `- ${category}${entry.content}`;
|
|
2542
|
+
}).join("\n");
|
|
2543
|
+
parts.push(`### Relevant memory (${scope})
|
|
2544
|
+
${lines}`);
|
|
2545
|
+
}
|
|
2546
|
+
let text = parts.join("\n\n");
|
|
2547
|
+
const truncated = text.length > this.maxContextChars;
|
|
2548
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2549
|
+
return { text, entries, truncated };
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
2553
|
+
*/
|
|
2554
|
+
handleFailure(operation, error, fallback) {
|
|
2555
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2556
|
+
if (this.required) {
|
|
2557
|
+
throw new MemoryError(
|
|
2558
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2559
|
+
{ cause: error }
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
2563
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
|
|
2567
|
+
// src/tasks/engine-factory.ts
|
|
2568
|
+
function createTaskEngine(config, log = logger, directory) {
|
|
2569
|
+
const agents = buildAgentRegistry(config, log);
|
|
2570
|
+
const roles = buildRoleRegistry(config);
|
|
2571
|
+
return new TaskEngine({
|
|
2572
|
+
agents,
|
|
2573
|
+
roles,
|
|
2574
|
+
store: new FileTaskStore(directory),
|
|
2575
|
+
...config.project.memory !== void 0 ? {
|
|
2576
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
2577
|
+
required: config.project.memory.required
|
|
2578
|
+
})
|
|
2579
|
+
} : {},
|
|
2580
|
+
audit: new FileAuditLog(),
|
|
2581
|
+
log
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// src/pipelines/engine.ts
|
|
2586
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2587
|
+
|
|
2588
|
+
// src/reports/aggregate.ts
|
|
2589
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2590
|
+
|
|
2591
|
+
// src/reports/schema.ts
|
|
2592
|
+
import { z as z2 } from "zod";
|
|
2593
|
+
var evidenceSchema = z2.union([
|
|
2594
|
+
z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
|
|
2595
|
+
z2.object({
|
|
2596
|
+
kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
|
|
2597
|
+
ref: z2.string().optional(),
|
|
2598
|
+
line: z2.number().int().optional(),
|
|
2599
|
+
excerpt: z2.string().optional(),
|
|
2600
|
+
agent: z2.string().optional()
|
|
2601
|
+
})
|
|
2602
|
+
]);
|
|
2603
|
+
var findingSchema = z2.object({
|
|
2604
|
+
id: z2.string().min(1),
|
|
2605
|
+
title: z2.string().min(1),
|
|
2606
|
+
detail: z2.string().default(""),
|
|
2607
|
+
description: z2.string().optional(),
|
|
2608
|
+
severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
|
|
2609
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
2610
|
+
severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
|
|
2611
|
+
confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
|
|
2612
|
+
recommendations: z2.array(z2.string()).optional(),
|
|
2613
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
2614
|
+
location: z2.string().optional(),
|
|
2615
|
+
tags: z2.array(z2.string()).optional(),
|
|
2616
|
+
source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
|
|
2617
|
+
recommendation: z2.string().optional()
|
|
2618
|
+
});
|
|
2619
|
+
var artifactSchema = z2.object({
|
|
2620
|
+
kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
|
|
2621
|
+
ref: z2.string().min(1),
|
|
2622
|
+
description: z2.string().optional(),
|
|
2623
|
+
mediaType: z2.string().optional(),
|
|
2624
|
+
bytes: z2.number().int().optional()
|
|
2625
|
+
});
|
|
2626
|
+
var reportSchema = z2.object({
|
|
2627
|
+
id: z2.string().min(1),
|
|
2628
|
+
taskId: z2.string().min(1),
|
|
2629
|
+
agent: z2.string().min(1),
|
|
2630
|
+
agentId: z2.string().optional(),
|
|
2631
|
+
pipelineRunId: z2.string().optional(),
|
|
2632
|
+
model: z2.string().optional(),
|
|
2633
|
+
summary: z2.string(),
|
|
2634
|
+
findings: z2.array(findingSchema),
|
|
2635
|
+
recommendations: z2.array(z2.string()),
|
|
2636
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
2637
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
2638
|
+
artifacts: z2.array(artifactSchema).optional(),
|
|
2639
|
+
createdAt: z2.string().min(1),
|
|
2640
|
+
metadata: z2.record(z2.unknown()).optional()
|
|
2641
|
+
});
|
|
2642
|
+
function validateReport(value) {
|
|
2643
|
+
const parsed = reportSchema.safeParse(value);
|
|
2644
|
+
if (!parsed.success) {
|
|
2645
|
+
const issues = parsed.error.issues.map((issue) => ({
|
|
2646
|
+
path: issue.path.join(".") || "(root)",
|
|
2647
|
+
message: issue.message
|
|
2648
|
+
}));
|
|
2649
|
+
return {
|
|
2650
|
+
valid: false,
|
|
2651
|
+
issues,
|
|
2652
|
+
reason: `report failed schema validation (${issues.length} issue(s))`
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
const report = parsed.data;
|
|
2656
|
+
report.findings = report.findings.map((finding) => ({
|
|
2657
|
+
...finding,
|
|
2658
|
+
description: finding.description ?? finding.detail,
|
|
2659
|
+
...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
|
|
2660
|
+
}));
|
|
2661
|
+
return { valid: true, report };
|
|
2662
|
+
}
|
|
2663
|
+
function normalizeEvidence(input) {
|
|
2664
|
+
if (input === void 0) return void 0;
|
|
2665
|
+
return input.map(
|
|
2666
|
+
(item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
function normalizeFinding(finding) {
|
|
2670
|
+
return {
|
|
2671
|
+
...finding,
|
|
2672
|
+
description: finding.description ?? finding.detail,
|
|
2673
|
+
evidence: normalizeEvidence(finding.evidence)
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/reports/aggregate.ts
|
|
2678
|
+
var SEVERITY_RANK = {
|
|
2679
|
+
info: 0,
|
|
2680
|
+
low: 1,
|
|
2681
|
+
medium: 2,
|
|
2682
|
+
high: 3,
|
|
2683
|
+
critical: 4
|
|
2684
|
+
};
|
|
2685
|
+
function aggregateReports(reports, options = {}) {
|
|
2686
|
+
const threshold = options.similarityThreshold ?? 0.7;
|
|
2687
|
+
const findingGroups = [];
|
|
2688
|
+
const recommendationGroups = [];
|
|
2689
|
+
for (const report of reports) {
|
|
2690
|
+
for (const finding of report.findings) {
|
|
2691
|
+
mergeFinding(findingGroups, finding, report, threshold);
|
|
2692
|
+
}
|
|
2693
|
+
for (const text of report.recommendations) {
|
|
2694
|
+
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
2698
|
+
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2699
|
+
const conflicts = detectConflicts(findingGroups);
|
|
2700
|
+
const agreements = findingGroups.filter(
|
|
2701
|
+
(group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
|
|
2702
|
+
);
|
|
2703
|
+
const disputed = conflicts.length;
|
|
2704
|
+
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).`;
|
|
2705
|
+
return {
|
|
2706
|
+
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
2707
|
+
title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
|
|
2708
|
+
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
2709
|
+
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
2710
|
+
taskIds: options.taskIds ?? [],
|
|
2711
|
+
summary,
|
|
2712
|
+
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
2713
|
+
agreements,
|
|
2714
|
+
conflicts,
|
|
2715
|
+
recommendations: recommendationGroups,
|
|
2716
|
+
sources,
|
|
2717
|
+
reportIds: reports.map((r) => r.id),
|
|
2718
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2719
|
+
metadata: {
|
|
2720
|
+
agents: sources.length,
|
|
2721
|
+
reports: reports.length,
|
|
2722
|
+
rawFindings: totalRawFindings,
|
|
2723
|
+
findingGroups: findingGroups.length,
|
|
2724
|
+
agreements: agreements.length,
|
|
2725
|
+
conflicts: conflicts.length
|
|
2726
|
+
}
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
function rankConfidence(confidence) {
|
|
2730
|
+
return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
|
|
2731
|
+
}
|
|
2732
|
+
function detectConflicts(groups) {
|
|
2733
|
+
const conflicts = [];
|
|
2734
|
+
for (const group of groups) {
|
|
2735
|
+
if (group.sources.length < 2) continue;
|
|
2736
|
+
const severities = new Set(Object.values(group.severityBySource ?? {}));
|
|
2737
|
+
if (severities.size > 1) {
|
|
2738
|
+
conflicts.push({
|
|
2739
|
+
type: "severity",
|
|
2740
|
+
findingTitle: group.title,
|
|
2741
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
2742
|
+
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.`
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
|
|
2746
|
+
if (recommendations.length > 1) {
|
|
2747
|
+
conflicts.push({
|
|
2748
|
+
type: "recommendation",
|
|
2749
|
+
findingTitle: group.title,
|
|
2750
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
2751
|
+
note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
|
|
2752
|
+
});
|
|
1567
2753
|
}
|
|
1568
2754
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
2755
|
+
return conflicts;
|
|
2756
|
+
}
|
|
2757
|
+
function position(source, group) {
|
|
2758
|
+
return {
|
|
2759
|
+
agent: source.agent,
|
|
2760
|
+
reportId: source.reportId,
|
|
2761
|
+
...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
|
|
2762
|
+
...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
|
|
2763
|
+
...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
function rank(finding) {
|
|
2767
|
+
return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
|
|
2768
|
+
}
|
|
2769
|
+
function mergeFinding(groups, finding, report, threshold) {
|
|
2770
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
2771
|
+
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
2772
|
+
if (group) {
|
|
2773
|
+
group.occurrences += 1;
|
|
2774
|
+
group.details.push(finding.detail);
|
|
2775
|
+
group.sources.push(source);
|
|
2776
|
+
group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
|
|
2777
|
+
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
2778
|
+
group.severity = finding.severity;
|
|
2779
|
+
}
|
|
2780
|
+
if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
|
|
2781
|
+
group.confidence = finding.confidence;
|
|
2782
|
+
}
|
|
2783
|
+
if (finding.severity) {
|
|
2784
|
+
group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
|
|
2785
|
+
}
|
|
2786
|
+
if (finding.confidence) {
|
|
2787
|
+
group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
|
|
2788
|
+
}
|
|
2789
|
+
if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
|
|
2790
|
+
group.recommendations = [...group.recommendations ?? [], finding.recommendation];
|
|
2791
|
+
}
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
groups.push({
|
|
2795
|
+
title: finding.title,
|
|
2796
|
+
details: [finding.detail],
|
|
2797
|
+
sources: [source],
|
|
2798
|
+
occurrences: 1,
|
|
2799
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
2800
|
+
...finding.confidence ? { confidence: finding.confidence } : {},
|
|
2801
|
+
...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
|
|
2802
|
+
...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
|
|
2803
|
+
...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
|
|
2804
|
+
evidence: [...normalizeEvidence(finding.evidence) ?? []]
|
|
1582
2805
|
});
|
|
1583
2806
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2807
|
+
function mergeRecommendation(groups, text, report, threshold) {
|
|
2808
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
2809
|
+
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
2810
|
+
if (group) {
|
|
2811
|
+
group.occurrences += 1;
|
|
2812
|
+
group.sources.push(source);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
groups.push({ text, sources: [source], occurrences: 1 });
|
|
2816
|
+
}
|
|
2817
|
+
function similar(a, b, threshold) {
|
|
2818
|
+
const left = tokens(a);
|
|
2819
|
+
const right = tokens(b);
|
|
2820
|
+
if (left.size === 0 || right.size === 0) return false;
|
|
2821
|
+
let intersection = 0;
|
|
2822
|
+
for (const token of left) {
|
|
2823
|
+
if (right.has(token)) intersection += 1;
|
|
2824
|
+
}
|
|
2825
|
+
const union = left.size + right.size - intersection;
|
|
2826
|
+
return intersection / union >= threshold;
|
|
2827
|
+
}
|
|
2828
|
+
function tokens(text) {
|
|
2829
|
+
return new Set(
|
|
2830
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
function truncate(text, max) {
|
|
2834
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2835
|
+
}
|
|
1587
2836
|
|
|
1588
2837
|
// src/planner/heuristic.ts
|
|
1589
|
-
import { randomBytes as
|
|
1590
|
-
var
|
|
2838
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
2839
|
+
var SEVERITY_RANK2 = {
|
|
1591
2840
|
info: 0,
|
|
1592
2841
|
low: 1,
|
|
1593
2842
|
medium: 2,
|
|
@@ -1598,19 +2847,32 @@ var HeuristicPlanner = class {
|
|
|
1598
2847
|
name = "heuristic";
|
|
1599
2848
|
async plan(request) {
|
|
1600
2849
|
const steps = [];
|
|
1601
|
-
const findings = request.reports.flatMap(
|
|
1602
|
-
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
1603
|
-
);
|
|
1604
2850
|
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
1605
|
-
for (const
|
|
2851
|
+
for (const text of recommendations) {
|
|
1606
2852
|
steps.push({
|
|
1607
2853
|
id: `step-${steps.length + 1}`,
|
|
1608
|
-
title:
|
|
2854
|
+
title: truncate2(text, 120),
|
|
1609
2855
|
order: steps.length + 1,
|
|
1610
|
-
...
|
|
2856
|
+
...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
1611
2857
|
});
|
|
1612
2858
|
}
|
|
1613
|
-
const
|
|
2859
|
+
const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
|
|
2860
|
+
finding: {
|
|
2861
|
+
id: `agg-${slug(finding.title)}`,
|
|
2862
|
+
title: finding.title,
|
|
2863
|
+
detail: finding.details[0] ?? finding.title,
|
|
2864
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
2865
|
+
...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
|
|
2866
|
+
},
|
|
2867
|
+
agent: finding.sources.map((s) => s.agent).join("+")
|
|
2868
|
+
})) : request.reports.flatMap(
|
|
2869
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
2870
|
+
);
|
|
2871
|
+
const significant = contributed.filter(
|
|
2872
|
+
({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
|
|
2873
|
+
).sort(
|
|
2874
|
+
(a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
|
|
2875
|
+
);
|
|
1614
2876
|
for (const { finding } of significant) {
|
|
1615
2877
|
steps.push(remediationStep(finding, steps.length + 1));
|
|
1616
2878
|
}
|
|
@@ -1623,12 +2885,20 @@ var HeuristicPlanner = class {
|
|
|
1623
2885
|
});
|
|
1624
2886
|
}
|
|
1625
2887
|
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
2888
|
+
const conflicts = request.aggregated?.conflicts ?? [];
|
|
2889
|
+
if (conflicts.length > 0) {
|
|
2890
|
+
risks.push(
|
|
2891
|
+
`${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
1626
2894
|
return {
|
|
1627
|
-
id: `plan-${Date.now().toString(36)}-${
|
|
2895
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
1628
2896
|
objective: request.objective,
|
|
2897
|
+
goal: request.aggregated?.title ?? truncate2(request.objective, 120),
|
|
1629
2898
|
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
1630
2899
|
steps,
|
|
1631
2900
|
...risks.length > 0 ? { risks } : {},
|
|
2901
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
1632
2902
|
generatedBy: "heuristic",
|
|
1633
2903
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
1634
2904
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
@@ -1639,28 +2909,80 @@ var HeuristicPlanner = class {
|
|
|
1639
2909
|
function remediationStep(finding, order) {
|
|
1640
2910
|
return {
|
|
1641
2911
|
id: `step-${order}`,
|
|
1642
|
-
title: `Fix: ${
|
|
2912
|
+
title: `Fix: ${truncate2(finding.title, 100)}`,
|
|
1643
2913
|
description: finding.detail,
|
|
1644
2914
|
order,
|
|
1645
2915
|
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
1646
|
-
sourceFindings: [finding.id]
|
|
2916
|
+
sourceFindings: [finding.id],
|
|
2917
|
+
...finding.recommendation ? { recommendation: finding.recommendation } : {}
|
|
1647
2918
|
};
|
|
1648
2919
|
}
|
|
1649
2920
|
function dedupe(texts) {
|
|
1650
2921
|
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
1651
2922
|
}
|
|
1652
|
-
function
|
|
2923
|
+
function slug(text) {
|
|
2924
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
2925
|
+
}
|
|
2926
|
+
function truncate2(text, max) {
|
|
1653
2927
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1654
2928
|
}
|
|
1655
2929
|
|
|
1656
2930
|
// src/planner/model.ts
|
|
1657
|
-
import { randomBytes as
|
|
1658
|
-
|
|
2931
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2932
|
+
|
|
2933
|
+
// src/planner/schema.ts
|
|
2934
|
+
import { z as z3 } from "zod";
|
|
2935
|
+
var planStepSchema = z3.object({
|
|
2936
|
+
id: z3.string().min(1).optional(),
|
|
2937
|
+
title: z3.string().min(1),
|
|
2938
|
+
description: z3.string().optional(),
|
|
2939
|
+
order: z3.number().int().optional(),
|
|
2940
|
+
targets: z3.array(z3.string()).optional(),
|
|
2941
|
+
acceptance_criteria: z3.array(z3.string()).optional(),
|
|
2942
|
+
depends_on: z3.array(z3.string()).optional(),
|
|
2943
|
+
source_findings: z3.array(z3.string()).optional()
|
|
2944
|
+
});
|
|
2945
|
+
var planJsonSchema = z3.object({
|
|
2946
|
+
goal: z3.string().min(1),
|
|
2947
|
+
summary: z3.string().min(1),
|
|
2948
|
+
steps: z3.array(planStepSchema).min(1),
|
|
2949
|
+
risks: z3.array(z3.string()).optional(),
|
|
2950
|
+
notes: z3.array(z3.string()).optional()
|
|
2951
|
+
});
|
|
2952
|
+
function validatePlan(plan) {
|
|
2953
|
+
const issues = [];
|
|
2954
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
2955
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
2956
|
+
if (stepIds.has(step.id)) {
|
|
2957
|
+
issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
|
|
2958
|
+
}
|
|
2959
|
+
stepIds.add(step.id);
|
|
2960
|
+
if (!step.title || step.title.trim().length === 0) {
|
|
2961
|
+
issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
|
|
2962
|
+
}
|
|
2963
|
+
for (const dep of step.dependsOn ?? []) {
|
|
2964
|
+
if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
|
|
2965
|
+
issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
if (plan.steps.length === 0) {
|
|
2970
|
+
issues.push({ path: "steps", message: "a plan needs at least one step" });
|
|
2971
|
+
}
|
|
2972
|
+
if (!plan.summary || plan.summary.trim().length === 0) {
|
|
2973
|
+
issues.push({ path: "summary", message: "summary must not be empty" });
|
|
2974
|
+
}
|
|
2975
|
+
return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
|
|
2976
|
+
}
|
|
1659
2977
|
|
|
1660
2978
|
// src/planner/context.ts
|
|
1661
2979
|
function buildPlannerContext(request) {
|
|
1662
|
-
const parts = [`#
|
|
2980
|
+
const parts = [`# Task
|
|
1663
2981
|
${request.objective}`];
|
|
2982
|
+
if (request.projectContext) {
|
|
2983
|
+
parts.push(`# Project context
|
|
2984
|
+
${request.projectContext}`);
|
|
2985
|
+
}
|
|
1664
2986
|
if (request.role) {
|
|
1665
2987
|
const roleLines = [`# Role
|
|
1666
2988
|
${request.role.name} (${request.role.id})`];
|
|
@@ -1680,7 +3002,7 @@ ${request.role.name} (${request.role.id})`];
|
|
|
1680
3002
|
)
|
|
1681
3003
|
);
|
|
1682
3004
|
parts.push(
|
|
1683
|
-
`#
|
|
3005
|
+
`# Agent reports (${request.reports.length} by ${agents})
|
|
1684
3006
|
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
1685
3007
|
);
|
|
1686
3008
|
const recommendationLines = request.reports.flatMap(
|
|
@@ -1690,9 +3012,20 @@ ${request.role.name} (${request.role.id})`];
|
|
|
1690
3012
|
parts.push(`# Recommendations
|
|
1691
3013
|
${recommendationLines.join("\n")}`);
|
|
1692
3014
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
3015
|
+
}
|
|
3016
|
+
if (request.aggregated) {
|
|
3017
|
+
const aggregatedLines = [
|
|
3018
|
+
`- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
|
|
3019
|
+
...request.aggregated.findings.map(
|
|
3020
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
|
|
3021
|
+
),
|
|
3022
|
+
// Conflicts stay visible to the planner — it must plan around disagreement.
|
|
3023
|
+
...request.aggregated.conflicts.map(
|
|
3024
|
+
(conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
|
|
3025
|
+
)
|
|
3026
|
+
];
|
|
3027
|
+
parts.push(`# Aggregated findings
|
|
3028
|
+
${aggregatedLines.join("\n")}`);
|
|
1696
3029
|
}
|
|
1697
3030
|
if (request.memory && request.memory.length > 0) {
|
|
1698
3031
|
parts.push(
|
|
@@ -1700,7 +3033,11 @@ ${summaryLines.join("\n")}`);
|
|
|
1700
3033
|
` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
|
|
1701
3034
|
);
|
|
1702
3035
|
}
|
|
1703
|
-
if (request.
|
|
3036
|
+
if (request.constraints && request.constraints.length > 0) {
|
|
3037
|
+
parts.push(`# Constraints
|
|
3038
|
+
${request.constraints.map((c) => `- ${c}`).join("\n")}`);
|
|
3039
|
+
}
|
|
3040
|
+
if (request.context) {
|
|
1704
3041
|
parts.push(`# Research context
|
|
1705
3042
|
${request.context}`);
|
|
1706
3043
|
}
|
|
@@ -1708,22 +3045,6 @@ ${request.context}`);
|
|
|
1708
3045
|
}
|
|
1709
3046
|
|
|
1710
3047
|
// src/planner/model.ts
|
|
1711
|
-
var planStepJsonSchema = z2.object({
|
|
1712
|
-
id: z2.string().optional(),
|
|
1713
|
-
title: z2.string().min(1),
|
|
1714
|
-
description: z2.string().optional(),
|
|
1715
|
-
order: z2.number().int().optional(),
|
|
1716
|
-
targets: z2.array(z2.string()).optional(),
|
|
1717
|
-
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
1718
|
-
depends_on: z2.array(z2.string()).optional(),
|
|
1719
|
-
source_findings: z2.array(z2.string()).optional()
|
|
1720
|
-
}).strict();
|
|
1721
|
-
var planJsonSchema = z2.object({
|
|
1722
|
-
summary: z2.string().min(1),
|
|
1723
|
-
steps: z2.array(planStepJsonSchema).min(1),
|
|
1724
|
-
risks: z2.array(z2.string()).optional(),
|
|
1725
|
-
notes: z2.array(z2.string()).optional()
|
|
1726
|
-
}).strict();
|
|
1727
3048
|
var ModelPlanner = class {
|
|
1728
3049
|
constructor(options) {
|
|
1729
3050
|
this.options = options;
|
|
@@ -1737,7 +3058,7 @@ var ModelPlanner = class {
|
|
|
1737
3058
|
const messages = [
|
|
1738
3059
|
{
|
|
1739
3060
|
role: "system",
|
|
1740
|
-
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 ?? "")
|
|
3061
|
+
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 ?? "")
|
|
1741
3062
|
},
|
|
1742
3063
|
{ role: "user", content: context }
|
|
1743
3064
|
];
|
|
@@ -1748,60 +3069,86 @@ var ModelPlanner = class {
|
|
|
1748
3069
|
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
1749
3070
|
}
|
|
1750
3071
|
const parsed = parsePlanJson(responseText);
|
|
1751
|
-
if (!parsed) {
|
|
1752
|
-
return this.degrade(request,
|
|
3072
|
+
if (!parsed.ok) {
|
|
3073
|
+
return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
|
|
1753
3074
|
}
|
|
1754
3075
|
const plan = {
|
|
1755
|
-
id: `plan-${Date.now().toString(36)}-${
|
|
3076
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
1756
3077
|
objective: request.objective,
|
|
3078
|
+
goal: parsed.goal,
|
|
1757
3079
|
summary: parsed.summary,
|
|
1758
3080
|
steps: parsed.steps,
|
|
1759
3081
|
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
1760
3082
|
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
3083
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
1761
3084
|
generatedBy: `model:${this.options.model}`,
|
|
1762
3085
|
model: this.options.model,
|
|
1763
3086
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
1764
3087
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
1765
3088
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1766
3089
|
};
|
|
1767
|
-
|
|
3090
|
+
const validation = validatePlan(plan);
|
|
3091
|
+
if (!validation.valid) {
|
|
3092
|
+
return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
|
|
3093
|
+
}
|
|
3094
|
+
return validation.plan;
|
|
1768
3095
|
}
|
|
1769
|
-
async degrade(request, reason) {
|
|
3096
|
+
async degrade(request, reason, issues = []) {
|
|
1770
3097
|
if (this.options.fallback) {
|
|
1771
3098
|
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
1772
3099
|
return this.options.fallback.plan(request);
|
|
1773
3100
|
}
|
|
1774
|
-
throw new PlannerError("
|
|
3101
|
+
throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
|
|
3102
|
+
details: { reason, issues }
|
|
3103
|
+
});
|
|
1775
3104
|
}
|
|
1776
3105
|
};
|
|
1777
3106
|
function parsePlanJson(text) {
|
|
1778
3107
|
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
1779
3108
|
const start = unfenced.indexOf("{");
|
|
1780
3109
|
const end = unfenced.lastIndexOf("}");
|
|
1781
|
-
if (start < 0 || end <= start)
|
|
3110
|
+
if (start < 0 || end <= start) {
|
|
3111
|
+
return { ok: false, reason: "planner output contained no JSON object", issues: [] };
|
|
3112
|
+
}
|
|
3113
|
+
let json;
|
|
1782
3114
|
try {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
if (!parsed.success) return void 0;
|
|
1786
|
-
const steps = parsed.data.steps.map((step, index) => ({
|
|
1787
|
-
id: step.id ?? `step-${index + 1}`,
|
|
1788
|
-
title: step.title,
|
|
1789
|
-
description: step.description,
|
|
1790
|
-
order: step.order ?? index + 1,
|
|
1791
|
-
targets: step.targets,
|
|
1792
|
-
acceptanceCriteria: step.acceptance_criteria,
|
|
1793
|
-
dependsOn: step.depends_on,
|
|
1794
|
-
sourceFindings: step.source_findings
|
|
1795
|
-
}));
|
|
3115
|
+
json = JSON.parse(unfenced.slice(start, end + 1));
|
|
3116
|
+
} catch (error) {
|
|
1796
3117
|
return {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
notes: parsed.data.notes
|
|
3118
|
+
ok: false,
|
|
3119
|
+
reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
3120
|
+
issues: []
|
|
1801
3121
|
};
|
|
1802
|
-
} catch {
|
|
1803
|
-
return void 0;
|
|
1804
3122
|
}
|
|
3123
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
3124
|
+
if (!parsed.success) {
|
|
3125
|
+
return {
|
|
3126
|
+
ok: false,
|
|
3127
|
+
reason: "planner output did not match the plan schema",
|
|
3128
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
3129
|
+
path: issue.path.join(".") || "(root)",
|
|
3130
|
+
message: issue.message
|
|
3131
|
+
}))
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
3135
|
+
id: step.id ?? `step-${index + 1}`,
|
|
3136
|
+
title: step.title,
|
|
3137
|
+
description: step.description,
|
|
3138
|
+
order: step.order ?? index + 1,
|
|
3139
|
+
targets: step.targets,
|
|
3140
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
3141
|
+
dependsOn: step.depends_on,
|
|
3142
|
+
sourceFindings: step.source_findings
|
|
3143
|
+
})).sort((a, b) => a.order - b.order);
|
|
3144
|
+
return {
|
|
3145
|
+
ok: true,
|
|
3146
|
+
goal: parsed.data.goal,
|
|
3147
|
+
summary: parsed.data.summary,
|
|
3148
|
+
steps,
|
|
3149
|
+
risks: parsed.data.risks,
|
|
3150
|
+
notes: parsed.data.notes
|
|
3151
|
+
};
|
|
1805
3152
|
}
|
|
1806
3153
|
function describe(error) {
|
|
1807
3154
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1815,9 +3162,23 @@ var PipelineEngine = class {
|
|
|
1815
3162
|
}
|
|
1816
3163
|
options;
|
|
1817
3164
|
log;
|
|
3165
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3166
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
1818
3167
|
get store() {
|
|
1819
3168
|
return this.options.store;
|
|
1820
3169
|
}
|
|
3170
|
+
/**
|
|
3171
|
+
* Saves the run while preserving fields that may be set by another process
|
|
3172
|
+
* (e.g. a cancellation request) — an in-memory snapshot must never clobber
|
|
3173
|
+
* externally requested cancellation (§9.2).
|
|
3174
|
+
*/
|
|
3175
|
+
async persist(run) {
|
|
3176
|
+
const current = await this.options.store.get(run.id);
|
|
3177
|
+
await this.options.store.save({
|
|
3178
|
+
...run,
|
|
3179
|
+
...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
1821
3182
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
1822
3183
|
validate(def) {
|
|
1823
3184
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -1917,18 +3278,44 @@ var PipelineEngine = class {
|
|
|
1917
3278
|
);
|
|
1918
3279
|
}
|
|
1919
3280
|
}
|
|
3281
|
+
if (step.model) {
|
|
3282
|
+
const separator = step.model.indexOf(":");
|
|
3283
|
+
if (separator <= 0 || separator === step.model.length - 1) {
|
|
3284
|
+
throw new PipelineError(
|
|
3285
|
+
"PIPELINE_INVALID",
|
|
3286
|
+
`Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
3289
|
+
const providerName = step.model.slice(0, separator);
|
|
3290
|
+
if (!this.options.providers.some((p) => p.name === providerName)) {
|
|
3291
|
+
throw new PipelineError(
|
|
3292
|
+
"PIPELINE_INVALID",
|
|
3293
|
+
`Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
1920
3297
|
}
|
|
1921
3298
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1922
3299
|
const run = {
|
|
1923
|
-
id: `run-${Date.now().toString(36)}-${
|
|
3300
|
+
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
1924
3301
|
pipelineId: def.id,
|
|
3302
|
+
attempt: 1,
|
|
1925
3303
|
objective,
|
|
1926
3304
|
status: "running",
|
|
1927
3305
|
startedAt: now,
|
|
1928
3306
|
stepRuns: []
|
|
1929
3307
|
};
|
|
1930
3308
|
await this.options.store.save(run);
|
|
1931
|
-
|
|
3309
|
+
const liveTaskIds = /* @__PURE__ */ new Set();
|
|
3310
|
+
this.activeRuns.set(run.id, {
|
|
3311
|
+
taskIds: liveTaskIds,
|
|
3312
|
+
cancelTasks: async () => {
|
|
3313
|
+
for (const taskId of liveTaskIds) {
|
|
3314
|
+
await this.options.tasks.cancel(taskId).catch(() => void 0);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
|
|
1932
3319
|
await this.options.audit?.record({
|
|
1933
3320
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1934
3321
|
action: "pipeline.run.started",
|
|
@@ -1940,7 +3327,22 @@ var PipelineEngine = class {
|
|
|
1940
3327
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
1941
3328
|
const contextParts = [];
|
|
1942
3329
|
const allReports = [];
|
|
3330
|
+
let cancelled = false;
|
|
1943
3331
|
while (pending.size > 0) {
|
|
3332
|
+
const persisted = await this.options.store.get(run.id);
|
|
3333
|
+
if (persisted?.cancellationRequested === true) {
|
|
3334
|
+
cancelled = true;
|
|
3335
|
+
run.cancellationRequested = true;
|
|
3336
|
+
run.cancellationRequestedAt = persisted.cancellationRequestedAt;
|
|
3337
|
+
await this.activeRuns.get(run.id)?.cancelTasks();
|
|
3338
|
+
for (const step of def.steps.filter((s) => pending.has(s.id))) {
|
|
3339
|
+
const stepRun = { stepId: step.id, status: "cancelled" };
|
|
3340
|
+
run.stepRuns.push(stepRun);
|
|
3341
|
+
options.onStep?.(stepRun);
|
|
3342
|
+
}
|
|
3343
|
+
pending.clear();
|
|
3344
|
+
break;
|
|
3345
|
+
}
|
|
1944
3346
|
const ready = def.steps.filter(
|
|
1945
3347
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
1946
3348
|
);
|
|
@@ -1957,7 +3359,8 @@ var PipelineEngine = class {
|
|
|
1957
3359
|
...role ? { role } : {},
|
|
1958
3360
|
combinedContext: contextParts.join("\n\n"),
|
|
1959
3361
|
reports: [...allReports],
|
|
1960
|
-
pipelineRunId: run.id
|
|
3362
|
+
pipelineRunId: run.id,
|
|
3363
|
+
runId: run.id
|
|
1961
3364
|
};
|
|
1962
3365
|
return this.executeStep(step, ctx);
|
|
1963
3366
|
})
|
|
@@ -1974,7 +3377,9 @@ ${contribution}`);
|
|
|
1974
3377
|
}
|
|
1975
3378
|
const stepRun = {
|
|
1976
3379
|
stepId: step.id,
|
|
1977
|
-
|
|
3380
|
+
attempt: outcome.attempt ?? 1,
|
|
3381
|
+
...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
|
|
3382
|
+
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
1978
3383
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
1979
3384
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
1980
3385
|
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
@@ -1983,10 +3388,12 @@ ${contribution}`);
|
|
|
1983
3388
|
options.onStep?.(stepRun);
|
|
1984
3389
|
});
|
|
1985
3390
|
run.reports = [...allReports];
|
|
1986
|
-
await this.
|
|
3391
|
+
await this.persist(run);
|
|
1987
3392
|
}
|
|
1988
|
-
const
|
|
1989
|
-
const
|
|
3393
|
+
const allOutcomes = [...outcomes.values()];
|
|
3394
|
+
const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
|
|
3395
|
+
const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3396
|
+
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
|
|
1990
3397
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
1991
3398
|
let resultSummary;
|
|
1992
3399
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -2014,16 +3421,19 @@ ${contribution}`);
|
|
|
2014
3421
|
run.reports = collected;
|
|
2015
3422
|
}
|
|
2016
3423
|
}
|
|
3424
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3425
|
+
this.activeRuns.delete(run.id);
|
|
2017
3426
|
const finished = {
|
|
2018
3427
|
...run,
|
|
2019
3428
|
status: finalStatus,
|
|
2020
|
-
finishedAt
|
|
3429
|
+
finishedAt,
|
|
3430
|
+
attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
2021
3431
|
...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
|
|
2022
3432
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2023
3433
|
...planId !== void 0 ? { planId } : {},
|
|
2024
3434
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
2025
3435
|
};
|
|
2026
|
-
await this.
|
|
3436
|
+
await this.persist(finished);
|
|
2027
3437
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
2028
3438
|
await this.options.audit?.record({
|
|
2029
3439
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2033,6 +3443,23 @@ ${contribution}`);
|
|
|
2033
3443
|
});
|
|
2034
3444
|
return finished;
|
|
2035
3445
|
}
|
|
3446
|
+
/** First-class cancellation (§9.2): requests cancellation and terminates
|
|
3447
|
+
* tasks that are running in this process immediately. */
|
|
3448
|
+
async cancel(runId) {
|
|
3449
|
+
const run = await this.options.store.get(runId);
|
|
3450
|
+
if (!run) {
|
|
3451
|
+
throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
|
|
3452
|
+
}
|
|
3453
|
+
await this.options.store.save({
|
|
3454
|
+
...run,
|
|
3455
|
+
cancellationRequested: true,
|
|
3456
|
+
cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3457
|
+
});
|
|
3458
|
+
const active = this.activeRuns.get(runId);
|
|
3459
|
+
if (active) {
|
|
3460
|
+
await active.cancelTasks();
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
2036
3463
|
async get(runId) {
|
|
2037
3464
|
return this.options.store.get(runId);
|
|
2038
3465
|
}
|
|
@@ -2056,9 +3483,16 @@ ${contribution}`);
|
|
|
2056
3483
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
2057
3484
|
}
|
|
2058
3485
|
last = await this.executeStepOnce(step, ctx);
|
|
2059
|
-
if (last.status !== "failed") return last;
|
|
3486
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
2060
3487
|
}
|
|
2061
|
-
return last;
|
|
3488
|
+
return { ...last, attempt: retries + 1 };
|
|
3489
|
+
}
|
|
3490
|
+
/** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
|
|
3491
|
+
trackLiveTasks(runId, taskIds) {
|
|
3492
|
+
if (!runId) return;
|
|
3493
|
+
const active = this.activeRuns.get(runId);
|
|
3494
|
+
if (!active) return;
|
|
3495
|
+
for (const id of taskIds) active.taskIds.add(id);
|
|
2062
3496
|
}
|
|
2063
3497
|
async executeStepOnce(step, ctx) {
|
|
2064
3498
|
switch (step.kind) {
|
|
@@ -2091,7 +3525,10 @@ ${contribution}`);
|
|
|
2091
3525
|
})
|
|
2092
3526
|
)
|
|
2093
3527
|
);
|
|
3528
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
2094
3529
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
3530
|
+
const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
|
|
3531
|
+
const completedTasks = results.filter((r) => r.status === "completed");
|
|
2095
3532
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2096
3533
|
const lines = results.map((result, index) => {
|
|
2097
3534
|
const agentName = agentNames[index] ?? "unknown";
|
|
@@ -2101,16 +3538,25 @@ ${contribution}`);
|
|
|
2101
3538
|
const reports = results.flatMap((r) => r.result?.reports ?? []);
|
|
2102
3539
|
const summary = lines.join("\n");
|
|
2103
3540
|
if (failedTasks.length > 0) {
|
|
2104
|
-
const firstError = failedTasks[0]?.result?.error ?? "unknown
|
|
3541
|
+
const firstError = failedTasks[0]?.result?.error ?? failedTasks[0]?.lastError ?? "unknown outcome";
|
|
3542
|
+
const states = failedTasks.map((r) => r.status).join(", ");
|
|
2105
3543
|
return {
|
|
2106
|
-
|
|
3544
|
+
// Some agents succeeded → the step produced usable but incomplete
|
|
3545
|
+
// intelligence: partial, not failed (roadmap §9.3).
|
|
3546
|
+
status: completedTasks.length > 0 ? "partial" : "failed",
|
|
2107
3547
|
summary,
|
|
2108
3548
|
taskIds: created.map((t) => t.id),
|
|
2109
|
-
error: `${failedTasks.length}/${results.length} task(s)
|
|
3549
|
+
error: `${failedTasks.length}/${results.length} task(s) incomplete (${states}) \u2014 ${firstError}`,
|
|
2110
3550
|
reports
|
|
2111
3551
|
};
|
|
2112
3552
|
}
|
|
2113
|
-
return {
|
|
3553
|
+
return {
|
|
3554
|
+
status: "completed",
|
|
3555
|
+
summary,
|
|
3556
|
+
taskIds: created.map((t) => t.id),
|
|
3557
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
3558
|
+
reports
|
|
3559
|
+
};
|
|
2114
3560
|
}
|
|
2115
3561
|
async runSingle(step, ctx, agentName) {
|
|
2116
3562
|
const task = await this.options.tasks.create({
|
|
@@ -2119,10 +3565,13 @@ ${contribution}`);
|
|
|
2119
3565
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
2120
3566
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2121
3567
|
});
|
|
3568
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
2122
3569
|
const result = await this.options.tasks.run(task.id);
|
|
2123
3570
|
if (result.status !== "completed") {
|
|
2124
3571
|
return {
|
|
2125
|
-
|
|
3572
|
+
// Unknown/timed_out outcomes are not failures — the work may have
|
|
3573
|
+
// happened remotely; the step is incomplete, not broken.
|
|
3574
|
+
status: result.status === "unknown" || result.status === "timed_out" ? "partial" : "failed",
|
|
2126
3575
|
summary: result.result?.summary ?? "",
|
|
2127
3576
|
taskIds: [task.id],
|
|
2128
3577
|
error: result.result?.error ?? `task ${task.id} did not complete`,
|
|
@@ -2137,11 +3586,14 @@ ${contribution}`);
|
|
|
2137
3586
|
};
|
|
2138
3587
|
}
|
|
2139
3588
|
async runPlan(step, ctx) {
|
|
3589
|
+
const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports) : void 0;
|
|
2140
3590
|
const request = {
|
|
2141
3591
|
objective: ctx.objective,
|
|
2142
3592
|
reports: ctx.reports,
|
|
3593
|
+
...aggregated !== void 0 ? { aggregated } : {},
|
|
2143
3594
|
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2144
3595
|
...ctx.role ? { role: ctx.role } : {},
|
|
3596
|
+
...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
|
|
2145
3597
|
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2146
3598
|
};
|
|
2147
3599
|
let planner;
|
|
@@ -2300,99 +3752,6 @@ function buildPlanner(plannerConfig, chat, log = logger) {
|
|
|
2300
3752
|
});
|
|
2301
3753
|
}
|
|
2302
3754
|
|
|
2303
|
-
// src/reports/aggregate.ts
|
|
2304
|
-
import { randomBytes as randomBytes5 } from "crypto";
|
|
2305
|
-
var SEVERITY_RANK2 = {
|
|
2306
|
-
info: 0,
|
|
2307
|
-
low: 1,
|
|
2308
|
-
medium: 2,
|
|
2309
|
-
high: 3,
|
|
2310
|
-
critical: 4
|
|
2311
|
-
};
|
|
2312
|
-
function aggregateReports(reports, options = {}) {
|
|
2313
|
-
const threshold = options.similarityThreshold ?? 0.7;
|
|
2314
|
-
const findingGroups = [];
|
|
2315
|
-
const recommendationGroups = [];
|
|
2316
|
-
for (const report of reports) {
|
|
2317
|
-
for (const finding of report.findings) {
|
|
2318
|
-
mergeFinding(findingGroups, finding, report, threshold);
|
|
2319
|
-
}
|
|
2320
|
-
for (const text of report.recommendations) {
|
|
2321
|
-
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
2325
|
-
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2326
|
-
const summary = `${reports.length} report(s) from ${sources.length > 0 ? sources.join(", ") : "no agents"}: ${findingGroups.length} unique finding group(s) (${totalRawFindings} raw), ${recommendationGroups.length} recommendation group(s).`;
|
|
2327
|
-
return {
|
|
2328
|
-
id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2329
|
-
title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
|
|
2330
|
-
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
2331
|
-
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
2332
|
-
taskIds: options.taskIds ?? [],
|
|
2333
|
-
summary,
|
|
2334
|
-
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
2335
|
-
recommendations: recommendationGroups,
|
|
2336
|
-
sources,
|
|
2337
|
-
reportIds: reports.map((r) => r.id),
|
|
2338
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2339
|
-
};
|
|
2340
|
-
}
|
|
2341
|
-
function rank(finding) {
|
|
2342
|
-
return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
|
|
2343
|
-
}
|
|
2344
|
-
function mergeFinding(groups, finding, report, threshold) {
|
|
2345
|
-
const source = { agent: report.agent, reportId: report.id };
|
|
2346
|
-
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
2347
|
-
if (group) {
|
|
2348
|
-
group.occurrences += 1;
|
|
2349
|
-
group.details.push(finding.detail);
|
|
2350
|
-
group.sources.push(source);
|
|
2351
|
-
group.evidence.push(...finding.evidence ?? []);
|
|
2352
|
-
if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
|
|
2353
|
-
group.severity = finding.severity;
|
|
2354
|
-
}
|
|
2355
|
-
return;
|
|
2356
|
-
}
|
|
2357
|
-
groups.push({
|
|
2358
|
-
title: finding.title,
|
|
2359
|
-
details: [finding.detail],
|
|
2360
|
-
sources: [source],
|
|
2361
|
-
occurrences: 1,
|
|
2362
|
-
...finding.severity ? { severity: finding.severity } : {},
|
|
2363
|
-
evidence: [...finding.evidence ?? []]
|
|
2364
|
-
});
|
|
2365
|
-
}
|
|
2366
|
-
function mergeRecommendation(groups, text, report, threshold) {
|
|
2367
|
-
const source = { agent: report.agent, reportId: report.id };
|
|
2368
|
-
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
2369
|
-
if (group) {
|
|
2370
|
-
group.occurrences += 1;
|
|
2371
|
-
group.sources.push(source);
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
groups.push({ text, sources: [source], occurrences: 1 });
|
|
2375
|
-
}
|
|
2376
|
-
function similar(a, b, threshold) {
|
|
2377
|
-
const left = tokens(a);
|
|
2378
|
-
const right = tokens(b);
|
|
2379
|
-
if (left.size === 0 || right.size === 0) return false;
|
|
2380
|
-
let intersection = 0;
|
|
2381
|
-
for (const token of left) {
|
|
2382
|
-
if (right.has(token)) intersection += 1;
|
|
2383
|
-
}
|
|
2384
|
-
const union = left.size + right.size - intersection;
|
|
2385
|
-
return intersection / union >= threshold;
|
|
2386
|
-
}
|
|
2387
|
-
function tokens(text) {
|
|
2388
|
-
return new Set(
|
|
2389
|
-
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
2390
|
-
);
|
|
2391
|
-
}
|
|
2392
|
-
function truncate2(text, max) {
|
|
2393
|
-
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
3755
|
// src/reports/extract.ts
|
|
2397
3756
|
var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
|
|
2398
3757
|
function parseSections(raw) {
|
|
@@ -2442,6 +3801,7 @@ function extractReportFromText(raw, meta) {
|
|
|
2442
3801
|
id: `${meta.taskId}-f-${index + 1}`,
|
|
2443
3802
|
title: truncate3(text, 120),
|
|
2444
3803
|
detail: text,
|
|
3804
|
+
description: text,
|
|
2445
3805
|
severity: guessSeverity(text)
|
|
2446
3806
|
}));
|
|
2447
3807
|
return {
|
|
@@ -2452,7 +3812,8 @@ function extractReportFromText(raw, meta) {
|
|
|
2452
3812
|
summary,
|
|
2453
3813
|
findings,
|
|
2454
3814
|
recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
|
|
2455
|
-
|
|
3815
|
+
...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
|
|
3816
|
+
metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
|
|
2456
3817
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2457
3818
|
};
|
|
2458
3819
|
}
|
|
@@ -2468,27 +3829,46 @@ var ReportService = class {
|
|
|
2468
3829
|
}
|
|
2469
3830
|
store;
|
|
2470
3831
|
log;
|
|
3832
|
+
/** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
|
|
3833
|
+
lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
|
|
2471
3834
|
get storeRef() {
|
|
2472
3835
|
return this.store;
|
|
2473
3836
|
}
|
|
2474
|
-
/** Normalizes finished tasks into reports (§17 — normalizer)
|
|
3837
|
+
/** Normalizes finished tasks into reports (§17 — normalizer), validating each
|
|
3838
|
+
* one so malformed reports never reach aggregation (§13). */
|
|
2475
3839
|
async collectFromTasks(tasks) {
|
|
2476
|
-
const
|
|
3840
|
+
const candidates = [];
|
|
2477
3841
|
for (const task of tasks) {
|
|
2478
3842
|
const structured = task.result?.reports ?? [];
|
|
2479
3843
|
if (structured.length > 0) {
|
|
2480
|
-
|
|
3844
|
+
candidates.push(...structured);
|
|
2481
3845
|
continue;
|
|
2482
3846
|
}
|
|
2483
3847
|
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
2484
3848
|
if (text.trim().length === 0) continue;
|
|
2485
|
-
|
|
3849
|
+
candidates.push(
|
|
2486
3850
|
extractReportFromText(text, {
|
|
2487
3851
|
taskId: task.id,
|
|
2488
3852
|
agent: task.agent ?? "unknown"
|
|
2489
3853
|
})
|
|
2490
3854
|
);
|
|
2491
3855
|
}
|
|
3856
|
+
const reports = [];
|
|
3857
|
+
const rejected = [];
|
|
3858
|
+
for (const candidate of candidates) {
|
|
3859
|
+
const result = validateReport(candidate);
|
|
3860
|
+
if (result.valid) {
|
|
3861
|
+
reports.push(result.report);
|
|
3862
|
+
} else {
|
|
3863
|
+
rejected.push({
|
|
3864
|
+
key: candidate?.id ?? "(unknown)",
|
|
3865
|
+
reason: result.reason,
|
|
3866
|
+
issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
|
|
3867
|
+
});
|
|
3868
|
+
this.log.warn(`Rejected malformed report: ${result.reason}`);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
|
|
2492
3872
|
return reports;
|
|
2493
3873
|
}
|
|
2494
3874
|
async saveReports(reports) {
|
|
@@ -2556,7 +3936,11 @@ function buildPipelineEngine(config, log = logger) {
|
|
|
2556
3936
|
agents,
|
|
2557
3937
|
roles,
|
|
2558
3938
|
store: new FileTaskStore(),
|
|
2559
|
-
...config.project.memory !== void 0 ? {
|
|
3939
|
+
...config.project.memory !== void 0 ? {
|
|
3940
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
3941
|
+
required: config.project.memory.required
|
|
3942
|
+
})
|
|
3943
|
+
} : {},
|
|
2560
3944
|
audit: new FileAuditLog(),
|
|
2561
3945
|
log
|
|
2562
3946
|
});
|
|
@@ -2594,12 +3978,14 @@ var ProjectStores = class {
|
|
|
2594
3978
|
};
|
|
2595
3979
|
|
|
2596
3980
|
// src/version.ts
|
|
2597
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
3981
|
+
var OMNINODE_VERSION = "2.0.0-alpha.11";
|
|
2598
3982
|
export {
|
|
3983
|
+
AgentAdapterRegistry,
|
|
2599
3984
|
AgentError,
|
|
2600
3985
|
AgentRegistry,
|
|
2601
3986
|
ConfigError,
|
|
2602
3987
|
ConsoleLogSink,
|
|
3988
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
2603
3989
|
FileAuditLog,
|
|
2604
3990
|
FilePipelineRunStore,
|
|
2605
3991
|
FilePlanStore,
|
|
@@ -2609,6 +3995,8 @@ export {
|
|
|
2609
3995
|
JsonFileStore,
|
|
2610
3996
|
LocalMemoryProvider,
|
|
2611
3997
|
Logger,
|
|
3998
|
+
MAX_CONTEXT_CHARS,
|
|
3999
|
+
MAX_MESSAGE_BYTES,
|
|
2612
4000
|
MemoryError,
|
|
2613
4001
|
MemoryService,
|
|
2614
4002
|
ModelPlanner,
|
|
@@ -2618,16 +4006,22 @@ export {
|
|
|
2618
4006
|
OmniHilbrasProvider,
|
|
2619
4007
|
OmniNodeError,
|
|
2620
4008
|
OpenAICompatibleProvider,
|
|
4009
|
+
PROTOCOL_V2,
|
|
2621
4010
|
PipelineEngine,
|
|
2622
4011
|
PipelineError,
|
|
2623
4012
|
PlannerError,
|
|
2624
4013
|
ProcessAgent,
|
|
4014
|
+
ProcessAgentAdapter,
|
|
2625
4015
|
ProjectStores,
|
|
4016
|
+
ProtocolDecoder,
|
|
2626
4017
|
ProtocolError,
|
|
4018
|
+
ProtocolSession,
|
|
2627
4019
|
ProviderError,
|
|
2628
4020
|
RememberaMemoryProvider,
|
|
2629
4021
|
ReportService,
|
|
2630
4022
|
RoleRegistry,
|
|
4023
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
4024
|
+
TERMINAL_TASK_STATUSES,
|
|
2631
4025
|
TaskEngine,
|
|
2632
4026
|
TaskError,
|
|
2633
4027
|
agentConfigSchema,
|
|
@@ -2636,6 +4030,8 @@ export {
|
|
|
2636
4030
|
authHeaders,
|
|
2637
4031
|
buildAgentRegistry,
|
|
2638
4032
|
buildChildEnv,
|
|
4033
|
+
buildHello,
|
|
4034
|
+
buildHelloAck,
|
|
2639
4035
|
buildPipelineEngine,
|
|
2640
4036
|
buildPlanner,
|
|
2641
4037
|
buildPlannerContext,
|
|
@@ -2646,24 +4042,54 @@ export {
|
|
|
2646
4042
|
createMemoryProvider,
|
|
2647
4043
|
createProvider,
|
|
2648
4044
|
createTaskEngine,
|
|
4045
|
+
decodeMessage,
|
|
4046
|
+
decodeStream,
|
|
4047
|
+
defaultMetadata,
|
|
2649
4048
|
defaultProjectConfigYaml,
|
|
4049
|
+
defaultSearch,
|
|
4050
|
+
describeExit,
|
|
4051
|
+
detectConflicts,
|
|
4052
|
+
encodeMessage,
|
|
4053
|
+
envelope,
|
|
2650
4054
|
expandEnvRefs,
|
|
4055
|
+
extractMessage,
|
|
2651
4056
|
extractReportFromText,
|
|
2652
4057
|
findConfigFile,
|
|
4058
|
+
isNetworkError,
|
|
2653
4059
|
isOmniNodeError,
|
|
4060
|
+
isRetryable,
|
|
4061
|
+
isSupportedProtocol,
|
|
4062
|
+
isTimeoutError,
|
|
2654
4063
|
keywords,
|
|
4064
|
+
killTree,
|
|
4065
|
+
kindFromStatus,
|
|
2655
4066
|
loadConfig,
|
|
2656
4067
|
logger,
|
|
4068
|
+
newMessageId,
|
|
4069
|
+
normalizeEvidence,
|
|
4070
|
+
normalizeFinding,
|
|
4071
|
+
parseHello,
|
|
2657
4072
|
parsePlanJson,
|
|
4073
|
+
parseRetryAfter,
|
|
2658
4074
|
pipelineConfigSchema,
|
|
2659
4075
|
pipelineStepConfigSchema,
|
|
4076
|
+
planJsonSchema,
|
|
2660
4077
|
plannerConfigSchema,
|
|
2661
4078
|
projectConfigSchema,
|
|
2662
4079
|
providerConfigSchema,
|
|
4080
|
+
providerHttpError,
|
|
4081
|
+
providerTransportError,
|
|
2663
4082
|
registryKey,
|
|
2664
4083
|
requestJson,
|
|
4084
|
+
requireCompletion,
|
|
2665
4085
|
resolveApiKey,
|
|
4086
|
+
resolveEnvPolicy,
|
|
2666
4087
|
roleConfigSchema,
|
|
2667
|
-
|
|
4088
|
+
sanitizeChunk,
|
|
4089
|
+
similar,
|
|
4090
|
+
validateEnvelope,
|
|
4091
|
+
validatePlan,
|
|
4092
|
+
validateReport,
|
|
4093
|
+
validateWorkingDirectory
|
|
2668
4094
|
};
|
|
2669
4095
|
//# sourceMappingURL=index.js.map
|