@hilbras/omninode 2.0.0-alpha.2 → 2.0.0-alpha.20
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 +123 -249
- package/dist/cli/chunk-BTJDQTWH.js +722 -0
- package/dist/cli/chunk-BTJDQTWH.js.map +1 -0
- package/dist/cli/index.js +2852 -1206
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/providers-KPW3FURD.js +36 -0
- package/dist/cli/providers-KPW3FURD.js.map +1 -0
- package/dist/index.d.ts +1201 -153
- package/dist/index.js +2309 -470
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// src/types/task.ts
|
|
2
|
-
var TERMINAL_TASK_STATUSES = [
|
|
3
|
-
"completed",
|
|
4
|
-
"failed",
|
|
5
|
-
"timed_out",
|
|
6
|
-
"cancelled",
|
|
7
|
-
"partially_completed"
|
|
8
|
-
];
|
|
9
|
-
|
|
10
1
|
// src/errors/index.ts
|
|
11
2
|
var OmniNodeError = class extends Error {
|
|
12
3
|
code;
|
|
@@ -80,16 +71,19 @@ var ConsoleLogSink = class {
|
|
|
80
71
|
};
|
|
81
72
|
var Logger = class _Logger {
|
|
82
73
|
level;
|
|
74
|
+
format;
|
|
83
75
|
sink;
|
|
84
76
|
context;
|
|
85
77
|
constructor(options = {}) {
|
|
86
78
|
this.level = options.level ?? "info";
|
|
79
|
+
this.format = options.format ?? "text";
|
|
87
80
|
this.sink = options.sink ?? new ConsoleLogSink();
|
|
88
81
|
this.context = options.context ?? {};
|
|
89
82
|
}
|
|
90
83
|
child(context) {
|
|
91
84
|
return new _Logger({
|
|
92
85
|
level: this.level,
|
|
86
|
+
format: this.format,
|
|
93
87
|
sink: this.sink,
|
|
94
88
|
context: { ...this.context, ...context }
|
|
95
89
|
});
|
|
@@ -108,8 +102,13 @@ var Logger = class _Logger {
|
|
|
108
102
|
}
|
|
109
103
|
log(level, message, data) {
|
|
110
104
|
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) return;
|
|
111
|
-
const
|
|
105
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
112
106
|
const merged = { ...this.context, ...data };
|
|
107
|
+
if (this.format === "json") {
|
|
108
|
+
this.sink.write(level, JSON.stringify({ at, level, message, ...merged }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const parts = [at, level.toUpperCase(), message];
|
|
113
112
|
if (Object.keys(merged).length > 0) {
|
|
114
113
|
parts.push(JSON.stringify(merged));
|
|
115
114
|
}
|
|
@@ -117,6 +116,10 @@ var Logger = class _Logger {
|
|
|
117
116
|
}
|
|
118
117
|
};
|
|
119
118
|
var logger = new Logger();
|
|
119
|
+
function configureLogger(options) {
|
|
120
|
+
if (options.level !== void 0) logger.level = options.level;
|
|
121
|
+
if (options.format !== void 0) logger.format = options.format;
|
|
122
|
+
}
|
|
120
123
|
|
|
121
124
|
// src/config/schema.ts
|
|
122
125
|
import { z } from "zod";
|
|
@@ -125,6 +128,7 @@ var providerConfigSchema = z.object({
|
|
|
125
128
|
type: z.enum(["openai-compatible", "omnihilbras", "openrouter", "local", "custom"]),
|
|
126
129
|
base_url: z.string().url(),
|
|
127
130
|
api_key_env_var: z.string().min(1).optional(),
|
|
131
|
+
timeout_ms: z.number().int().positive().optional(),
|
|
128
132
|
headers: z.record(z.string()).optional(),
|
|
129
133
|
enabled: z.boolean().optional(),
|
|
130
134
|
metadata: z.record(z.unknown()).optional()
|
|
@@ -140,6 +144,11 @@ var agentConfigSchema = z.object({
|
|
|
140
144
|
input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
|
|
141
145
|
timeout_ms: z.number().int().positive().optional(),
|
|
142
146
|
inherit_env: z.boolean().optional(),
|
|
147
|
+
env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
|
|
148
|
+
env_allowlist: z.array(z.string()).optional(),
|
|
149
|
+
env_denylist: z.array(z.string()).optional(),
|
|
150
|
+
max_output_bytes: z.number().int().positive().optional(),
|
|
151
|
+
allow_external_cwd: z.boolean().optional(),
|
|
143
152
|
metadata: z.record(z.unknown()).optional()
|
|
144
153
|
}).strict();
|
|
145
154
|
var roleConfigSchema = z.object({
|
|
@@ -160,7 +169,9 @@ var pipelineStepConfigSchema = z.object({
|
|
|
160
169
|
model: z.string().optional(),
|
|
161
170
|
depends_on: z.array(z.string()).optional(),
|
|
162
171
|
retries: z.number().int().min(0).max(10).optional(),
|
|
163
|
-
condition: z.enum(["always", "on-success", "on-failure"]).optional()
|
|
172
|
+
condition: z.enum(["always", "on-success", "on-failure"]).optional(),
|
|
173
|
+
/** Constraints passed to the planner for plan steps (§15). */
|
|
174
|
+
constraints: z.array(z.string()).optional()
|
|
164
175
|
}).strict();
|
|
165
176
|
var pipelineConfigSchema = z.object({
|
|
166
177
|
id: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "pipeline id must be kebab-case"),
|
|
@@ -184,14 +195,135 @@ var projectConfigSchema = z.object({
|
|
|
184
195
|
memory: z.object({
|
|
185
196
|
provider: z.string().min(1).default("local"),
|
|
186
197
|
base_url: z.string().url().optional(),
|
|
187
|
-
api_key_env_var: z.string().min(1).optional()
|
|
198
|
+
api_key_env_var: z.string().min(1).optional(),
|
|
199
|
+
/** When true, memory failures fail tasks instead of being tolerated (§12). */
|
|
200
|
+
required: z.boolean().optional()
|
|
188
201
|
}).strict().optional()
|
|
189
202
|
}).strict();
|
|
190
203
|
var appConfigSchema = z.object({
|
|
204
|
+
/** Active profile; overlays live under `profiles` (§16). */
|
|
205
|
+
profile: z.string().min(1).optional(),
|
|
206
|
+
/** Named partial overlays, e.g. development/production/testing. */
|
|
207
|
+
profiles: z.record(z.unknown()).optional(),
|
|
191
208
|
project: projectConfigSchema,
|
|
192
|
-
logging: z.object({
|
|
209
|
+
logging: z.object({
|
|
210
|
+
level: z.enum(["debug", "info", "warn", "error"]),
|
|
211
|
+
format: z.enum(["text", "json"]).optional()
|
|
212
|
+
}).strict().optional()
|
|
193
213
|
}).strict();
|
|
194
214
|
|
|
215
|
+
// src/config/secrets.ts
|
|
216
|
+
var SAFE_KEY_SUFFIXES = ["_env", "_env_var", "_file", "_path", "_ref", "_id", "_url", "_command"];
|
|
217
|
+
var SECRET_KEY = /^\s*([A-Za-z0-9_-]*(?:api[_-]?key|token|password|passwd|secret|credential)[A-Za-z0-9_-]*)\s*:\s*(.*)$/i;
|
|
218
|
+
var SAFE_VALUE = /^\s*(?:\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*|<[^>]*>|""|''|null|~)\s*$/i;
|
|
219
|
+
function scanForInlineSecrets(source) {
|
|
220
|
+
const findings = [];
|
|
221
|
+
source.split("\n").forEach((line, index) => {
|
|
222
|
+
const trimmed = line.trim();
|
|
223
|
+
if (trimmed.startsWith("#")) return;
|
|
224
|
+
const match = SECRET_KEY.exec(line);
|
|
225
|
+
if (!match) return;
|
|
226
|
+
const key = match[1] ?? "";
|
|
227
|
+
const rawValue = (match[2] ?? "").trim().replace(/,\s*$/, "");
|
|
228
|
+
const keyIsReference = SAFE_KEY_SUFFIXES.some((suffix) => key.toLowerCase().endsWith(suffix));
|
|
229
|
+
if (keyIsReference) return;
|
|
230
|
+
const value = rawValue.replace(/^["']|["']$/g, "");
|
|
231
|
+
if (value.length === 0 || SAFE_VALUE.test(rawValue)) return;
|
|
232
|
+
if (value.length < 8) return;
|
|
233
|
+
findings.push({
|
|
234
|
+
line: index + 1,
|
|
235
|
+
key,
|
|
236
|
+
excerpt: `${key}: ${value.slice(0, 3)}\u2026 (${value.length} chars, redacted)`
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return findings;
|
|
240
|
+
}
|
|
241
|
+
function describeSecretFindings(file, findings) {
|
|
242
|
+
const lines = findings.map((f) => ` line ${f.line}: ${f.excerpt}`).join("\n");
|
|
243
|
+
return `${file} appears to contain ${findings.length} inline secret(s):
|
|
244
|
+
${lines}
|
|
245
|
+
OmniNode never stores credentials in configuration. Replace each value with an environment-variable reference, e.g. api_key_env_var: MY_API_KEY.`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/config/profiles.ts
|
|
249
|
+
import { homedir } from "os";
|
|
250
|
+
import path from "path";
|
|
251
|
+
function userConfigPath(env = process.env) {
|
|
252
|
+
if (env.OMNINODE_USER_CONFIG) return env.OMNINODE_USER_CONFIG;
|
|
253
|
+
const home = env.HOME ?? homedir();
|
|
254
|
+
return path.join(home, ".omninode", "config.yaml");
|
|
255
|
+
}
|
|
256
|
+
function resolveProfile(input, env) {
|
|
257
|
+
if (env.OMNINODE_PROFILE) return env.OMNINODE_PROFILE;
|
|
258
|
+
if (typeof input.profile === "string" && input.profile.length > 0) return input.profile;
|
|
259
|
+
return "default";
|
|
260
|
+
}
|
|
261
|
+
function mergeConfig(base, overlay) {
|
|
262
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) {
|
|
263
|
+
return overlay === void 0 ? base : overlay;
|
|
264
|
+
}
|
|
265
|
+
const result = { ...base };
|
|
266
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
267
|
+
const existing = result[key];
|
|
268
|
+
if (Array.isArray(value)) {
|
|
269
|
+
result[key] = value;
|
|
270
|
+
} else if (isPlainObject(value) && isPlainObject(existing)) {
|
|
271
|
+
result[key] = mergeConfig(existing, value);
|
|
272
|
+
} else {
|
|
273
|
+
result[key] = value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
function isPlainObject(value) {
|
|
279
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
280
|
+
}
|
|
281
|
+
function asObject(value) {
|
|
282
|
+
return isPlainObject(value) ? value : {};
|
|
283
|
+
}
|
|
284
|
+
function applyEnvOverrides(input, env) {
|
|
285
|
+
const applied = [];
|
|
286
|
+
const config = mergeConfig(input, {});
|
|
287
|
+
if (env.OMNINODE_LOG_LEVEL) {
|
|
288
|
+
config.logging = { ...asObject(config.logging), level: env.OMNINODE_LOG_LEVEL };
|
|
289
|
+
applied.push("OMNINODE_LOG_LEVEL");
|
|
290
|
+
}
|
|
291
|
+
if (env.OMNINODE_LOG_FORMAT) {
|
|
292
|
+
config.logging = { ...asObject(config.logging), format: env.OMNINODE_LOG_FORMAT };
|
|
293
|
+
applied.push("OMNINODE_LOG_FORMAT");
|
|
294
|
+
}
|
|
295
|
+
const memoryProvider = env.OMNINODE_MEMORY_PROVIDER;
|
|
296
|
+
const memoryRequired = env.OMNINODE_MEMORY_REQUIRED;
|
|
297
|
+
if (memoryProvider || memoryRequired) {
|
|
298
|
+
const project = asObject(config.project);
|
|
299
|
+
config.project = {
|
|
300
|
+
...project,
|
|
301
|
+
memory: {
|
|
302
|
+
...asObject(project.memory ?? { provider: "local" }),
|
|
303
|
+
...memoryProvider ? { provider: memoryProvider } : {},
|
|
304
|
+
...memoryRequired ? { required: memoryRequired !== "false" } : {}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
if (memoryProvider) applied.push("OMNINODE_MEMORY_PROVIDER");
|
|
308
|
+
if (memoryRequired) applied.push("OMNINODE_MEMORY_REQUIRED");
|
|
309
|
+
}
|
|
310
|
+
return { config, applied };
|
|
311
|
+
}
|
|
312
|
+
function applyDefaults(input) {
|
|
313
|
+
const applied = [];
|
|
314
|
+
const config = mergeConfig(input, {});
|
|
315
|
+
const logging = asObject(config.logging);
|
|
316
|
+
if (logging.level === void 0) {
|
|
317
|
+
config.logging = { ...logging, level: "info" };
|
|
318
|
+
applied.push("logging.level");
|
|
319
|
+
}
|
|
320
|
+
if (logging.format === void 0) {
|
|
321
|
+
config.logging = { ...asObject(config.logging), format: "text" };
|
|
322
|
+
applied.push("logging.format");
|
|
323
|
+
}
|
|
324
|
+
return { config, applied };
|
|
325
|
+
}
|
|
326
|
+
|
|
195
327
|
// src/config/loader.ts
|
|
196
328
|
import { existsSync, readFileSync } from "fs";
|
|
197
329
|
import { parse as parseYaml } from "yaml";
|
|
@@ -218,34 +350,132 @@ function expandEnvRefs(text, env, source = "config") {
|
|
|
218
350
|
}
|
|
219
351
|
);
|
|
220
352
|
}
|
|
221
|
-
function
|
|
353
|
+
function loadConfigDetailed(options = {}) {
|
|
222
354
|
const env = options.env ?? process.env;
|
|
223
|
-
const
|
|
224
|
-
|
|
355
|
+
const explicitPath = options.path;
|
|
356
|
+
let input = {};
|
|
357
|
+
let projectFile;
|
|
358
|
+
let userFile;
|
|
359
|
+
if (!explicitPath) {
|
|
360
|
+
const candidate = userConfigPath(env);
|
|
361
|
+
if (existsSync(candidate)) {
|
|
362
|
+
input = mergeConfig(input, readConfigDocument(candidate, env));
|
|
363
|
+
userFile = candidate;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const resolvedProject = explicitPath ?? (options.directory ? findConfigFile(options.directory) : findConfigFile());
|
|
367
|
+
if (resolvedProject) {
|
|
368
|
+
input = mergeConfig(input, readConfigDocument(resolvedProject, env));
|
|
369
|
+
projectFile = resolvedProject;
|
|
370
|
+
}
|
|
371
|
+
if (!projectFile && !userFile) {
|
|
225
372
|
throw new ConfigError(
|
|
226
373
|
"CONFIG_NOT_FOUND",
|
|
227
374
|
"No omninode.yaml found. Run `omninode init` to create one, or pass an explicit path."
|
|
228
375
|
);
|
|
229
376
|
}
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
throw new ConfigError(
|
|
377
|
+
const profile = options.profile ?? resolveProfile(input, env);
|
|
378
|
+
const overlay = input.profiles?.[profile];
|
|
379
|
+
if (overlay !== void 0) {
|
|
380
|
+
const { profiles: _drop, ...base } = input;
|
|
381
|
+
input = mergeConfig(base, overlay);
|
|
382
|
+
} else if (profile !== "default" && profile !== "") {
|
|
383
|
+
throw new ConfigError(
|
|
384
|
+
"CONFIG_INVALID",
|
|
385
|
+
`Profile "${profile}" is not defined in this configuration (known: ${Object.keys(input.profiles ?? {}).join(", ") || "none"}).`
|
|
386
|
+
);
|
|
237
387
|
}
|
|
238
|
-
const
|
|
388
|
+
const withEnv = applyEnvOverrides(input, env);
|
|
389
|
+
const withDefaults = applyDefaults(withEnv.config);
|
|
390
|
+
const parsed = appConfigSchema.safeParse(withDefaults.config);
|
|
239
391
|
if (!parsed.success) {
|
|
240
392
|
const issues = parsed.error.issues.map((issue) => {
|
|
241
|
-
const location = issue.path.
|
|
393
|
+
const location = issue.path.join(".") || "(root)";
|
|
242
394
|
return `${location}: ${issue.message}`;
|
|
243
395
|
});
|
|
244
|
-
throw new ConfigError(
|
|
245
|
-
|
|
246
|
-
|
|
396
|
+
throw new ConfigError(
|
|
397
|
+
"CONFIG_INVALID",
|
|
398
|
+
`Invalid configuration:
|
|
399
|
+
${issues.join("\n ")}`,
|
|
400
|
+
{ details: { issues } }
|
|
401
|
+
);
|
|
247
402
|
}
|
|
248
|
-
|
|
403
|
+
assertUniqueNames(parsed.data);
|
|
404
|
+
return {
|
|
405
|
+
config: toAppConfig(parsed.data),
|
|
406
|
+
sources: {
|
|
407
|
+
...projectFile !== void 0 ? { projectFile } : {},
|
|
408
|
+
...userFile !== void 0 ? { userFile } : {},
|
|
409
|
+
profile,
|
|
410
|
+
envOverrides: withEnv.applied,
|
|
411
|
+
defaults: withDefaults.applied
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function assertUniqueNames(data) {
|
|
416
|
+
const groups = [
|
|
417
|
+
["provider", data.project.providers.map((p) => p.name)],
|
|
418
|
+
["agent", data.project.agents.map((a) => a.name)],
|
|
419
|
+
["role", data.project.roles.map((r) => r.id)],
|
|
420
|
+
["pipeline", data.project.pipelines.map((p) => p.id)]
|
|
421
|
+
];
|
|
422
|
+
for (const [kind, names] of groups) {
|
|
423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
424
|
+
for (const name of names) {
|
|
425
|
+
if (seen.has(name)) {
|
|
426
|
+
throw new ConfigError(
|
|
427
|
+
"CONFIG_INVALID",
|
|
428
|
+
`Duplicate ${kind} name "${name}" \u2014 every ${kind} must have a unique name.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
seen.add(name);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function readConfigDocument(path2, optionsEnv) {
|
|
436
|
+
const raw = readFileSync(path2, "utf8");
|
|
437
|
+
const secretFindings = scanForInlineSecrets(raw);
|
|
438
|
+
if (secretFindings.length > 0) {
|
|
439
|
+
throw new ConfigError("CONFIG_INVALID", describeSecretFindings(path2, secretFindings));
|
|
440
|
+
}
|
|
441
|
+
const expanded = expandEnvRefs(raw, optionsEnv, path2);
|
|
442
|
+
try {
|
|
443
|
+
return parseYaml(expanded);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
throw new ConfigError("CONFIG_INVALID", `Failed to parse ${path2}.`, { cause: error });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function loadConfig(options = {}) {
|
|
449
|
+
return loadConfigDetailed(options).config;
|
|
450
|
+
}
|
|
451
|
+
function configDiagnostics(config) {
|
|
452
|
+
const notes = [];
|
|
453
|
+
for (const provider of config.project.providers) {
|
|
454
|
+
if (!provider.apiKeyEnvVar && provider.baseUrl.startsWith("https://")) {
|
|
455
|
+
notes.push(
|
|
456
|
+
`Provider "${provider.name}": API key reference is missing \u2014 add api_key_env_var: <ENV_VAR> unless this endpoint is intentionally keyless.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const agent of config.project.agents) {
|
|
461
|
+
if (!agent.command && agent.integration !== "custom") {
|
|
462
|
+
notes.push(`Agent "${agent.name}": no command configured \u2014 the process adapter needs one.`);
|
|
463
|
+
}
|
|
464
|
+
if (agent.allowExternalCwd) {
|
|
465
|
+
notes.push(`Agent "${agent.name}": may run outside the project root (allow_external_cwd).`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
for (const pipeline of config.project.pipelines) {
|
|
469
|
+
const ids = new Set(pipeline.steps.map((s) => s.id));
|
|
470
|
+
for (const step of pipeline.steps) {
|
|
471
|
+
for (const dep of step.dependsOn ?? []) {
|
|
472
|
+
if (!ids.has(dep)) {
|
|
473
|
+
notes.push(`Pipeline "${pipeline.id}": step "${step.id}" depends on unknown step "${dep}".`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return notes;
|
|
249
479
|
}
|
|
250
480
|
function toAppConfig(yaml) {
|
|
251
481
|
return {
|
|
@@ -266,7 +496,8 @@ function toAppConfig(yaml) {
|
|
|
266
496
|
memory: {
|
|
267
497
|
provider: yaml.project.memory.provider,
|
|
268
498
|
baseUrl: yaml.project.memory.base_url,
|
|
269
|
-
apiKeyEnvVar: yaml.project.memory.api_key_env_var
|
|
499
|
+
apiKeyEnvVar: yaml.project.memory.api_key_env_var,
|
|
500
|
+
required: yaml.project.memory.required
|
|
270
501
|
}
|
|
271
502
|
} : {}
|
|
272
503
|
},
|
|
@@ -287,7 +518,8 @@ function toPipelineDefinition(raw) {
|
|
|
287
518
|
model: step.model,
|
|
288
519
|
dependsOn: step.depends_on,
|
|
289
520
|
retries: step.retries,
|
|
290
|
-
condition: step.condition
|
|
521
|
+
condition: step.condition,
|
|
522
|
+
constraints: step.constraints
|
|
291
523
|
}))
|
|
292
524
|
};
|
|
293
525
|
}
|
|
@@ -297,6 +529,7 @@ function toProviderConfig(raw) {
|
|
|
297
529
|
type: raw.type,
|
|
298
530
|
baseUrl: raw.base_url,
|
|
299
531
|
apiKeyEnvVar: raw.api_key_env_var,
|
|
532
|
+
timeoutMs: raw.timeout_ms,
|
|
300
533
|
headers: raw.headers,
|
|
301
534
|
enabled: raw.enabled,
|
|
302
535
|
metadata: raw.metadata
|
|
@@ -314,6 +547,11 @@ function toAgentConfig(raw) {
|
|
|
314
547
|
inputMode: raw.input_mode,
|
|
315
548
|
timeoutMs: raw.timeout_ms,
|
|
316
549
|
inheritEnv: raw.inherit_env,
|
|
550
|
+
envPolicy: raw.env_policy,
|
|
551
|
+
envAllowlist: raw.env_allowlist,
|
|
552
|
+
envDenylist: raw.env_denylist,
|
|
553
|
+
maxOutputBytes: raw.max_output_bytes,
|
|
554
|
+
allowExternalCwd: raw.allow_external_cwd,
|
|
317
555
|
metadata: raw.metadata
|
|
318
556
|
};
|
|
319
557
|
}
|
|
@@ -384,57 +622,213 @@ function authHeaders(config, env = process.env) {
|
|
|
384
622
|
}
|
|
385
623
|
|
|
386
624
|
// src/providers/http.ts
|
|
625
|
+
var HttpResponseError = class extends Error {
|
|
626
|
+
constructor(message, status, cause) {
|
|
627
|
+
super(message, { cause });
|
|
628
|
+
this.status = status;
|
|
629
|
+
this.name = "HttpResponseError";
|
|
630
|
+
}
|
|
631
|
+
status;
|
|
632
|
+
};
|
|
387
633
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
388
634
|
async function requestJson(url, options = {}) {
|
|
389
635
|
const { method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
401
|
-
});
|
|
402
|
-
} catch (error) {
|
|
403
|
-
const reason = error instanceof Error && error.name === "TimeoutError" ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
404
|
-
throw new ProviderError("PROVIDER_UNAVAILABLE", `Request to ${url} failed: ${reason}.`, {
|
|
405
|
-
cause: error
|
|
406
|
-
});
|
|
407
|
-
}
|
|
636
|
+
const response = await fetch(url, {
|
|
637
|
+
method,
|
|
638
|
+
headers: {
|
|
639
|
+
accept: "application/json",
|
|
640
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
641
|
+
...headers
|
|
642
|
+
},
|
|
643
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
644
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
645
|
+
});
|
|
408
646
|
let parsed;
|
|
409
647
|
try {
|
|
410
648
|
parsed = await response.json();
|
|
411
649
|
} catch (error) {
|
|
412
|
-
throw new
|
|
413
|
-
cause: error,
|
|
414
|
-
details: { status: response.status }
|
|
415
|
-
});
|
|
650
|
+
throw new HttpResponseError(`Response from ${url} was not valid JSON.`, response.status, error);
|
|
416
651
|
}
|
|
417
652
|
return { status: response.status, body: parsed };
|
|
418
653
|
}
|
|
419
654
|
|
|
655
|
+
// src/providers/errors.ts
|
|
656
|
+
function kindToCode(kind) {
|
|
657
|
+
switch (kind) {
|
|
658
|
+
case "AUTHENTICATION_ERROR":
|
|
659
|
+
return "PROVIDER_AUTH_FAILED";
|
|
660
|
+
case "MODEL_NOT_FOUND":
|
|
661
|
+
return "MODEL_NOT_FOUND";
|
|
662
|
+
default:
|
|
663
|
+
return "PROVIDER_UNAVAILABLE";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function isRetryable(kind) {
|
|
667
|
+
return kind === "RATE_LIMIT_ERROR" || kind === "SERVER_ERROR" || kind === "NETWORK_ERROR";
|
|
668
|
+
}
|
|
669
|
+
function kindFromStatus(status) {
|
|
670
|
+
if (status === 401 || status === 403) return "AUTHENTICATION_ERROR";
|
|
671
|
+
if (status === 429) return "RATE_LIMIT_ERROR";
|
|
672
|
+
if (status === 404) return "MODEL_NOT_FOUND";
|
|
673
|
+
if (status === 408 || status === 504) return "TIMEOUT";
|
|
674
|
+
if (status >= 500) return "SERVER_ERROR";
|
|
675
|
+
if (status >= 400) return "INVALID_REQUEST";
|
|
676
|
+
return "UNKNOWN_ERROR";
|
|
677
|
+
}
|
|
678
|
+
function extractMessage(body) {
|
|
679
|
+
if (body === null || typeof body !== "object") return void 0;
|
|
680
|
+
const root = body;
|
|
681
|
+
const error = root.error ?? root;
|
|
682
|
+
const message = error.message ?? error.detail ?? root.message;
|
|
683
|
+
return typeof message === "string" ? message : void 0;
|
|
684
|
+
}
|
|
685
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
686
|
+
if (!value) return void 0;
|
|
687
|
+
const seconds = Number(value);
|
|
688
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
689
|
+
const date = Date.parse(value);
|
|
690
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
691
|
+
}
|
|
692
|
+
function isTimeoutError(error) {
|
|
693
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError") || typeof error?.code === "string" && error.code === "ABORT_ERR";
|
|
694
|
+
}
|
|
695
|
+
function isNetworkError(error) {
|
|
696
|
+
return error instanceof TypeError || typeof error?.cause === "object" && error instanceof Error && !error.code;
|
|
697
|
+
}
|
|
698
|
+
function fallbackMessage(kind, context, status) {
|
|
699
|
+
const { provider, operation } = context;
|
|
700
|
+
switch (kind) {
|
|
701
|
+
case "AUTHENTICATION_ERROR":
|
|
702
|
+
return `Authentication with provider "${provider}" failed (HTTP ${status}). Check that the referenced API key is set and valid.`;
|
|
703
|
+
case "RATE_LIMIT_ERROR":
|
|
704
|
+
return `Provider "${provider}" rate-limited ${operation} (HTTP ${status}). Retry later.`;
|
|
705
|
+
case "MODEL_NOT_FOUND":
|
|
706
|
+
return `Provider "${provider}" does not expose the requested model (HTTP ${status}).`;
|
|
707
|
+
case "TIMEOUT":
|
|
708
|
+
return `Provider "${provider}" timed out during ${operation} (HTTP ${status}).`;
|
|
709
|
+
case "SERVER_ERROR":
|
|
710
|
+
return `Provider "${provider}" returned a server error during ${operation} (HTTP ${status}).`;
|
|
711
|
+
case "INVALID_REQUEST":
|
|
712
|
+
return `Provider "${provider}" rejected ${operation} as invalid (HTTP ${status}).`;
|
|
713
|
+
default:
|
|
714
|
+
return `Provider "${provider}" failed during ${operation} (HTTP ${status}).`;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function providerHttpError(context) {
|
|
718
|
+
const status = context.status ?? 0;
|
|
719
|
+
const kind = kindFromStatus(status);
|
|
720
|
+
const detail = extractMessage(context.body);
|
|
721
|
+
const retryAfterMs = context.retryAfterMs ?? parseRetryAfter(void 0);
|
|
722
|
+
return new ProviderError(
|
|
723
|
+
context.code ?? kindToCode(kind),
|
|
724
|
+
detail ?? fallbackMessage(kind, context, status),
|
|
725
|
+
{
|
|
726
|
+
cause: context.cause,
|
|
727
|
+
details: {
|
|
728
|
+
kind,
|
|
729
|
+
provider: context.provider,
|
|
730
|
+
operation: context.operation,
|
|
731
|
+
status,
|
|
732
|
+
retryable: isRetryable(kind),
|
|
733
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {},
|
|
734
|
+
...detail ? { providerMessage: detail } : {}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
function providerTransportError(context) {
|
|
740
|
+
const cause = context.cause;
|
|
741
|
+
const kind = isTimeoutError(cause) ? "TIMEOUT" : cause instanceof SyntaxError || cause?.name === "HttpResponseError" ? "UNKNOWN_ERROR" : "NETWORK_ERROR";
|
|
742
|
+
const message = context.cause instanceof Error ? context.cause.message : String(context.cause);
|
|
743
|
+
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider request failed: ${message}`, {
|
|
744
|
+
cause: context.cause,
|
|
745
|
+
details: {
|
|
746
|
+
kind,
|
|
747
|
+
provider: context.provider,
|
|
748
|
+
operation: context.operation,
|
|
749
|
+
retryable: isRetryable(kind)
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
420
754
|
// src/providers/openai-compatible/index.ts
|
|
755
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
421
756
|
var OpenAICompatibleProvider = class {
|
|
422
757
|
config;
|
|
758
|
+
providerId;
|
|
759
|
+
authentication;
|
|
423
760
|
log;
|
|
424
|
-
|
|
761
|
+
/** Discovered models, so getModel() answers without another round-trip. */
|
|
762
|
+
discovered;
|
|
763
|
+
env;
|
|
764
|
+
audit;
|
|
765
|
+
constructor(config, log = logger, env = process.env, audit) {
|
|
766
|
+
this.audit = audit;
|
|
425
767
|
this.config = { ...config, baseUrl: normalizeBaseUrl(config.baseUrl) };
|
|
768
|
+
this.env = env;
|
|
769
|
+
this.providerId = config.providerId ?? config.name;
|
|
770
|
+
this.authentication = {
|
|
771
|
+
method: config.apiKeyEnvVar ? "bearer" : "none",
|
|
772
|
+
...config.apiKeyEnvVar ? { envVar: config.apiKeyEnvVar } : {},
|
|
773
|
+
configured: config.apiKeyEnvVar ? Boolean(env[config.apiKeyEnvVar]) : true
|
|
774
|
+
};
|
|
426
775
|
this.log = log.child({ provider: config.name });
|
|
427
776
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
777
|
+
/** Provider-level request timeout (roadmap §6.6/§11) with the shared default. */
|
|
778
|
+
get requestTimeoutMs() {
|
|
779
|
+
return this.config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
780
|
+
}
|
|
781
|
+
/** Authenticate + discover (roadmap §10, Model Discovery). */
|
|
782
|
+
async connect() {
|
|
783
|
+
authHeaders(this.config, this.env);
|
|
784
|
+
const models = await this.listModels();
|
|
785
|
+
this.discovered = models;
|
|
786
|
+
return models;
|
|
787
|
+
}
|
|
788
|
+
async getModel(modelId) {
|
|
789
|
+
const models = this.discovered ?? await this.connect();
|
|
790
|
+
return models.find((m) => m.id === modelId);
|
|
791
|
+
}
|
|
792
|
+
/** Raw /models body (protected so adapters can capture gateway-level metadata). */
|
|
793
|
+
async rawModelsResponse() {
|
|
794
|
+
const response = await this.modelsRequest();
|
|
795
|
+
return response.body;
|
|
796
|
+
}
|
|
797
|
+
async modelsRequest() {
|
|
798
|
+
let response;
|
|
799
|
+
try {
|
|
800
|
+
response = await requestJson(`${this.config.baseUrl}/models`, {
|
|
801
|
+
headers: authHeaders(this.config, this.env),
|
|
802
|
+
timeoutMs: this.requestTimeoutMs
|
|
803
|
+
});
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (error instanceof ProviderError) {
|
|
806
|
+
await this.recordProviderError("listModels", error);
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
const normalized = providerTransportError({
|
|
810
|
+
provider: this.config.name,
|
|
811
|
+
operation: "listModels",
|
|
812
|
+
cause: error
|
|
813
|
+
});
|
|
814
|
+
await this.recordProviderError("listModels", normalized);
|
|
815
|
+
throw normalized;
|
|
434
816
|
}
|
|
817
|
+
const { status, body } = response;
|
|
435
818
|
if (status !== 200) {
|
|
436
|
-
|
|
819
|
+
const httpError = providerHttpError({
|
|
820
|
+
provider: this.config.name,
|
|
821
|
+
operation: "listModels",
|
|
822
|
+
status,
|
|
823
|
+
body
|
|
824
|
+
});
|
|
825
|
+
await this.recordProviderError("listModels", httpError);
|
|
826
|
+
throw httpError;
|
|
437
827
|
}
|
|
828
|
+
return response;
|
|
829
|
+
}
|
|
830
|
+
async listModels() {
|
|
831
|
+
const body = await this.rawModelsResponse();
|
|
438
832
|
const entries = extractModelEntries(body, this.config);
|
|
439
833
|
const models = [];
|
|
440
834
|
let skipped = 0;
|
|
@@ -443,22 +837,59 @@ var OpenAICompatibleProvider = class {
|
|
|
443
837
|
skipped += 1;
|
|
444
838
|
continue;
|
|
445
839
|
}
|
|
446
|
-
models.push(
|
|
447
|
-
provider: this.config.name,
|
|
448
|
-
id: entry.id,
|
|
449
|
-
displayName: entry.id,
|
|
450
|
-
status: "available",
|
|
451
|
-
metadata: {
|
|
452
|
-
...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
|
|
453
|
-
...typeof entry.created === "number" ? { createdAt: entry.created } : {}
|
|
454
|
-
}
|
|
455
|
-
});
|
|
840
|
+
models.push(this.toModelInfo(entry));
|
|
456
841
|
}
|
|
457
842
|
if (skipped > 0) {
|
|
458
843
|
this.log.warn(`Skipped ${skipped} model entr(y/ies) without a valid id.`);
|
|
459
844
|
}
|
|
460
845
|
return models;
|
|
461
846
|
}
|
|
847
|
+
/** Maps a raw model entry onto the normalized metadata shape (§10). */
|
|
848
|
+
toModelInfo(entry) {
|
|
849
|
+
const parameters = Array.isArray(entry.supported_parameters) ? entry.supported_parameters.filter((p) => typeof p === "string") : void 0;
|
|
850
|
+
const contextWindow = typeof entry.context_length === "number" ? entry.context_length : typeof entry.context_window === "number" ? entry.context_window : void 0;
|
|
851
|
+
const inputTypes = normalizeModalities(entry.input_modalities);
|
|
852
|
+
const outputTypes = normalizeModalities(entry.output_modalities);
|
|
853
|
+
return {
|
|
854
|
+
provider: this.config.name,
|
|
855
|
+
id: entry.id,
|
|
856
|
+
displayName: entry.id,
|
|
857
|
+
...typeof entry.name === "string" ? { name: entry.name } : {},
|
|
858
|
+
status: "available",
|
|
859
|
+
...contextWindow !== void 0 ? { contextWindow } : {},
|
|
860
|
+
...inputTypes !== void 0 ? { inputTypes } : {},
|
|
861
|
+
...outputTypes !== void 0 ? { outputTypes } : {},
|
|
862
|
+
...parameters !== void 0 ? {
|
|
863
|
+
supportsTools: parameters.includes("tools") || parameters.includes("tool_choice"),
|
|
864
|
+
supportsStructuredOutput: parameters.includes("response_format"),
|
|
865
|
+
supportsStreaming: parameters.includes("stream")
|
|
866
|
+
} : {},
|
|
867
|
+
metadata: {
|
|
868
|
+
...typeof entry.owned_by === "string" ? { ownedBy: entry.owned_by } : {},
|
|
869
|
+
...typeof entry.created === "number" ? { createdAt: entry.created } : {},
|
|
870
|
+
...parameters !== void 0 ? { supportedParameters: parameters } : {}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/** Streaming where the gateway supports it (§11). */
|
|
875
|
+
stream(request, onChunk) {
|
|
876
|
+
return streamChatCompletion(this.config, request, onChunk, this.env);
|
|
877
|
+
}
|
|
878
|
+
/** Provider errors are part of the audit trail (§14 correlation, §18 observability). */
|
|
879
|
+
async recordProviderError(operation, error) {
|
|
880
|
+
await this.audit?.record({
|
|
881
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
882
|
+
action: "provider.error",
|
|
883
|
+
id: this.providerId,
|
|
884
|
+
providerId: this.providerId,
|
|
885
|
+
detail: {
|
|
886
|
+
operation,
|
|
887
|
+
code: error.code,
|
|
888
|
+
kind: error.details?.kind,
|
|
889
|
+
message: error.message
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}
|
|
462
893
|
async healthCheck() {
|
|
463
894
|
const lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
464
895
|
try {
|
|
@@ -488,27 +919,42 @@ var OpenAICompatibleProvider = class {
|
|
|
488
919
|
}
|
|
489
920
|
}
|
|
490
921
|
async chat(request) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
922
|
+
let response;
|
|
923
|
+
try {
|
|
924
|
+
response = await requestJson(`${this.config.baseUrl}/chat/completions`, {
|
|
925
|
+
method: "POST",
|
|
926
|
+
headers: authHeaders(this.config, this.env),
|
|
927
|
+
timeoutMs: this.requestTimeoutMs,
|
|
928
|
+
body: {
|
|
929
|
+
model: request.model,
|
|
930
|
+
messages: request.messages,
|
|
931
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
932
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {},
|
|
933
|
+
...request.stop !== void 0 ? { stop: request.stop } : {}
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (error instanceof ProviderError) {
|
|
938
|
+
await this.recordProviderError("chat", error);
|
|
939
|
+
throw error;
|
|
500
940
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
throw new ProviderError(
|
|
505
|
-
"MODEL_NOT_FOUND",
|
|
506
|
-
`Model "${request.model}" or the chat endpoint was not found on provider "${this.config.name}" (HTTP 404).`,
|
|
507
|
-
{ details: { status } }
|
|
508
|
-
);
|
|
941
|
+
const normalized = providerTransportError({ provider: this.config.name, operation: "chat", cause: error });
|
|
942
|
+
await this.recordProviderError("chat", normalized);
|
|
943
|
+
throw normalized;
|
|
509
944
|
}
|
|
945
|
+
const { status, body } = response;
|
|
510
946
|
if (status !== 200) {
|
|
511
|
-
|
|
947
|
+
const httpError = providerHttpError({
|
|
948
|
+
provider: this.config.name,
|
|
949
|
+
operation: "chat",
|
|
950
|
+
status,
|
|
951
|
+
body,
|
|
952
|
+
...status === 404 ? {
|
|
953
|
+
code: "MODEL_NOT_FOUND"
|
|
954
|
+
} : {}
|
|
955
|
+
});
|
|
956
|
+
await this.recordProviderError("chat", httpError);
|
|
957
|
+
throw httpError;
|
|
512
958
|
}
|
|
513
959
|
const payload = body;
|
|
514
960
|
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
@@ -530,6 +976,81 @@ var OpenAICompatibleProvider = class {
|
|
|
530
976
|
};
|
|
531
977
|
}
|
|
532
978
|
};
|
|
979
|
+
async function streamChatCompletion(config, request, onChunk, env = process.env) {
|
|
980
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
981
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
982
|
+
let response;
|
|
983
|
+
try {
|
|
984
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
985
|
+
method: "POST",
|
|
986
|
+
headers: {
|
|
987
|
+
accept: "text/event-stream",
|
|
988
|
+
"content-type": "application/json",
|
|
989
|
+
...authHeaders(config, env)
|
|
990
|
+
},
|
|
991
|
+
body: JSON.stringify({
|
|
992
|
+
model: request.model,
|
|
993
|
+
messages: request.messages,
|
|
994
|
+
stream: true,
|
|
995
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {},
|
|
996
|
+
...request.maxTokens !== void 0 ? { max_tokens: request.maxTokens } : {}
|
|
997
|
+
}),
|
|
998
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
999
|
+
});
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
throw providerTransportError({ provider: config.name, operation: "stream", cause: error });
|
|
1002
|
+
}
|
|
1003
|
+
if (!response.ok) {
|
|
1004
|
+
const body = await response.json().catch(() => void 0);
|
|
1005
|
+
throw providerHttpError({ provider: config.name, operation: "stream", status: response.status, body });
|
|
1006
|
+
}
|
|
1007
|
+
if (!response.body) {
|
|
1008
|
+
throw providerHttpError({
|
|
1009
|
+
provider: config.name,
|
|
1010
|
+
operation: "stream",
|
|
1011
|
+
status: 500,
|
|
1012
|
+
body: { error: { message: "streaming is not supported by this endpoint" } }
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
let content = "";
|
|
1016
|
+
let model = request.model;
|
|
1017
|
+
let finishReason;
|
|
1018
|
+
const decoder = new TextDecoder();
|
|
1019
|
+
let buffer = "";
|
|
1020
|
+
for await (const piece of response.body) {
|
|
1021
|
+
buffer += decoder.decode(piece, { stream: true });
|
|
1022
|
+
let newline = buffer.indexOf("\n");
|
|
1023
|
+
while (newline >= 0) {
|
|
1024
|
+
const line = buffer.slice(0, newline).trim();
|
|
1025
|
+
buffer = buffer.slice(newline + 1);
|
|
1026
|
+
newline = buffer.indexOf("\n");
|
|
1027
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
1028
|
+
const data = line.startsWith("data:") ? line.slice(5).trim() : line;
|
|
1029
|
+
if (data === "[DONE]") continue;
|
|
1030
|
+
let event;
|
|
1031
|
+
try {
|
|
1032
|
+
event = JSON.parse(data);
|
|
1033
|
+
} catch {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
if (typeof event.model === "string") model = event.model;
|
|
1037
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
1038
|
+
const first = choices[0];
|
|
1039
|
+
const delta = typeof first?.delta?.content === "string" ? first.delta.content : "";
|
|
1040
|
+
if (delta.length > 0) content += delta;
|
|
1041
|
+
if (typeof first?.finish_reason === "string") finishReason = first.finish_reason;
|
|
1042
|
+
onChunk({ delta, ...finishReason !== void 0 ? { finishReason } : {}, raw: event });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return { model, content, ...finishReason !== void 0 ? { finishReason } : {} };
|
|
1046
|
+
}
|
|
1047
|
+
function normalizeModalities(value) {
|
|
1048
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : void 0;
|
|
1049
|
+
if (!list) return void 0;
|
|
1050
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image", "audio", "video", "embedding"]);
|
|
1051
|
+
const modalities = list.filter((m) => typeof m === "string" && allowed.has(m));
|
|
1052
|
+
return modalities.length > 0 ? modalities : void 0;
|
|
1053
|
+
}
|
|
533
1054
|
function normalizeBaseUrl(baseUrl) {
|
|
534
1055
|
return baseUrl.replace(/\/+$/, "");
|
|
535
1056
|
}
|
|
@@ -544,14 +1065,6 @@ function extractModelEntries(body, config) {
|
|
|
544
1065
|
{ details: { endpoint: "models" } }
|
|
545
1066
|
);
|
|
546
1067
|
}
|
|
547
|
-
function authFailed(config, status) {
|
|
548
|
-
const envHint = config.apiKeyEnvVar ? ` Check that $${config.apiKeyEnvVar} is set and valid.` : " The provider requires authentication; configure api_key_env_var for it.";
|
|
549
|
-
return new ProviderError(
|
|
550
|
-
"PROVIDER_AUTH_FAILED",
|
|
551
|
-
`Authentication with provider "${config.name}" failed (HTTP ${status}).${envHint}`,
|
|
552
|
-
{ details: { status } }
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
1068
|
function unavailable(config, message, status) {
|
|
556
1069
|
return new ProviderError("PROVIDER_UNAVAILABLE", `Provider "${config.name}": ${message}`, {
|
|
557
1070
|
details: { status }
|
|
@@ -564,11 +1077,33 @@ function inferHealth(error) {
|
|
|
564
1077
|
|
|
565
1078
|
// src/providers/omnihilbras/index.ts
|
|
566
1079
|
var OmniHilbrasProvider = class extends OpenAICompatibleProvider {
|
|
1080
|
+
gateway;
|
|
1081
|
+
async listModels() {
|
|
1082
|
+
const raw = await this.rawModelsResponse();
|
|
1083
|
+
await this.extractGatewayMetadata(raw);
|
|
1084
|
+
const models = await super.listModels();
|
|
1085
|
+
return this.gateway ? models.map((model) => ({ ...model, metadata: { ...model.metadata, gateway: this.gateway } })) : models;
|
|
1086
|
+
}
|
|
567
1087
|
/**
|
|
568
1088
|
* Run the full connect flow. Throws on authentication or transport failure
|
|
569
1089
|
* (use healthCheck() for a non-throwing probe).
|
|
570
1090
|
*/
|
|
571
|
-
|
|
1091
|
+
/**
|
|
1092
|
+
* Gateway metadata reported alongside the model list (gateway version, tier,
|
|
1093
|
+
* region…), kept separate from per-model metadata (§11 — provider metadata).
|
|
1094
|
+
*/
|
|
1095
|
+
gatewayMetadata() {
|
|
1096
|
+
return this.gateway;
|
|
1097
|
+
}
|
|
1098
|
+
async extractGatewayMetadata(body) {
|
|
1099
|
+
if (body !== null && typeof body === "object") {
|
|
1100
|
+
const gateway = body.gateway;
|
|
1101
|
+
if (gateway !== null && typeof gateway === "object") {
|
|
1102
|
+
this.gateway = gateway;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async connectAndRegister(registry) {
|
|
572
1107
|
const apiKey = resolveApiKey(this.config);
|
|
573
1108
|
const discovered = await this.listModels();
|
|
574
1109
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -612,13 +1147,32 @@ var OPENAI_COMPATIBLE_TYPES = [
|
|
|
612
1147
|
"local",
|
|
613
1148
|
"custom"
|
|
614
1149
|
];
|
|
615
|
-
function
|
|
1150
|
+
function assertProviderConfig(config) {
|
|
1151
|
+
if (!config.name || config.name.trim().length === 0) {
|
|
1152
|
+
throw new ConfigError("CONFIG_INVALID", "Provider name must not be empty.");
|
|
1153
|
+
}
|
|
1154
|
+
if (!config.baseUrl || config.baseUrl.trim().length === 0) {
|
|
1155
|
+
throw new ConfigError("CONFIG_INVALID", `Provider "${config.name}" needs a baseUrl.`);
|
|
1156
|
+
}
|
|
1157
|
+
try {
|
|
1158
|
+
new URL(config.baseUrl);
|
|
1159
|
+
} catch {
|
|
1160
|
+
throw new ConfigError("CONFIG_INVALID", `Provider "${config.name}" has an invalid baseUrl: ${config.baseUrl}`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function createProvider(config, options = {}) {
|
|
1164
|
+
assertProviderConfig(config);
|
|
1165
|
+
const shared = {
|
|
1166
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
1167
|
+
...options.audit !== void 0 ? { audit: options.audit } : {}
|
|
1168
|
+
};
|
|
616
1169
|
if (config.type === "omnihilbras") {
|
|
617
|
-
return new OmniHilbrasProvider(config);
|
|
1170
|
+
return new OmniHilbrasProvider(config, void 0, options.env, options.audit);
|
|
618
1171
|
}
|
|
619
1172
|
if (OPENAI_COMPATIBLE_TYPES.includes(config.type)) {
|
|
620
|
-
return new OpenAICompatibleProvider(config);
|
|
1173
|
+
return new OpenAICompatibleProvider(config, void 0, options.env, options.audit);
|
|
621
1174
|
}
|
|
1175
|
+
void shared;
|
|
622
1176
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Provider type "${config.type}" has no adapter yet.`);
|
|
623
1177
|
}
|
|
624
1178
|
|
|
@@ -672,20 +1226,420 @@ var ModelRegistry = class {
|
|
|
672
1226
|
};
|
|
673
1227
|
|
|
674
1228
|
// src/agents/process/index.ts
|
|
675
|
-
import { spawn } from "child_process";
|
|
1229
|
+
import { spawn } from "child_process";
|
|
1230
|
+
|
|
1231
|
+
// src/agent-protocol/version.ts
|
|
1232
|
+
var PROTOCOL_V2 = "omninode-agent-protocol/2";
|
|
1233
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
1234
|
+
"omninode-agent-protocol/1",
|
|
1235
|
+
PROTOCOL_V2
|
|
1236
|
+
];
|
|
1237
|
+
var MAX_MESSAGE_BYTES = 1048576;
|
|
1238
|
+
function isSupportedProtocol(protocol) {
|
|
1239
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/agent-protocol/types.ts
|
|
1243
|
+
function newMessageId(prefix = "msg") {
|
|
1244
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
1245
|
+
return `${prefix}-${Date.now().toString(36)}-${random}`;
|
|
1246
|
+
}
|
|
1247
|
+
function envelope(type, payload, ids = {}) {
|
|
1248
|
+
return {
|
|
1249
|
+
protocol: PROTOCOL_V2,
|
|
1250
|
+
messageId: newMessageId(),
|
|
1251
|
+
...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
|
|
1252
|
+
...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
|
|
1253
|
+
...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
|
|
1254
|
+
...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
|
|
1255
|
+
...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
|
|
1256
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1257
|
+
type,
|
|
1258
|
+
payload,
|
|
1259
|
+
...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/agent-protocol/codec.ts
|
|
1264
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
1265
|
+
// node → agent
|
|
1266
|
+
"TASK",
|
|
1267
|
+
"HELLO_ACK",
|
|
1268
|
+
"CONTEXT",
|
|
1269
|
+
"INSTRUCTION",
|
|
1270
|
+
"RESPONSE",
|
|
1271
|
+
"CANCEL",
|
|
1272
|
+
// agent → node
|
|
1273
|
+
"HELLO",
|
|
1274
|
+
"TASK_REQUEST",
|
|
1275
|
+
"TASK_ACCEPTED",
|
|
1276
|
+
"TASK_STARTED",
|
|
1277
|
+
"STATUS",
|
|
1278
|
+
"QUESTION",
|
|
1279
|
+
"REPORT",
|
|
1280
|
+
"ARTIFACT",
|
|
1281
|
+
"COMPLETION",
|
|
1282
|
+
"ERROR",
|
|
1283
|
+
"CANCELLED"
|
|
1284
|
+
]);
|
|
1285
|
+
var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
|
|
1286
|
+
function validateEnvelope(value, line) {
|
|
1287
|
+
const sample = line?.slice(0, 200);
|
|
1288
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1289
|
+
return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
|
|
1290
|
+
}
|
|
1291
|
+
const raw = value;
|
|
1292
|
+
const protocol = raw.protocol;
|
|
1293
|
+
if (protocol === void 0) {
|
|
1294
|
+
const type = raw.type;
|
|
1295
|
+
if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
|
|
1296
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
ok: true,
|
|
1300
|
+
legacy: true,
|
|
1301
|
+
envelope: {
|
|
1302
|
+
protocol: "omninode-agent-protocol/1",
|
|
1303
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
|
|
1304
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1305
|
+
type,
|
|
1306
|
+
payload: raw.payload
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
|
|
1311
|
+
return {
|
|
1312
|
+
ok: false,
|
|
1313
|
+
error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
if (raw.type === void 0) {
|
|
1317
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
|
|
1318
|
+
}
|
|
1319
|
+
if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
|
|
1320
|
+
return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
|
|
1321
|
+
}
|
|
1322
|
+
if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
|
|
1323
|
+
return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
|
|
1324
|
+
}
|
|
1325
|
+
if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
|
|
1326
|
+
return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
ok: true,
|
|
1330
|
+
legacy: protocol !== PROTOCOL_V2,
|
|
1331
|
+
envelope: {
|
|
1332
|
+
protocol,
|
|
1333
|
+
messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
|
|
1334
|
+
...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
|
|
1335
|
+
...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
|
|
1336
|
+
...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
|
|
1337
|
+
...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
|
|
1338
|
+
...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
|
|
1339
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1340
|
+
type: raw.type,
|
|
1341
|
+
payload: raw.payload,
|
|
1342
|
+
...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1347
|
+
const trimmed = line.trim();
|
|
1348
|
+
if (trimmed.length === 0) {
|
|
1349
|
+
return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
|
|
1350
|
+
}
|
|
1351
|
+
if (trimmed.length > maxBytes) {
|
|
1352
|
+
return {
|
|
1353
|
+
ok: false,
|
|
1354
|
+
error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
let value;
|
|
1358
|
+
try {
|
|
1359
|
+
value = JSON.parse(trimmed);
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
return {
|
|
1362
|
+
ok: false,
|
|
1363
|
+
error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
return validateEnvelope(value, trimmed);
|
|
1367
|
+
}
|
|
1368
|
+
function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
|
|
1369
|
+
const line = JSON.stringify(envelope2);
|
|
1370
|
+
if (line.length > maxBytes) {
|
|
1371
|
+
throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
|
|
1372
|
+
}
|
|
1373
|
+
return line;
|
|
1374
|
+
}
|
|
1375
|
+
var ProtocolDecoder = class {
|
|
1376
|
+
buffer = "";
|
|
1377
|
+
maxBytes;
|
|
1378
|
+
constructor(maxBytes = MAX_MESSAGE_BYTES) {
|
|
1379
|
+
this.maxBytes = maxBytes;
|
|
1380
|
+
}
|
|
1381
|
+
push(chunk) {
|
|
1382
|
+
this.buffer += chunk;
|
|
1383
|
+
const results = [];
|
|
1384
|
+
let index = this.buffer.indexOf("\n");
|
|
1385
|
+
while (index >= 0) {
|
|
1386
|
+
const line = this.buffer.slice(0, index);
|
|
1387
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
1388
|
+
if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
|
|
1389
|
+
index = this.buffer.indexOf("\n");
|
|
1390
|
+
}
|
|
1391
|
+
if (this.buffer.length > this.maxBytes) {
|
|
1392
|
+
results.push({
|
|
1393
|
+
ok: false,
|
|
1394
|
+
error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
|
|
1395
|
+
});
|
|
1396
|
+
this.buffer = "";
|
|
1397
|
+
}
|
|
1398
|
+
return results;
|
|
1399
|
+
}
|
|
1400
|
+
/** Flush any trailing line when the process exits. */
|
|
1401
|
+
flush() {
|
|
1402
|
+
if (this.buffer.trim().length === 0) {
|
|
1403
|
+
this.buffer = "";
|
|
1404
|
+
return [];
|
|
1405
|
+
}
|
|
1406
|
+
const result = decodeMessage(this.buffer, this.maxBytes);
|
|
1407
|
+
this.buffer = "";
|
|
1408
|
+
return [result];
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// src/agent-protocol/handshake.ts
|
|
1413
|
+
function parseHello(envelopeValue) {
|
|
1414
|
+
if (envelopeValue.type !== "HELLO") {
|
|
1415
|
+
return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
|
|
1416
|
+
}
|
|
1417
|
+
const payload = envelopeValue.payload;
|
|
1418
|
+
if (payload === null || typeof payload !== "object") {
|
|
1419
|
+
return { ok: false, reason: "HELLO payload must be an object" };
|
|
1420
|
+
}
|
|
1421
|
+
const p = payload;
|
|
1422
|
+
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
1423
|
+
return { ok: false, reason: "HELLO payload is missing 'name'" };
|
|
1424
|
+
}
|
|
1425
|
+
return {
|
|
1426
|
+
ok: true,
|
|
1427
|
+
descriptor: {
|
|
1428
|
+
protocol: envelopeValue.protocol,
|
|
1429
|
+
name: p.name,
|
|
1430
|
+
...typeof p.version === "string" ? { version: p.version } : {},
|
|
1431
|
+
capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
|
|
1432
|
+
supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function buildHello(descriptor) {
|
|
1437
|
+
return envelope("HELLO", descriptor, { agentId: descriptor.name });
|
|
1438
|
+
}
|
|
1439
|
+
function buildHelloAck(descriptor) {
|
|
1440
|
+
return envelope("HELLO_ACK", {
|
|
1441
|
+
ok: true,
|
|
1442
|
+
protocol: PROTOCOL_V2,
|
|
1443
|
+
agent: descriptor.name,
|
|
1444
|
+
...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
|
|
1445
|
+
supportedProtocols: [PROTOCOL_V2]
|
|
1446
|
+
}, { agentId: descriptor.name });
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/agent-protocol/session.ts
|
|
1450
|
+
var ProtocolSession = class {
|
|
1451
|
+
decoder;
|
|
1452
|
+
ids;
|
|
1453
|
+
responder;
|
|
1454
|
+
violations = [];
|
|
1455
|
+
questions = [];
|
|
1456
|
+
artifacts = [];
|
|
1457
|
+
reports = [];
|
|
1458
|
+
descriptor;
|
|
1459
|
+
completionSummary;
|
|
1460
|
+
errorMessage;
|
|
1461
|
+
constructor(ids, options = {}) {
|
|
1462
|
+
this.ids = ids;
|
|
1463
|
+
this.responder = options.responder;
|
|
1464
|
+
this.decoder = new ProtocolDecoder(options.maxBytes);
|
|
1465
|
+
}
|
|
1466
|
+
/** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
|
|
1467
|
+
taskMessages(objective, role, context, instruction) {
|
|
1468
|
+
const ids = this.ids;
|
|
1469
|
+
const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
|
|
1470
|
+
if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
|
|
1471
|
+
if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
|
|
1472
|
+
if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
|
|
1473
|
+
return messages.map((m) => encodeMessage(m));
|
|
1474
|
+
}
|
|
1475
|
+
helloAck(descriptor) {
|
|
1476
|
+
return encodeMessage(buildHelloAck(descriptor));
|
|
1477
|
+
}
|
|
1478
|
+
/** Consumes decoded stdout, answering questions as they arrive. */
|
|
1479
|
+
async handle(decoded) {
|
|
1480
|
+
if (!decoded.ok) {
|
|
1481
|
+
this.violations.push(decoded.error);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const message = decoded.envelope;
|
|
1485
|
+
if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
|
|
1486
|
+
if (message.type === "HELLO") {
|
|
1487
|
+
const hello = parseHello(message);
|
|
1488
|
+
if (hello.ok) this.descriptor = hello.descriptor;
|
|
1489
|
+
else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (message.type === "QUESTION") {
|
|
1493
|
+
await this.answerQuestion(message);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (message.type === "REPORT") {
|
|
1497
|
+
this.reports.push(message.payload);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (message.type === "ARTIFACT") {
|
|
1501
|
+
this.artifacts.push(message.payload);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (message.type === "COMPLETION") {
|
|
1505
|
+
const payload = message.payload;
|
|
1506
|
+
this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
if (message.type === "ERROR") {
|
|
1510
|
+
const payload = message.payload;
|
|
1511
|
+
const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
|
|
1512
|
+
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
1513
|
+
this.errorMessage = `Agent error${code}: ${detail}`;
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.violations.push({
|
|
1520
|
+
code: "UNKNOWN_TYPE",
|
|
1521
|
+
message: `unexpected message type ${message.type} from agent`
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async answerQuestion(message) {
|
|
1525
|
+
const payload = message.payload ?? {};
|
|
1526
|
+
const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
|
|
1527
|
+
const question = typeof payload.question === "string" ? payload.question : "";
|
|
1528
|
+
const expected = typeof payload.expected === "string" ? payload.expected : "text";
|
|
1529
|
+
let answer;
|
|
1530
|
+
if (this.responder) {
|
|
1531
|
+
try {
|
|
1532
|
+
answer = await this.responder({ questionId, question, expected });
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
this.violations.push({
|
|
1535
|
+
code: "INVALID_PAYLOAD",
|
|
1536
|
+
message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
|
|
1541
|
+
this.pendingAnswers.push(
|
|
1542
|
+
encodeMessage(
|
|
1543
|
+
envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
|
|
1544
|
+
...this.ids,
|
|
1545
|
+
requestId: message.requestId ?? this.ids.requestId
|
|
1546
|
+
})
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
pendingAnswers = [];
|
|
1551
|
+
/** True once any inbound message carried the v2 protocol field. */
|
|
1552
|
+
sawV2 = false;
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
// src/agents/cwd.ts
|
|
1556
|
+
import { statSync } from "fs";
|
|
1557
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1558
|
+
function validateWorkingDirectory(cwd, options = {}) {
|
|
1559
|
+
if (!cwd) return void 0;
|
|
1560
|
+
const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
|
|
1561
|
+
let stats;
|
|
1562
|
+
try {
|
|
1563
|
+
stats = statSync(normalized);
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
throw new AgentError(
|
|
1566
|
+
"AGENT_NOT_FOUND",
|
|
1567
|
+
`Working directory "${normalized}" does not exist or is not accessible.`,
|
|
1568
|
+
{ cause: error, details: { cwd: normalized } }
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (!stats.isDirectory()) {
|
|
1572
|
+
throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
|
|
1573
|
+
details: { cwd: normalized }
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
try {
|
|
1577
|
+
statSync(resolve(normalized, "."));
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
throw new AgentError(
|
|
1580
|
+
"AGENT_NOT_FOUND",
|
|
1581
|
+
`Working directory "${normalized}" is not accessible.`,
|
|
1582
|
+
{ cause: error }
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
1586
|
+
const rel = relative(projectRoot, normalized);
|
|
1587
|
+
const outsideProject = rel.startsWith("..") || isAbsolute(rel);
|
|
1588
|
+
if (outsideProject && options.allowExternal !== true) {
|
|
1589
|
+
throw new AgentError(
|
|
1590
|
+
"AGENT_NOT_FOUND",
|
|
1591
|
+
`Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
|
|
1592
|
+
{ details: { cwd: normalized, projectRoot } }
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
return { cwd: normalized, outsideProject };
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/agents/env.ts
|
|
1599
|
+
var BASELINE_KEYS = ["PATH", "HOME"];
|
|
1600
|
+
function resolveEnvPolicy(config) {
|
|
1601
|
+
if (config.envPolicy !== void 0) return config.envPolicy;
|
|
1602
|
+
if (config.inheritEnv === false) return "explicit";
|
|
1603
|
+
return "inherit";
|
|
1604
|
+
}
|
|
1605
|
+
function buildChildEnv(config) {
|
|
1606
|
+
const configured = config.env ?? {};
|
|
1607
|
+
const policy = resolveEnvPolicy(config);
|
|
1608
|
+
if (policy === "inherit") {
|
|
1609
|
+
return { ...process.env, ...configured };
|
|
1610
|
+
}
|
|
1611
|
+
const base = {};
|
|
1612
|
+
for (const key of BASELINE_KEYS) {
|
|
1613
|
+
const value = process.env[key];
|
|
1614
|
+
if (value !== void 0) base[key] = value;
|
|
1615
|
+
}
|
|
1616
|
+
switch (policy) {
|
|
1617
|
+
case "explicit":
|
|
1618
|
+
return { ...base, ...configured };
|
|
1619
|
+
case "allowlist": {
|
|
1620
|
+
const allowed = { ...base };
|
|
1621
|
+
for (const key of config.envAllowlist ?? []) {
|
|
1622
|
+
const value = process.env[key];
|
|
1623
|
+
if (value !== void 0) allowed[key] = value;
|
|
1624
|
+
}
|
|
1625
|
+
return { ...allowed, ...configured };
|
|
1626
|
+
}
|
|
1627
|
+
case "denylist": {
|
|
1628
|
+
const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
|
|
1629
|
+
const filtered = {};
|
|
1630
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1631
|
+
if (denied.has(key.toUpperCase())) continue;
|
|
1632
|
+
filtered[key] = value;
|
|
1633
|
+
}
|
|
1634
|
+
return { ...filtered, ...configured };
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/agents/process/index.ts
|
|
676
1640
|
var DEFAULT_TIMEOUT_MS2 = 6e5;
|
|
677
1641
|
var KILL_GRACE_MS = 5e3;
|
|
678
|
-
|
|
679
|
-
const configured = config.env ?? {};
|
|
680
|
-
if (config.inheritEnv !== false) {
|
|
681
|
-
return { ...process.env, ...configured };
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
684
|
-
PATH: process.env.PATH ?? "",
|
|
685
|
-
HOME: process.env.HOME ?? "",
|
|
686
|
-
...configured
|
|
687
|
-
};
|
|
688
|
-
}
|
|
1642
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
|
|
689
1643
|
function composeTaskText(input) {
|
|
690
1644
|
const sections = [`# Objective
|
|
691
1645
|
${input.objective}`];
|
|
@@ -706,18 +1660,19 @@ ${input.instruction}`);
|
|
|
706
1660
|
return sections.join("\n\n");
|
|
707
1661
|
}
|
|
708
1662
|
function buildProtocolMessages(input, agentName) {
|
|
709
|
-
const
|
|
1663
|
+
const session = new ProtocolSession({
|
|
1664
|
+
requestId: `req-${input.taskId}`,
|
|
710
1665
|
taskId: input.taskId,
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1666
|
+
agentId: agentName,
|
|
1667
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1668
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1669
|
+
});
|
|
1670
|
+
return session.taskMessages(
|
|
1671
|
+
input.objective,
|
|
1672
|
+
input.role,
|
|
1673
|
+
input.context,
|
|
1674
|
+
input.instruction
|
|
1675
|
+
);
|
|
721
1676
|
}
|
|
722
1677
|
var ProcessAgent = class {
|
|
723
1678
|
config;
|
|
@@ -746,40 +1701,76 @@ var ProcessAgent = class {
|
|
|
746
1701
|
}
|
|
747
1702
|
const mode = this.config.inputMode ?? "stdin";
|
|
748
1703
|
const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
749
|
-
return new Promise((
|
|
1704
|
+
return new Promise((resolve2, reject) => {
|
|
1705
|
+
let cwd;
|
|
1706
|
+
try {
|
|
1707
|
+
cwd = validateWorkingDirectory(this.config.cwd, {
|
|
1708
|
+
allowExternal: this.config.allowExternalCwd
|
|
1709
|
+
})?.cwd;
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
reject(error);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
750
1714
|
const child = spawn(
|
|
751
1715
|
command,
|
|
752
1716
|
[...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
|
|
753
1717
|
{
|
|
754
|
-
cwd:
|
|
1718
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
755
1719
|
env: buildChildEnv(this.config),
|
|
756
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1720
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1721
|
+
// Own process group so the whole tree can be terminated together
|
|
1722
|
+
// (roadmap §8: no orphaned children).
|
|
1723
|
+
...process.platform === "win32" ? {} : { detached: true }
|
|
757
1724
|
}
|
|
758
1725
|
);
|
|
759
1726
|
this.running.set(input.taskId, child);
|
|
1727
|
+
GLOBAL_RUNNING.add(child);
|
|
1728
|
+
installSignalForwarding();
|
|
760
1729
|
let stdout = "";
|
|
761
1730
|
let stderr = "";
|
|
762
1731
|
let timedOut = false;
|
|
1732
|
+
let outputLimitExceeded = false;
|
|
763
1733
|
let settled = false;
|
|
1734
|
+
const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
764
1735
|
const timer = setTimeout(() => {
|
|
765
1736
|
timedOut = true;
|
|
766
|
-
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
|
|
767
|
-
child
|
|
768
|
-
setTimeout(() => child
|
|
1737
|
+
this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
|
|
1738
|
+
killTree(child, "SIGTERM");
|
|
1739
|
+
setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
|
|
769
1740
|
}, timeoutMs);
|
|
770
1741
|
const settle = (finish) => {
|
|
771
1742
|
if (settled) return;
|
|
772
1743
|
settled = true;
|
|
773
1744
|
clearTimeout(timer);
|
|
774
1745
|
this.running.delete(input.taskId);
|
|
1746
|
+
GLOBAL_RUNNING.delete(child);
|
|
1747
|
+
child.stdin?.destroy();
|
|
1748
|
+
child.stdout?.destroy();
|
|
1749
|
+
child.stderr?.destroy();
|
|
775
1750
|
finish();
|
|
776
1751
|
};
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1752
|
+
const collect = (into, chunk) => {
|
|
1753
|
+
if (outputLimitExceeded) return;
|
|
1754
|
+
const text = sanitizeChunk(chunk);
|
|
1755
|
+
if (into === "stdout") {
|
|
1756
|
+
if (stdout.length + text.length > maxOutputBytes) {
|
|
1757
|
+
stdout = (stdout + text).slice(0, maxOutputBytes);
|
|
1758
|
+
outputLimitExceeded = true;
|
|
1759
|
+
this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
|
|
1760
|
+
killTree(child, "SIGKILL");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
stdout += text;
|
|
1764
|
+
} else {
|
|
1765
|
+
if (stderr.length + text.length > maxOutputBytes) {
|
|
1766
|
+
stderr = (stderr + text).slice(0, maxOutputBytes);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
stderr += text;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
child.stdout?.on("data", (chunk) => collect("stdout", chunk));
|
|
1773
|
+
child.stderr?.on("data", (chunk) => collect("stderr", chunk));
|
|
783
1774
|
child.stdin?.on("error", () => {
|
|
784
1775
|
});
|
|
785
1776
|
if (mode !== "arg") {
|
|
@@ -833,7 +1824,7 @@ var ProcessAgent = class {
|
|
|
833
1824
|
const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
|
|
834
1825
|
if (mode === "protocol") {
|
|
835
1826
|
try {
|
|
836
|
-
|
|
1827
|
+
resolve2(
|
|
837
1828
|
interpretProtocolRun({
|
|
838
1829
|
input,
|
|
839
1830
|
agentName: this.config.name,
|
|
@@ -848,14 +1839,14 @@ var ProcessAgent = class {
|
|
|
848
1839
|
return;
|
|
849
1840
|
}
|
|
850
1841
|
const trimmed = stdout.trim();
|
|
851
|
-
|
|
1842
|
+
resolve2({
|
|
852
1843
|
taskId: input.taskId,
|
|
853
|
-
status: code === 0 ? "completed" : "failed",
|
|
1844
|
+
status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
|
|
854
1845
|
summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
|
|
855
1846
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
856
1847
|
exitCode: code ?? void 0,
|
|
857
1848
|
logs,
|
|
858
|
-
error:
|
|
1849
|
+
...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
|
|
859
1850
|
});
|
|
860
1851
|
});
|
|
861
1852
|
});
|
|
@@ -864,67 +1855,117 @@ var ProcessAgent = class {
|
|
|
864
1855
|
/** Ask the agent to stop current work (SIGTERM). */
|
|
865
1856
|
async cancel(taskId) {
|
|
866
1857
|
const child = this.running.get(taskId);
|
|
867
|
-
if (child) child
|
|
1858
|
+
if (child) killTree(child, "SIGTERM");
|
|
868
1859
|
}
|
|
869
1860
|
runningTaskIds() {
|
|
870
1861
|
return [...this.running.keys()];
|
|
871
1862
|
}
|
|
872
1863
|
};
|
|
873
|
-
function interpretProtocolRun({
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
|
|
894
|
-
error = `Agent error${code}: ${detail}`;
|
|
895
|
-
}
|
|
1864
|
+
function interpretProtocolRun({
|
|
1865
|
+
input,
|
|
1866
|
+
agentName,
|
|
1867
|
+
stdout,
|
|
1868
|
+
exitCode,
|
|
1869
|
+
logs,
|
|
1870
|
+
responder
|
|
1871
|
+
}) {
|
|
1872
|
+
const session = new ProtocolSession(
|
|
1873
|
+
{
|
|
1874
|
+
requestId: `req-${input.taskId}`,
|
|
1875
|
+
taskId: input.taskId,
|
|
1876
|
+
agentId: agentName,
|
|
1877
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1878
|
+
...input.executionId !== void 0 ? { executionId: input.executionId } : {}
|
|
1879
|
+
},
|
|
1880
|
+
{ ...responder !== void 0 ? { responder } : {} }
|
|
1881
|
+
);
|
|
1882
|
+
for (const result of session.decoder.push(stdout)) {
|
|
1883
|
+
session.handle(result);
|
|
896
1884
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
`Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
|
|
900
|
-
{ details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
|
|
901
|
-
);
|
|
1885
|
+
for (const result of session.decoder.flush()) {
|
|
1886
|
+
session.handle(result);
|
|
902
1887
|
}
|
|
1888
|
+
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);
|
|
903
1889
|
const trimmed = stdout.trim();
|
|
1890
|
+
const protocolBlock = {
|
|
1891
|
+
protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
|
|
1892
|
+
legacy: !session.sawV2 && session.descriptor === void 0,
|
|
1893
|
+
...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
|
|
1894
|
+
questions: session.questions,
|
|
1895
|
+
violations: session.violations.map((v) => `${v.code}: ${v.message}`)
|
|
1896
|
+
};
|
|
1897
|
+
if (session.errorMessage !== void 0) {
|
|
1898
|
+
return {
|
|
1899
|
+
taskId: input.taskId,
|
|
1900
|
+
status: "failed",
|
|
1901
|
+
error: session.errorMessage,
|
|
1902
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1903
|
+
exitCode: exitCode ?? void 0,
|
|
1904
|
+
logs,
|
|
1905
|
+
protocol: protocolBlock,
|
|
1906
|
+
...reports.length > 0 ? { reports } : {}
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
|
|
1910
|
+
return {
|
|
1911
|
+
taskId: input.taskId,
|
|
1912
|
+
status: "failed",
|
|
1913
|
+
error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
|
|
1914
|
+
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
1915
|
+
exitCode: exitCode ?? void 0,
|
|
1916
|
+
logs,
|
|
1917
|
+
protocol: protocolBlock
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
const completed = exitCode === 0 && session.completionSummary !== void 0;
|
|
904
1921
|
return {
|
|
905
1922
|
taskId: input.taskId,
|
|
906
|
-
status:
|
|
907
|
-
summary:
|
|
1923
|
+
status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
|
|
1924
|
+
summary: session.completionSummary ?? reports[0]?.summary,
|
|
908
1925
|
rawOutput: trimmed.length > 0 ? trimmed : void 0,
|
|
909
1926
|
exitCode: exitCode ?? void 0,
|
|
910
1927
|
logs,
|
|
911
|
-
|
|
912
|
-
|
|
1928
|
+
protocol: protocolBlock,
|
|
1929
|
+
...reports.length > 0 ? { reports } : {},
|
|
1930
|
+
...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
|
|
913
1931
|
};
|
|
914
1932
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1933
|
+
var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
|
|
1934
|
+
var signalForwardingInstalled = false;
|
|
1935
|
+
function installSignalForwarding() {
|
|
1936
|
+
if (signalForwardingInstalled) return;
|
|
1937
|
+
signalForwardingInstalled = true;
|
|
1938
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1939
|
+
process.on(signal, () => {
|
|
1940
|
+
for (const child of GLOBAL_RUNNING) {
|
|
1941
|
+
killTree(child, "SIGTERM");
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
function killTree(child, signal) {
|
|
1947
|
+
if (child.pid === void 0) return;
|
|
1948
|
+
if (process.platform !== "win32") {
|
|
1949
|
+
try {
|
|
1950
|
+
process.kill(-child.pid, signal);
|
|
1951
|
+
return;
|
|
1952
|
+
} catch {
|
|
924
1953
|
}
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
child.kill(signal);
|
|
925
1957
|
} catch {
|
|
926
1958
|
}
|
|
927
|
-
|
|
1959
|
+
}
|
|
1960
|
+
function describeExit(code, signal) {
|
|
1961
|
+
if (signal !== null) {
|
|
1962
|
+
return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
|
|
1963
|
+
}
|
|
1964
|
+
if (code === null) return "Process terminated without an exit code.";
|
|
1965
|
+
return `Process exited with code ${code}.`;
|
|
1966
|
+
}
|
|
1967
|
+
function sanitizeChunk(chunk) {
|
|
1968
|
+
return [...chunk.toString("utf8")].filter((char) => char.charCodeAt(0) !== 0).join("");
|
|
928
1969
|
}
|
|
929
1970
|
function normalizeReport(payload, taskId, agentName, index) {
|
|
930
1971
|
if (payload === null || typeof payload !== "object") return void 0;
|
|
@@ -945,6 +1986,40 @@ function firstLine(text, maxLength = 200) {
|
|
|
945
1986
|
return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
|
|
946
1987
|
}
|
|
947
1988
|
|
|
1989
|
+
// src/agents/adapters.ts
|
|
1990
|
+
var AgentAdapterRegistry = class {
|
|
1991
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1992
|
+
register(adapter) {
|
|
1993
|
+
this.adapters.set(adapter.integration, adapter);
|
|
1994
|
+
}
|
|
1995
|
+
get(integration) {
|
|
1996
|
+
return this.adapters.get(integration);
|
|
1997
|
+
}
|
|
1998
|
+
list() {
|
|
1999
|
+
return [...this.adapters.values()];
|
|
2000
|
+
}
|
|
2001
|
+
create(config) {
|
|
2002
|
+
const adapter = this.get(config.integration);
|
|
2003
|
+
if (!adapter) {
|
|
2004
|
+
throw new AgentError(
|
|
2005
|
+
"AGENT_NOT_FOUND",
|
|
2006
|
+
`No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
return adapter.create(config);
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
var ProcessAgentAdapter = class {
|
|
2013
|
+
constructor(factory) {
|
|
2014
|
+
this.factory = factory;
|
|
2015
|
+
}
|
|
2016
|
+
factory;
|
|
2017
|
+
integration = "process";
|
|
2018
|
+
create(config) {
|
|
2019
|
+
return this.factory(config);
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
|
|
948
2023
|
// src/agents/registry.ts
|
|
949
2024
|
var AgentRegistry = class {
|
|
950
2025
|
agents = /* @__PURE__ */ new Map();
|
|
@@ -960,14 +2035,24 @@ var AgentRegistry = class {
|
|
|
960
2035
|
};
|
|
961
2036
|
|
|
962
2037
|
// src/agents/factory.ts
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
2038
|
+
var defaultAgentAdapters = new AgentAdapterRegistry();
|
|
2039
|
+
defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
|
|
2040
|
+
function assertAgentConfig(config) {
|
|
2041
|
+
if (!config.name || config.name.trim().length === 0) {
|
|
2042
|
+
throw new ConfigError("CONFIG_INVALID", "Agent name must not be empty.");
|
|
2043
|
+
}
|
|
2044
|
+
if (config.integration === "process" && (!config.command || config.command.trim().length === 0)) {
|
|
2045
|
+
throw new ConfigError("CONFIG_INVALID", `Agent "${config.name}" needs a command for the process adapter.`);
|
|
966
2046
|
}
|
|
2047
|
+
}
|
|
2048
|
+
function createAgent(config, adapters = defaultAgentAdapters) {
|
|
2049
|
+
assertAgentConfig(config);
|
|
2050
|
+
const registered = adapters.get(config.integration);
|
|
2051
|
+
if (registered) return registered.create(config);
|
|
967
2052
|
if (config.integration === "native") {
|
|
968
2053
|
throw new OmniNodeError(
|
|
969
2054
|
"NOT_IMPLEMENTED",
|
|
970
|
-
`
|
|
2055
|
+
`No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
|
|
971
2056
|
);
|
|
972
2057
|
}
|
|
973
2058
|
throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
|
|
@@ -1009,51 +2094,127 @@ function buildRoleRegistry(config) {
|
|
|
1009
2094
|
}
|
|
1010
2095
|
|
|
1011
2096
|
// src/persistence/json-file-store.ts
|
|
1012
|
-
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
2097
|
+
import { open, mkdir, readFile, rename, stat, writeFile } from "fs/promises";
|
|
1013
2098
|
import { dirname } from "path";
|
|
2099
|
+
var SCHEMA_VERSION = 2;
|
|
2100
|
+
async function writeFileAtomic(target, contents, durability = "rename") {
|
|
2101
|
+
const tmp = `${target}.tmp`;
|
|
2102
|
+
await mkdir(dirname(target), { recursive: true });
|
|
2103
|
+
if (durability === "fsync") {
|
|
2104
|
+
const handle = await open(tmp, "w");
|
|
2105
|
+
try {
|
|
2106
|
+
await handle.writeFile(contents, "utf8");
|
|
2107
|
+
await handle.sync();
|
|
2108
|
+
} finally {
|
|
2109
|
+
await handle.close();
|
|
2110
|
+
}
|
|
2111
|
+
} else {
|
|
2112
|
+
await writeFile(tmp, contents, "utf8");
|
|
2113
|
+
}
|
|
2114
|
+
await rename(tmp, target);
|
|
2115
|
+
if (durability === "fsync") {
|
|
2116
|
+
try {
|
|
2117
|
+
const dir = await open(dirname(target), "r");
|
|
2118
|
+
try {
|
|
2119
|
+
await dir.sync();
|
|
2120
|
+
} finally {
|
|
2121
|
+
await dir.close();
|
|
2122
|
+
}
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
async function inspectDocument(path2) {
|
|
2128
|
+
let raw;
|
|
2129
|
+
try {
|
|
2130
|
+
await stat(path2);
|
|
2131
|
+
} catch {
|
|
2132
|
+
return { status: "missing" };
|
|
2133
|
+
}
|
|
2134
|
+
try {
|
|
2135
|
+
raw = await readFile(path2, "utf8");
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
return { status: "corrupt", detail: `unreadable: ${error instanceof Error ? error.message : String(error)}` };
|
|
2138
|
+
}
|
|
2139
|
+
if (raw.trim().length === 0) return { status: "empty" };
|
|
2140
|
+
let parsed;
|
|
2141
|
+
try {
|
|
2142
|
+
parsed = JSON.parse(raw);
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
return { status: "corrupt", detail: error instanceof Error ? error.message : "invalid JSON" };
|
|
2145
|
+
}
|
|
2146
|
+
if (Array.isArray(parsed)) return { status: "v1-legacy" };
|
|
2147
|
+
if (parsed === null || typeof parsed !== "object") return { status: "corrupt", detail: "not an object" };
|
|
2148
|
+
const doc = parsed;
|
|
2149
|
+
if (!Array.isArray(doc.items)) return { status: "corrupt", detail: "missing items array" };
|
|
2150
|
+
const version = typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1;
|
|
2151
|
+
if (version > SCHEMA_VERSION) return { status: "future", schemaVersion: version };
|
|
2152
|
+
if (version < SCHEMA_VERSION) return { status: "v1-legacy" };
|
|
2153
|
+
return { status: "current", schemaVersion: version, count: doc.items.length };
|
|
2154
|
+
}
|
|
2155
|
+
async function readDocument(path2, onCorrupt, quarantine = true) {
|
|
2156
|
+
const state = await inspectDocument(path2);
|
|
2157
|
+
if (state.status === "missing" || state.status === "empty") return [];
|
|
2158
|
+
if (state.status === "future") {
|
|
2159
|
+
const error = new Error(
|
|
2160
|
+
`${path2} was written by a newer OmniNode (schema v${state.schemaVersion} > v${SCHEMA_VERSION})`
|
|
2161
|
+
);
|
|
2162
|
+
onCorrupt?.(path2, error);
|
|
2163
|
+
return [];
|
|
2164
|
+
}
|
|
2165
|
+
let raw;
|
|
2166
|
+
try {
|
|
2167
|
+
raw = await readFile(path2, "utf8");
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
onCorrupt?.(path2, error);
|
|
2170
|
+
return [];
|
|
2171
|
+
}
|
|
2172
|
+
try {
|
|
2173
|
+
const parsed = JSON.parse(raw);
|
|
2174
|
+
if (Array.isArray(parsed)) return parsed;
|
|
2175
|
+
return parsed.items ?? [];
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
if (quarantine) {
|
|
2178
|
+
const quarantinePath = `${path2}.corrupt-${Date.now()}`;
|
|
2179
|
+
try {
|
|
2180
|
+
await rename(path2, quarantinePath);
|
|
2181
|
+
logger.child({ component: "persistence" }).warn(
|
|
2182
|
+
`Quarantined unreadable store file ${path2} \u2192 ${quarantinePath}`
|
|
2183
|
+
);
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
onCorrupt?.(path2, error);
|
|
2188
|
+
return [];
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
1014
2191
|
var JsonFileStore = class {
|
|
1015
2192
|
filePath;
|
|
1016
2193
|
schemaVersion;
|
|
2194
|
+
durability;
|
|
1017
2195
|
onCorrupt;
|
|
2196
|
+
quarantine;
|
|
1018
2197
|
log;
|
|
1019
2198
|
queue = Promise.resolve();
|
|
1020
2199
|
constructor(options) {
|
|
1021
2200
|
this.filePath = `${options.directory ?? `${process.cwd()}/.omninode`}/${options.fileName}`;
|
|
1022
|
-
this.schemaVersion = options.schemaVersion ??
|
|
2201
|
+
this.schemaVersion = options.schemaVersion ?? SCHEMA_VERSION;
|
|
2202
|
+
this.durability = options.durability ?? "rename";
|
|
1023
2203
|
this.onCorrupt = options.onCorrupt;
|
|
2204
|
+
this.quarantine = options.quarantine ?? true;
|
|
1024
2205
|
this.log = logger.child({ component: "persistence", file: options.fileName });
|
|
1025
2206
|
}
|
|
1026
2207
|
get path() {
|
|
1027
2208
|
return this.filePath;
|
|
1028
2209
|
}
|
|
1029
|
-
/** All items, or [] when missing/corrupt. */
|
|
2210
|
+
/** All items, or [] when missing/migrated-from-v1/corrupt. */
|
|
1030
2211
|
async readAll() {
|
|
1031
|
-
|
|
1032
|
-
try {
|
|
1033
|
-
raw = await readFile(this.filePath, "utf8");
|
|
1034
|
-
} catch {
|
|
1035
|
-
return [];
|
|
1036
|
-
}
|
|
1037
|
-
try {
|
|
1038
|
-
const parsed = JSON.parse(raw);
|
|
1039
|
-
if (Array.isArray(parsed)) return parsed;
|
|
1040
|
-
if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.items)) {
|
|
1041
|
-
return parsed.items;
|
|
1042
|
-
}
|
|
1043
|
-
throw new Error("persisted file has an unrecognized shape");
|
|
1044
|
-
} catch (error) {
|
|
1045
|
-
this.log.warn(`Treating ${this.filePath} as empty \u2014 it could not be parsed.`);
|
|
1046
|
-
this.onCorrupt?.(this.filePath, error);
|
|
1047
|
-
return [];
|
|
1048
|
-
}
|
|
2212
|
+
return readDocument(this.filePath, this.onCorrupt, this.quarantine);
|
|
1049
2213
|
}
|
|
1050
2214
|
async writeAll(items) {
|
|
1051
2215
|
const document = { schemaVersion: this.schemaVersion, items };
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
await writeFile(tmp, `${JSON.stringify(document, null, 2)}
|
|
1055
|
-
`, "utf8");
|
|
1056
|
-
await rename(tmp, this.filePath);
|
|
2216
|
+
await writeFileAtomic(this.filePath, `${JSON.stringify(document, null, 2)}
|
|
2217
|
+
`, this.durability);
|
|
1057
2218
|
}
|
|
1058
2219
|
/** Serializes an async operation against this file. */
|
|
1059
2220
|
synchronized(operation) {
|
|
@@ -1150,13 +2311,14 @@ var TaskEngine = class {
|
|
|
1150
2311
|
createdAt: now,
|
|
1151
2312
|
updatedAt: now,
|
|
1152
2313
|
...input.project !== void 0 ? { project: input.project } : {},
|
|
2314
|
+
...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
|
|
1153
2315
|
...input.role !== void 0 ? { role: input.role } : {},
|
|
1154
2316
|
...input.agent !== void 0 ? { agent: input.agent } : {},
|
|
1155
2317
|
...input.context !== void 0 ? { context: input.context } : {}
|
|
1156
2318
|
};
|
|
1157
2319
|
await this.options.store.save(task);
|
|
1158
2320
|
this.log.info(`Created task ${task.id}.`);
|
|
1159
|
-
await this.audit("task.created", task
|
|
2321
|
+
await this.audit("task.created", task, { objective: task.objective });
|
|
1160
2322
|
return task;
|
|
1161
2323
|
}
|
|
1162
2324
|
async queue(id) {
|
|
@@ -1199,12 +2361,24 @@ var TaskEngine = class {
|
|
|
1199
2361
|
const executionId = `exec-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
1200
2362
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1201
2363
|
current = await this.transition(current, "running", { attempt });
|
|
2364
|
+
await this.options.audit?.record({
|
|
2365
|
+
at: startedAt,
|
|
2366
|
+
action: "agent.started",
|
|
2367
|
+
id: current.id,
|
|
2368
|
+
taskId: current.id,
|
|
2369
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2370
|
+
agentId: task.agent,
|
|
2371
|
+
executionId,
|
|
2372
|
+
detail: { attempt }
|
|
2373
|
+
});
|
|
1202
2374
|
let output;
|
|
1203
2375
|
let thrown;
|
|
1204
2376
|
try {
|
|
1205
2377
|
output = await agent.run({
|
|
1206
2378
|
taskId: current.id,
|
|
1207
2379
|
objective: current.objective,
|
|
2380
|
+
executionId,
|
|
2381
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
1208
2382
|
...role ? { role } : {},
|
|
1209
2383
|
...background !== void 0 ? { context: background } : {}
|
|
1210
2384
|
});
|
|
@@ -1212,6 +2386,16 @@ var TaskEngine = class {
|
|
|
1212
2386
|
thrown = error;
|
|
1213
2387
|
}
|
|
1214
2388
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2389
|
+
await this.options.audit?.record({
|
|
2390
|
+
at: finishedAt,
|
|
2391
|
+
action: "agent.completed",
|
|
2392
|
+
id: current.id,
|
|
2393
|
+
taskId: current.id,
|
|
2394
|
+
...current.pipelineId !== void 0 ? { pipelineId: current.pipelineId } : {},
|
|
2395
|
+
agentId: task.agent,
|
|
2396
|
+
executionId,
|
|
2397
|
+
detail: { attempt, outcome: thrown !== void 0 || output === void 0 ? "threw" : "returned" }
|
|
2398
|
+
});
|
|
1215
2399
|
if (thrown !== void 0 || output === void 0) {
|
|
1216
2400
|
const outcome = classifyThrown(thrown);
|
|
1217
2401
|
const message = thrown instanceof Error ? thrown.message : String(thrown);
|
|
@@ -1236,9 +2420,15 @@ var TaskEngine = class {
|
|
|
1236
2420
|
if (this.options.memory) await this.options.memory.recordTaskOutcome(current);
|
|
1237
2421
|
return current;
|
|
1238
2422
|
}
|
|
2423
|
+
const currentState = await this.options.store.get(current.id);
|
|
2424
|
+
if (currentState?.status === "cancelled") {
|
|
2425
|
+
current = currentState;
|
|
2426
|
+
break;
|
|
2427
|
+
}
|
|
1239
2428
|
const result = {
|
|
1240
2429
|
...output.summary !== void 0 ? { summary: output.summary } : {},
|
|
1241
2430
|
...output.reports !== void 0 && output.reports.length > 0 ? { reports: output.reports } : {},
|
|
2431
|
+
...output.protocol !== void 0 ? { protocol: output.protocol } : {},
|
|
1242
2432
|
// Keep the verbatim output (bounded) for report extraction and audit.
|
|
1243
2433
|
...output.rawOutput !== void 0 && output.rawOutput.length > 0 ? { rawOutput: output.rawOutput.slice(0, 1e4) } : {},
|
|
1244
2434
|
...output.error !== void 0 ? { error: output.error } : {},
|
|
@@ -1286,7 +2476,7 @@ var TaskEngine = class {
|
|
|
1286
2476
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1287
2477
|
};
|
|
1288
2478
|
await this.options.store.save(reset);
|
|
1289
|
-
await this.audit("task.retry",
|
|
2479
|
+
await this.audit("task.retry", reset, { from: task.status });
|
|
1290
2480
|
this.log.info(`Task ${id} reset for retry.`);
|
|
1291
2481
|
return reset;
|
|
1292
2482
|
}
|
|
@@ -1319,16 +2509,21 @@ var TaskEngine = class {
|
|
|
1319
2509
|
...extra.result !== void 0 ? { result: { ...task.result, ...extra.result } } : {}
|
|
1320
2510
|
};
|
|
1321
2511
|
await this.options.store.save(updated);
|
|
1322
|
-
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated
|
|
2512
|
+
await this.audit(`task.${to === "queued" ? "queued" : to}`, updated, {
|
|
1323
2513
|
from: task.status
|
|
1324
2514
|
});
|
|
1325
2515
|
return updated;
|
|
1326
2516
|
}
|
|
1327
|
-
async audit(action,
|
|
2517
|
+
async audit(action, task, detail, executionId) {
|
|
1328
2518
|
await this.options.audit?.record({
|
|
1329
2519
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1330
2520
|
action,
|
|
1331
|
-
id,
|
|
2521
|
+
id: task.id,
|
|
2522
|
+
taskId: task.id,
|
|
2523
|
+
...task.project !== void 0 ? { project: task.project } : {},
|
|
2524
|
+
...task.pipelineId !== void 0 ? { pipelineId: task.pipelineId } : {},
|
|
2525
|
+
...task.agent !== void 0 ? { agentId: task.agent } : {},
|
|
2526
|
+
...executionId !== void 0 ? { executionId } : {},
|
|
1332
2527
|
...detail !== void 0 ? { detail } : {}
|
|
1333
2528
|
});
|
|
1334
2529
|
}
|
|
@@ -1347,22 +2542,35 @@ function classifyThrown(error) {
|
|
|
1347
2542
|
return "failed";
|
|
1348
2543
|
}
|
|
1349
2544
|
function sleep(ms) {
|
|
1350
|
-
return new Promise((
|
|
2545
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1351
2546
|
}
|
|
1352
2547
|
|
|
1353
2548
|
// src/audit/index.ts
|
|
1354
|
-
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
2549
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, rename as rename2, stat as stat2 } from "fs/promises";
|
|
1355
2550
|
import { dirname as dirname2 } from "path";
|
|
2551
|
+
var DEFAULT_AUDIT_MAX_BYTES = 5 * 1024 * 1024;
|
|
1356
2552
|
var FileAuditLog = class {
|
|
1357
2553
|
filePath;
|
|
2554
|
+
maxBytes;
|
|
1358
2555
|
log;
|
|
1359
|
-
constructor(directory = `${process.cwd()}/.omninode`, log = logger) {
|
|
2556
|
+
constructor(directory = `${process.cwd()}/.omninode`, log = logger, maxBytes = DEFAULT_AUDIT_MAX_BYTES) {
|
|
1360
2557
|
this.filePath = `${directory}/audit.jsonl`;
|
|
2558
|
+
this.maxBytes = maxBytes;
|
|
1361
2559
|
this.log = log.child({ component: "audit" });
|
|
1362
2560
|
}
|
|
2561
|
+
/** Rotates the log once it passes the size budget, keeping one backup. */
|
|
2562
|
+
async rotateIfNeeded() {
|
|
2563
|
+
try {
|
|
2564
|
+
const stats = await stat2(this.filePath);
|
|
2565
|
+
if (stats.size <= this.maxBytes) return;
|
|
2566
|
+
await rename2(this.filePath, `${this.filePath}.1`);
|
|
2567
|
+
} catch {
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
1363
2570
|
async record(event) {
|
|
1364
2571
|
try {
|
|
1365
2572
|
await mkdir2(dirname2(this.filePath), { recursive: true });
|
|
2573
|
+
await this.rotateIfNeeded();
|
|
1366
2574
|
await appendFile(this.filePath, `${JSON.stringify(event)}
|
|
1367
2575
|
`, "utf8");
|
|
1368
2576
|
} catch (error) {
|
|
@@ -1373,7 +2581,6 @@ var FileAuditLog = class {
|
|
|
1373
2581
|
}
|
|
1374
2582
|
/** Recent events, oldest first; used by the CLI. */
|
|
1375
2583
|
async recent(limit = 20) {
|
|
1376
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
1377
2584
|
let raw;
|
|
1378
2585
|
try {
|
|
1379
2586
|
raw = await readFile2(this.filePath, "utf8");
|
|
@@ -1391,6 +2598,23 @@ var FileAuditLog = class {
|
|
|
1391
2598
|
}
|
|
1392
2599
|
};
|
|
1393
2600
|
|
|
2601
|
+
// src/memory/base.ts
|
|
2602
|
+
function defaultSearch(provider, text, options = {}) {
|
|
2603
|
+
return provider.retrieve({
|
|
2604
|
+
text,
|
|
2605
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2606
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
function defaultMetadata(name, options = {}) {
|
|
2610
|
+
return {
|
|
2611
|
+
name,
|
|
2612
|
+
capabilities: options.capabilities ?? ["retrieve", "store", "search"],
|
|
2613
|
+
relevanceRanking: options.relevanceRanking ?? false,
|
|
2614
|
+
...options.backend !== void 0 ? { backend: options.backend } : {}
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
|
|
1394
2618
|
// src/memory/local.ts
|
|
1395
2619
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1396
2620
|
"the",
|
|
@@ -1453,9 +2677,27 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1453
2677
|
constructor(directory) {
|
|
1454
2678
|
super({ ...directory !== void 0 ? { directory } : {}, fileName: "memory.json" });
|
|
1455
2679
|
}
|
|
1456
|
-
|
|
2680
|
+
metadata() {
|
|
2681
|
+
return defaultMetadata(this.name, {
|
|
2682
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2683
|
+
relevanceRanking: true,
|
|
2684
|
+
backend: { kind: "json-file", deterministicScoring: true }
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
search(text, options = {}) {
|
|
2688
|
+
return defaultSearch(this, text, options);
|
|
2689
|
+
}
|
|
2690
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2691
|
+
query(query) {
|
|
2692
|
+
return this.retrieve(query);
|
|
2693
|
+
}
|
|
2694
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2695
|
+
write(entry) {
|
|
2696
|
+
return this.store(entry);
|
|
2697
|
+
}
|
|
2698
|
+
async retrieve(query) {
|
|
1457
2699
|
const entries = await this.readAll();
|
|
1458
|
-
const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter(
|
|
2700
|
+
const filtered = entries.filter((entry) => query.scope ? entry.scope === query.scope : true).filter((entry) => query.category ? entry.category === query.category : true).filter(
|
|
1459
2701
|
(entry) => query.tags && query.tags.length > 0 ? query.tags.every((tag) => (entry.tags ?? []).includes(tag)) : true
|
|
1460
2702
|
);
|
|
1461
2703
|
if (!query.text || query.text.trim().length === 0) {
|
|
@@ -1464,7 +2706,7 @@ var LocalMemoryProvider = class extends JsonFileStore {
|
|
|
1464
2706
|
const queryKeywords = keywords(query.text);
|
|
1465
2707
|
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);
|
|
1466
2708
|
}
|
|
1467
|
-
|
|
2709
|
+
store(entry) {
|
|
1468
2710
|
return this.mutate((entries) => {
|
|
1469
2711
|
const index = entries.findIndex((e) => e.key === entry.key);
|
|
1470
2712
|
const stamped = {
|
|
@@ -1495,11 +2737,40 @@ var RememberaMemoryProvider = class {
|
|
|
1495
2737
|
this.env = config.env ?? process.env;
|
|
1496
2738
|
this.log = log.child({ component: "remembera" });
|
|
1497
2739
|
}
|
|
2740
|
+
get authenticationConfigured() {
|
|
2741
|
+
return this.apiKeyEnvVar === void 0 || Boolean(this.env[this.apiKeyEnvVar]);
|
|
2742
|
+
}
|
|
1498
2743
|
headers() {
|
|
1499
2744
|
const key = this.apiKeyEnvVar ? resolveApiKey({ name: "remembera", apiKeyEnvVar: this.apiKeyEnvVar }, this.env) : void 0;
|
|
1500
2745
|
return key ? { authorization: `Bearer ${key}` } : {};
|
|
1501
2746
|
}
|
|
1502
|
-
|
|
2747
|
+
/** Search delegates to the gateway's relevance query. */
|
|
2748
|
+
search(text, options = {}) {
|
|
2749
|
+
return defaultSearch(
|
|
2750
|
+
this,
|
|
2751
|
+
text,
|
|
2752
|
+
{
|
|
2753
|
+
...options.scope !== void 0 ? { scope: options.scope } : {},
|
|
2754
|
+
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2755
|
+
}
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
metadata() {
|
|
2759
|
+
return defaultMetadata(this.name, {
|
|
2760
|
+
capabilities: ["retrieve", "store", "search", "categories", "tags"],
|
|
2761
|
+
relevanceRanking: true,
|
|
2762
|
+
backend: { kind: "http", baseUrl: this.baseUrl, authenticated: this.authenticationConfigured }
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
/** @deprecated v1 alias of {@link retrieve}. */
|
|
2766
|
+
query(query) {
|
|
2767
|
+
return this.retrieve(query);
|
|
2768
|
+
}
|
|
2769
|
+
/** @deprecated v1 alias of {@link store}. */
|
|
2770
|
+
write(entry) {
|
|
2771
|
+
return this.store(entry);
|
|
2772
|
+
}
|
|
2773
|
+
async retrieve(query) {
|
|
1503
2774
|
const { status, body } = await this.request(`${this.baseUrl}/api/memory/query`, query);
|
|
1504
2775
|
if (status !== 200) {
|
|
1505
2776
|
throw new MemoryError(`Remembera query returned HTTP ${status}.`);
|
|
@@ -1510,7 +2781,7 @@ var RememberaMemoryProvider = class {
|
|
|
1510
2781
|
}
|
|
1511
2782
|
return entries.map((entry) => this.toEntry(entry)).filter((entry) => entry !== void 0);
|
|
1512
2783
|
}
|
|
1513
|
-
async
|
|
2784
|
+
async store(entry) {
|
|
1514
2785
|
const { status } = await this.request(`${this.baseUrl}/api/memory`, entry);
|
|
1515
2786
|
if (status !== 200 && status !== 201) {
|
|
1516
2787
|
throw new MemoryError(`Remembera write returned HTTP ${status}.`);
|
|
@@ -1545,6 +2816,7 @@ var RememberaMemoryProvider = class {
|
|
|
1545
2816
|
content: entry.content,
|
|
1546
2817
|
scope: scope === "project" || scope === "role" || scope === "task" || scope === "knowledge" ? scope : "knowledge",
|
|
1547
2818
|
...Array.isArray(entry.tags) ? { tags: entry.tags.filter((t) => typeof t === "string") } : {},
|
|
2819
|
+
...typeof entry.category === "string" ? { category: entry.category } : {},
|
|
1548
2820
|
...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
|
|
1549
2821
|
...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {}
|
|
1550
2822
|
};
|
|
@@ -1575,57 +2847,54 @@ function createMemoryProvider(config) {
|
|
|
1575
2847
|
}
|
|
1576
2848
|
|
|
1577
2849
|
// src/memory/service.ts
|
|
2850
|
+
var DEFAULT_ENTRY_LIMIT = 8;
|
|
1578
2851
|
var MAX_CONTEXT_CHARS = 4e3;
|
|
1579
2852
|
var MemoryService = class {
|
|
1580
|
-
constructor(provider, log = logger) {
|
|
2853
|
+
constructor(provider, log = logger, options = {}) {
|
|
1581
2854
|
this.provider = provider;
|
|
1582
2855
|
this.log = log.child({ component: "memory", provider: provider.name });
|
|
2856
|
+
this.required = options.required ?? false;
|
|
2857
|
+
this.maxContextChars = options.maxContextChars ?? MAX_CONTEXT_CHARS;
|
|
1583
2858
|
}
|
|
1584
2859
|
provider;
|
|
1585
2860
|
log;
|
|
2861
|
+
required;
|
|
2862
|
+
maxContextChars;
|
|
1586
2863
|
get providerRef() {
|
|
1587
2864
|
return this.provider;
|
|
1588
2865
|
}
|
|
1589
2866
|
get providerName() {
|
|
1590
2867
|
return this.provider.name;
|
|
1591
2868
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
2869
|
+
get isRequired() {
|
|
2870
|
+
return this.required;
|
|
2871
|
+
}
|
|
2872
|
+
/** Structured, budgeted context for a task (§12 — avoid loading all memory). */
|
|
2873
|
+
async contextFor(query) {
|
|
2874
|
+
let entries;
|
|
1594
2875
|
try {
|
|
1595
|
-
|
|
2876
|
+
entries = await this.provider.retrieve({
|
|
1596
2877
|
text: query.objective,
|
|
1597
2878
|
...query.role ? { tags: [query.role] } : {},
|
|
1598
|
-
limit:
|
|
2879
|
+
limit: query.limit ?? DEFAULT_ENTRY_LIMIT
|
|
1599
2880
|
});
|
|
1600
|
-
if (entries.length === 0) return "";
|
|
1601
|
-
const byScope = /* @__PURE__ */ new Map();
|
|
1602
|
-
for (const entry of entries) {
|
|
1603
|
-
byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
|
|
1604
|
-
}
|
|
1605
|
-
const parts = [];
|
|
1606
|
-
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
1607
|
-
const scopeEntries = byScope.get(scope) ?? [];
|
|
1608
|
-
if (scopeEntries.length === 0) continue;
|
|
1609
|
-
const lines = scopeEntries.map((entry) => `- ${entry.content}`).join("\n");
|
|
1610
|
-
parts.push(`### Relevant memory (${scope})
|
|
1611
|
-
${lines}`);
|
|
1612
|
-
}
|
|
1613
|
-
const context = parts.join("\n\n");
|
|
1614
|
-
return context.length > MAX_CONTEXT_CHARS ? `${context.slice(0, MAX_CONTEXT_CHARS)}\u2026` : context;
|
|
1615
2881
|
} catch (error) {
|
|
1616
|
-
this.
|
|
1617
|
-
`Memory query failed (continuing without memory): ${error instanceof Error ? error.message : String(error)}`
|
|
1618
|
-
);
|
|
1619
|
-
return "";
|
|
2882
|
+
return this.handleFailure("retrieve", error, { text: "", entries: [], truncated: false });
|
|
1620
2883
|
}
|
|
2884
|
+
return this.format(entries);
|
|
2885
|
+
}
|
|
2886
|
+
/** Prompt-ready context text (v1 convenience wrapper). */
|
|
2887
|
+
async gatherContext(query) {
|
|
2888
|
+
return (await this.contextFor(query)).text;
|
|
1621
2889
|
}
|
|
1622
|
-
/** Records a finished task's outcome —
|
|
2890
|
+
/** Records a finished task's outcome — task history plus "known problems" (§12). */
|
|
1623
2891
|
async recordTaskOutcome(task) {
|
|
1624
2892
|
const outcome = task.result?.summary ?? task.result?.error ?? "(no output)";
|
|
1625
2893
|
try {
|
|
1626
|
-
await this.provider.
|
|
2894
|
+
await this.provider.store({
|
|
1627
2895
|
key: `task:${task.id}`,
|
|
1628
2896
|
scope: "task",
|
|
2897
|
+
category: "tasks",
|
|
1629
2898
|
content: `Objective: ${task.objective}
|
|
1630
2899
|
Status: ${task.status}
|
|
1631
2900
|
Outcome: ${outcome}`,
|
|
@@ -1636,41 +2905,335 @@ Outcome: ${outcome}`,
|
|
|
1636
2905
|
(report) => report.findings.filter((finding) => finding.severity === "high" || finding.severity === "critical").map((finding) => ({ report, finding }))
|
|
1637
2906
|
);
|
|
1638
2907
|
for (const { report, finding } of significant) {
|
|
1639
|
-
await this.provider.
|
|
2908
|
+
await this.provider.store({
|
|
1640
2909
|
key: `finding:${report.id}:${finding.id}`,
|
|
1641
2910
|
scope: "project",
|
|
2911
|
+
category: "problems",
|
|
1642
2912
|
content: `[${finding.severity}] ${finding.title} \u2014 ${finding.detail}`,
|
|
1643
2913
|
tags: ["known-problem", report.agent]
|
|
1644
2914
|
});
|
|
1645
2915
|
}
|
|
1646
2916
|
} catch (error) {
|
|
1647
|
-
this.
|
|
1648
|
-
|
|
2917
|
+
this.handleFailure("store", error, void 0);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
format(entries) {
|
|
2921
|
+
if (entries.length === 0) return { text: "", entries, truncated: false };
|
|
2922
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2923
|
+
for (const entry of entries) {
|
|
2924
|
+
byScope.set(entry.scope, [...byScope.get(entry.scope) ?? [], entry]);
|
|
2925
|
+
}
|
|
2926
|
+
const parts = [];
|
|
2927
|
+
for (const scope of ["project", "task", "role", "knowledge"]) {
|
|
2928
|
+
const scopeEntries = byScope.get(scope) ?? [];
|
|
2929
|
+
if (scopeEntries.length === 0) continue;
|
|
2930
|
+
const lines = scopeEntries.map((entry) => {
|
|
2931
|
+
const category = entry.category ? `[${entry.category}] ` : "";
|
|
2932
|
+
return `- ${category}${entry.content}`;
|
|
2933
|
+
}).join("\n");
|
|
2934
|
+
parts.push(`### Relevant memory (${scope})
|
|
2935
|
+
${lines}`);
|
|
2936
|
+
}
|
|
2937
|
+
let text = parts.join("\n\n");
|
|
2938
|
+
const truncated = text.length > this.maxContextChars;
|
|
2939
|
+
if (truncated) text = `${text.slice(0, this.maxContextChars - 1)}\u2026`;
|
|
2940
|
+
return { text, entries, truncated };
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Failure policy (§12): continue unless memory is explicitly required.
|
|
2944
|
+
*/
|
|
2945
|
+
handleFailure(operation, error, fallback) {
|
|
2946
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2947
|
+
if (this.required) {
|
|
2948
|
+
throw new MemoryError(
|
|
2949
|
+
`Memory is marked required, but ${operation} failed: ${message}`,
|
|
2950
|
+
{ cause: error }
|
|
1649
2951
|
);
|
|
1650
2952
|
}
|
|
2953
|
+
this.log.warn(`Memory ${operation} failed (continuing without memory): ${message}`);
|
|
2954
|
+
return fallback ?? { text: "", entries: [], truncated: false };
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
// src/tasks/engine-factory.ts
|
|
2959
|
+
function createTaskEngine(config, log = logger, directory) {
|
|
2960
|
+
const agents = buildAgentRegistry(config, log);
|
|
2961
|
+
const roles = buildRoleRegistry(config);
|
|
2962
|
+
return new TaskEngine({
|
|
2963
|
+
agents,
|
|
2964
|
+
roles,
|
|
2965
|
+
store: new FileTaskStore(directory),
|
|
2966
|
+
...config.project.memory !== void 0 ? {
|
|
2967
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
2968
|
+
required: config.project.memory.required
|
|
2969
|
+
})
|
|
2970
|
+
} : {},
|
|
2971
|
+
audit: new FileAuditLog(),
|
|
2972
|
+
log
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// src/pipelines/engine.ts
|
|
2977
|
+
import { randomBytes as randomBytes5 } from "crypto";
|
|
2978
|
+
|
|
2979
|
+
// src/reports/aggregate.ts
|
|
2980
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2981
|
+
|
|
2982
|
+
// src/reports/schema.ts
|
|
2983
|
+
import { z as z2 } from "zod";
|
|
2984
|
+
var evidenceSchema = z2.union([
|
|
2985
|
+
z2.string().transform((excerpt) => ({ kind: "observation", excerpt })),
|
|
2986
|
+
z2.object({
|
|
2987
|
+
kind: z2.enum(["file", "line", "url", "command", "observation", "artifact"]),
|
|
2988
|
+
ref: z2.string().optional(),
|
|
2989
|
+
line: z2.number().int().optional(),
|
|
2990
|
+
excerpt: z2.string().optional(),
|
|
2991
|
+
agent: z2.string().optional()
|
|
2992
|
+
})
|
|
2993
|
+
]);
|
|
2994
|
+
var findingSchema = z2.object({
|
|
2995
|
+
id: z2.string().min(1),
|
|
2996
|
+
title: z2.string().min(1),
|
|
2997
|
+
detail: z2.string().default(""),
|
|
2998
|
+
description: z2.string().optional(),
|
|
2999
|
+
severity: z2.enum(["info", "low", "medium", "high", "critical"]).optional(),
|
|
3000
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
3001
|
+
severityBySource: z2.record(z2.enum(["info", "low", "medium", "high", "critical"])).optional(),
|
|
3002
|
+
confidenceBySource: z2.record(z2.enum(["low", "medium", "high"])).optional(),
|
|
3003
|
+
recommendations: z2.array(z2.string()).optional(),
|
|
3004
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
3005
|
+
location: z2.string().optional(),
|
|
3006
|
+
tags: z2.array(z2.string()).optional(),
|
|
3007
|
+
source: z2.object({ agent: z2.string().optional(), reportId: z2.string().optional(), pipelineRunId: z2.string().optional() }).optional(),
|
|
3008
|
+
recommendation: z2.string().optional()
|
|
3009
|
+
});
|
|
3010
|
+
var artifactSchema = z2.object({
|
|
3011
|
+
kind: z2.enum(["file", "patch", "log", "screenshot", "document", "data"]),
|
|
3012
|
+
ref: z2.string().min(1),
|
|
3013
|
+
description: z2.string().optional(),
|
|
3014
|
+
mediaType: z2.string().optional(),
|
|
3015
|
+
bytes: z2.number().int().optional()
|
|
3016
|
+
});
|
|
3017
|
+
var reportSchema = z2.object({
|
|
3018
|
+
id: z2.string().min(1),
|
|
3019
|
+
taskId: z2.string().min(1),
|
|
3020
|
+
agent: z2.string().min(1),
|
|
3021
|
+
agentId: z2.string().optional(),
|
|
3022
|
+
pipelineRunId: z2.string().optional(),
|
|
3023
|
+
model: z2.string().optional(),
|
|
3024
|
+
summary: z2.string(),
|
|
3025
|
+
findings: z2.array(findingSchema),
|
|
3026
|
+
recommendations: z2.array(z2.string()),
|
|
3027
|
+
confidence: z2.enum(["low", "medium", "high"]).optional(),
|
|
3028
|
+
evidence: z2.array(evidenceSchema).optional(),
|
|
3029
|
+
artifacts: z2.array(artifactSchema).optional(),
|
|
3030
|
+
createdAt: z2.string().min(1),
|
|
3031
|
+
metadata: z2.record(z2.unknown()).optional()
|
|
3032
|
+
});
|
|
3033
|
+
function validateReport(value) {
|
|
3034
|
+
const parsed = reportSchema.safeParse(value);
|
|
3035
|
+
if (!parsed.success) {
|
|
3036
|
+
const issues = parsed.error.issues.map((issue) => ({
|
|
3037
|
+
path: issue.path.join(".") || "(root)",
|
|
3038
|
+
message: issue.message
|
|
3039
|
+
}));
|
|
3040
|
+
return {
|
|
3041
|
+
valid: false,
|
|
3042
|
+
issues,
|
|
3043
|
+
reason: `report failed schema validation (${issues.length} issue(s))`
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
const report = parsed.data;
|
|
3047
|
+
report.findings = report.findings.map((finding) => ({
|
|
3048
|
+
...finding,
|
|
3049
|
+
description: finding.description ?? finding.detail,
|
|
3050
|
+
...finding.source === void 0 ? { source: { agent: report.agent, reportId: report.id, ...report.pipelineRunId !== void 0 ? { pipelineRunId: report.pipelineRunId } : {} } } : {}
|
|
3051
|
+
}));
|
|
3052
|
+
return { valid: true, report };
|
|
3053
|
+
}
|
|
3054
|
+
function normalizeEvidence(input) {
|
|
3055
|
+
if (input === void 0) return void 0;
|
|
3056
|
+
return input.map(
|
|
3057
|
+
(item) => typeof item === "string" ? { kind: "observation", excerpt: item } : item
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
function normalizeFinding(finding) {
|
|
3061
|
+
return {
|
|
3062
|
+
...finding,
|
|
3063
|
+
description: finding.description ?? finding.detail,
|
|
3064
|
+
evidence: normalizeEvidence(finding.evidence)
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/reports/aggregate.ts
|
|
3069
|
+
var SEVERITY_RANK = {
|
|
3070
|
+
info: 0,
|
|
3071
|
+
low: 1,
|
|
3072
|
+
medium: 2,
|
|
3073
|
+
high: 3,
|
|
3074
|
+
critical: 4
|
|
3075
|
+
};
|
|
3076
|
+
var MAX_COMBINED_FINDINGS = 200;
|
|
3077
|
+
function aggregateReports(reports, options = {}) {
|
|
3078
|
+
const threshold = options.similarityThreshold ?? 0.7;
|
|
3079
|
+
const findingGroups = [];
|
|
3080
|
+
const recommendationGroups = [];
|
|
3081
|
+
for (const report of reports) {
|
|
3082
|
+
for (const finding of report.findings) {
|
|
3083
|
+
mergeFinding(findingGroups, finding, report, threshold);
|
|
3084
|
+
}
|
|
3085
|
+
for (const text of report.recommendations) {
|
|
3086
|
+
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
3090
|
+
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
3091
|
+
const conflicts = detectConflicts(findingGroups);
|
|
3092
|
+
const agreements = findingGroups.filter(
|
|
3093
|
+
(group) => group.sources.length > 1 && group.severity !== void 0 && new Set(Object.values(group.severityBySource ?? {})).size === 1
|
|
3094
|
+
);
|
|
3095
|
+
const maxFindings = options.maxFindings ?? MAX_COMBINED_FINDINGS;
|
|
3096
|
+
const orderedFindings = findingGroups.sort((a, b) => rank(b) - rank(a));
|
|
3097
|
+
const retainedFindings = orderedFindings.slice(0, maxFindings);
|
|
3098
|
+
const droppedFindings = orderedFindings.length - retainedFindings.length;
|
|
3099
|
+
const disputed = conflicts.length;
|
|
3100
|
+
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).`;
|
|
3101
|
+
return {
|
|
3102
|
+
id: `combined-${Date.now().toString(36)}-${randomBytes2(3).toString("hex")}`,
|
|
3103
|
+
title: options.objective !== void 0 ? truncate(options.objective, 120) : "Combined report",
|
|
3104
|
+
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
3105
|
+
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
3106
|
+
taskIds: options.taskIds ?? [],
|
|
3107
|
+
summary,
|
|
3108
|
+
findings: retainedFindings,
|
|
3109
|
+
agreements,
|
|
3110
|
+
conflicts,
|
|
3111
|
+
recommendations: recommendationGroups,
|
|
3112
|
+
sources,
|
|
3113
|
+
reportIds: reports.map((r) => r.id),
|
|
3114
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3115
|
+
metadata: {
|
|
3116
|
+
agents: sources.length,
|
|
3117
|
+
reports: reports.length,
|
|
3118
|
+
rawFindings: totalRawFindings,
|
|
3119
|
+
findingGroups: findingGroups.length,
|
|
3120
|
+
agreements: agreements.length,
|
|
3121
|
+
conflicts: conflicts.length,
|
|
3122
|
+
findingsTruncated: droppedFindings
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
function rankConfidence(confidence) {
|
|
3127
|
+
return confidence ? { low: 1, medium: 2, high: 3 }[confidence] : 0;
|
|
3128
|
+
}
|
|
3129
|
+
function detectConflicts(groups) {
|
|
3130
|
+
const conflicts = [];
|
|
3131
|
+
for (const group of groups) {
|
|
3132
|
+
if (group.sources.length < 2) continue;
|
|
3133
|
+
const severities = new Set(Object.values(group.severityBySource ?? {}));
|
|
3134
|
+
if (severities.size > 1) {
|
|
3135
|
+
conflicts.push({
|
|
3136
|
+
type: "severity",
|
|
3137
|
+
findingTitle: group.title,
|
|
3138
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
3139
|
+
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.`
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
const recommendations = (group.recommendations ?? []).filter((r) => r.length > 0);
|
|
3143
|
+
if (recommendations.length > 1) {
|
|
3144
|
+
conflicts.push({
|
|
3145
|
+
type: "recommendation",
|
|
3146
|
+
findingTitle: group.title,
|
|
3147
|
+
positions: group.sources.map((source) => position(source, group)),
|
|
3148
|
+
note: `Agents proposed ${recommendations.length} different fixes; none was discarded.`
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
return conflicts;
|
|
3153
|
+
}
|
|
3154
|
+
function position(source, group) {
|
|
3155
|
+
return {
|
|
3156
|
+
agent: source.agent,
|
|
3157
|
+
reportId: source.reportId,
|
|
3158
|
+
...group.severityBySource?.[source.agent] !== void 0 ? { severity: group.severityBySource[source.agent] } : {},
|
|
3159
|
+
...group.confidenceBySource?.[source.agent] !== void 0 ? { confidence: group.confidenceBySource[source.agent] } : {},
|
|
3160
|
+
...group.recommendations?.[0] !== void 0 ? { recommendation: group.recommendations[0] } : {}
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
function rank(finding) {
|
|
3164
|
+
return finding.severity ? SEVERITY_RANK[finding.severity] : -1;
|
|
3165
|
+
}
|
|
3166
|
+
function mergeFinding(groups, finding, report, threshold) {
|
|
3167
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
3168
|
+
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
3169
|
+
if (group) {
|
|
3170
|
+
group.occurrences += 1;
|
|
3171
|
+
group.details.push(finding.detail);
|
|
3172
|
+
group.sources.push(source);
|
|
3173
|
+
group.evidence.push(...normalizeEvidence(finding.evidence) ?? []);
|
|
3174
|
+
if (finding.severity && (!group.severity || SEVERITY_RANK[finding.severity] > SEVERITY_RANK[group.severity])) {
|
|
3175
|
+
group.severity = finding.severity;
|
|
3176
|
+
}
|
|
3177
|
+
if (finding.confidence && rankConfidence(finding.confidence) > rankConfidence(group.confidence)) {
|
|
3178
|
+
group.confidence = finding.confidence;
|
|
3179
|
+
}
|
|
3180
|
+
if (finding.severity) {
|
|
3181
|
+
group.severityBySource = { ...group.severityBySource, [report.agent]: finding.severity };
|
|
3182
|
+
}
|
|
3183
|
+
if (finding.confidence) {
|
|
3184
|
+
group.confidenceBySource = { ...group.confidenceBySource, [report.agent]: finding.confidence };
|
|
3185
|
+
}
|
|
3186
|
+
if (finding.recommendation && !group.recommendations?.includes(finding.recommendation)) {
|
|
3187
|
+
group.recommendations = [...group.recommendations ?? [], finding.recommendation];
|
|
3188
|
+
}
|
|
3189
|
+
return;
|
|
1651
3190
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
audit: new FileAuditLog(),
|
|
1664
|
-
log
|
|
3191
|
+
groups.push({
|
|
3192
|
+
title: finding.title,
|
|
3193
|
+
details: [finding.detail],
|
|
3194
|
+
sources: [source],
|
|
3195
|
+
occurrences: 1,
|
|
3196
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
3197
|
+
...finding.confidence ? { confidence: finding.confidence } : {},
|
|
3198
|
+
...finding.severity ? { severityBySource: { [report.agent]: finding.severity } } : {},
|
|
3199
|
+
...finding.confidence ? { confidenceBySource: { [report.agent]: finding.confidence } } : {},
|
|
3200
|
+
...finding.recommendation ? { recommendations: [finding.recommendation] } : {},
|
|
3201
|
+
evidence: [...normalizeEvidence(finding.evidence) ?? []]
|
|
1665
3202
|
});
|
|
1666
3203
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
3204
|
+
function mergeRecommendation(groups, text, report, threshold) {
|
|
3205
|
+
const source = { agent: report.agent, reportId: report.id };
|
|
3206
|
+
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
3207
|
+
if (group) {
|
|
3208
|
+
group.occurrences += 1;
|
|
3209
|
+
group.sources.push(source);
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
groups.push({ text, sources: [source], occurrences: 1 });
|
|
3213
|
+
}
|
|
3214
|
+
function similar(a, b, threshold) {
|
|
3215
|
+
const left = tokens(a);
|
|
3216
|
+
const right = tokens(b);
|
|
3217
|
+
if (left.size === 0 || right.size === 0) return false;
|
|
3218
|
+
let intersection = 0;
|
|
3219
|
+
for (const token of left) {
|
|
3220
|
+
if (right.has(token)) intersection += 1;
|
|
3221
|
+
}
|
|
3222
|
+
const union = left.size + right.size - intersection;
|
|
3223
|
+
return intersection / union >= threshold;
|
|
3224
|
+
}
|
|
3225
|
+
function tokens(text) {
|
|
3226
|
+
return new Set(
|
|
3227
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
function truncate(text, max) {
|
|
3231
|
+
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
3232
|
+
}
|
|
1670
3233
|
|
|
1671
3234
|
// src/planner/heuristic.ts
|
|
1672
|
-
import { randomBytes as
|
|
1673
|
-
var
|
|
3235
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3236
|
+
var SEVERITY_RANK2 = {
|
|
1674
3237
|
info: 0,
|
|
1675
3238
|
low: 1,
|
|
1676
3239
|
medium: 2,
|
|
@@ -1681,19 +3244,32 @@ var HeuristicPlanner = class {
|
|
|
1681
3244
|
name = "heuristic";
|
|
1682
3245
|
async plan(request) {
|
|
1683
3246
|
const steps = [];
|
|
1684
|
-
const findings = request.reports.flatMap(
|
|
1685
|
-
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
1686
|
-
);
|
|
1687
3247
|
const recommendations = dedupe(request.reports.flatMap((r) => r.recommendations));
|
|
1688
|
-
for (const
|
|
3248
|
+
for (const text of recommendations) {
|
|
1689
3249
|
steps.push({
|
|
1690
3250
|
id: `step-${steps.length + 1}`,
|
|
1691
|
-
title:
|
|
3251
|
+
title: truncate2(text, 120),
|
|
1692
3252
|
order: steps.length + 1,
|
|
1693
|
-
...
|
|
3253
|
+
...steps.length > 0 ? { dependsOn: [`step-${steps.length}`] } : {}
|
|
1694
3254
|
});
|
|
1695
3255
|
}
|
|
1696
|
-
const
|
|
3256
|
+
const contributed = request.aggregated ? request.aggregated.findings.map((finding) => ({
|
|
3257
|
+
finding: {
|
|
3258
|
+
id: `agg-${slug(finding.title)}`,
|
|
3259
|
+
title: finding.title,
|
|
3260
|
+
detail: finding.details[0] ?? finding.title,
|
|
3261
|
+
...finding.severity ? { severity: finding.severity } : {},
|
|
3262
|
+
...finding.recommendations && finding.recommendations.length > 0 ? { recommendation: finding.recommendations[0] } : {}
|
|
3263
|
+
},
|
|
3264
|
+
agent: finding.sources.map((s) => s.agent).join("+")
|
|
3265
|
+
})) : request.reports.flatMap(
|
|
3266
|
+
(report) => report.findings.map((finding) => ({ finding, agent: report.agent }))
|
|
3267
|
+
);
|
|
3268
|
+
const significant = contributed.filter(
|
|
3269
|
+
({ finding }) => finding.severity ? SEVERITY_RANK2[finding.severity] >= SEVERITY_RANK2.high : false
|
|
3270
|
+
).sort(
|
|
3271
|
+
(a, b) => SEVERITY_RANK2[b.finding.severity ?? "info"] - SEVERITY_RANK2[a.finding.severity ?? "info"]
|
|
3272
|
+
);
|
|
1697
3273
|
for (const { finding } of significant) {
|
|
1698
3274
|
steps.push(remediationStep(finding, steps.length + 1));
|
|
1699
3275
|
}
|
|
@@ -1706,12 +3282,20 @@ var HeuristicPlanner = class {
|
|
|
1706
3282
|
});
|
|
1707
3283
|
}
|
|
1708
3284
|
const risks = significant.filter(({ finding }) => finding.severity === "critical").map(({ finding }) => `Critical finding unaddressed until its step completes: ${finding.title}`);
|
|
3285
|
+
const conflicts = request.aggregated?.conflicts ?? [];
|
|
3286
|
+
if (conflicts.length > 0) {
|
|
3287
|
+
risks.push(
|
|
3288
|
+
`${conflicts.length} unresolved agent disagreement(s): ${conflicts.map((c) => c.findingTitle).join("; ")}`
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
1709
3291
|
return {
|
|
1710
|
-
id: `plan-${Date.now().toString(36)}-${
|
|
3292
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes3(3).toString("hex")}`,
|
|
1711
3293
|
objective: request.objective,
|
|
3294
|
+
goal: request.aggregated?.title ?? truncate2(request.objective, 120),
|
|
1712
3295
|
summary: `Derived from ${request.reports.length} report(s): ${recommendations.length} recommendation step(s), ${significant.length} remediation step(s).`,
|
|
1713
3296
|
steps,
|
|
1714
3297
|
...risks.length > 0 ? { risks } : {},
|
|
3298
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
1715
3299
|
generatedBy: "heuristic",
|
|
1716
3300
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
1717
3301
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
@@ -1722,28 +3306,80 @@ var HeuristicPlanner = class {
|
|
|
1722
3306
|
function remediationStep(finding, order) {
|
|
1723
3307
|
return {
|
|
1724
3308
|
id: `step-${order}`,
|
|
1725
|
-
title: `Fix: ${
|
|
3309
|
+
title: `Fix: ${truncate2(finding.title, 100)}`,
|
|
1726
3310
|
description: finding.detail,
|
|
1727
3311
|
order,
|
|
1728
3312
|
dependsOn: order > 1 ? [`step-${order - 1}`] : [],
|
|
1729
|
-
sourceFindings: [finding.id]
|
|
3313
|
+
sourceFindings: [finding.id],
|
|
3314
|
+
...finding.recommendation ? { recommendation: finding.recommendation } : {}
|
|
1730
3315
|
};
|
|
1731
3316
|
}
|
|
1732
3317
|
function dedupe(texts) {
|
|
1733
3318
|
return [...new Set(texts.map((t) => t.trim()).filter((t) => t.length > 0))];
|
|
1734
3319
|
}
|
|
1735
|
-
function
|
|
3320
|
+
function slug(text) {
|
|
3321
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
3322
|
+
}
|
|
3323
|
+
function truncate2(text, max) {
|
|
1736
3324
|
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
1737
3325
|
}
|
|
1738
3326
|
|
|
1739
3327
|
// src/planner/model.ts
|
|
1740
|
-
import { randomBytes as
|
|
1741
|
-
|
|
3328
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
3329
|
+
|
|
3330
|
+
// src/planner/schema.ts
|
|
3331
|
+
import { z as z3 } from "zod";
|
|
3332
|
+
var planStepSchema = z3.object({
|
|
3333
|
+
id: z3.string().min(1).optional(),
|
|
3334
|
+
title: z3.string().min(1),
|
|
3335
|
+
description: z3.string().optional(),
|
|
3336
|
+
order: z3.number().int().optional(),
|
|
3337
|
+
targets: z3.array(z3.string()).optional(),
|
|
3338
|
+
acceptance_criteria: z3.array(z3.string()).optional(),
|
|
3339
|
+
depends_on: z3.array(z3.string()).optional(),
|
|
3340
|
+
source_findings: z3.array(z3.string()).optional()
|
|
3341
|
+
});
|
|
3342
|
+
var planJsonSchema = z3.object({
|
|
3343
|
+
goal: z3.string().min(1),
|
|
3344
|
+
summary: z3.string().min(1),
|
|
3345
|
+
steps: z3.array(planStepSchema).min(1),
|
|
3346
|
+
risks: z3.array(z3.string()).optional(),
|
|
3347
|
+
notes: z3.array(z3.string()).optional()
|
|
3348
|
+
});
|
|
3349
|
+
function validatePlan(plan) {
|
|
3350
|
+
const issues = [];
|
|
3351
|
+
const stepIds = /* @__PURE__ */ new Set();
|
|
3352
|
+
for (const [index, step] of plan.steps.entries()) {
|
|
3353
|
+
if (stepIds.has(step.id)) {
|
|
3354
|
+
issues.push({ path: `steps.${index}.id`, message: `duplicate step id "${step.id}"` });
|
|
3355
|
+
}
|
|
3356
|
+
stepIds.add(step.id);
|
|
3357
|
+
if (!step.title || step.title.trim().length === 0) {
|
|
3358
|
+
issues.push({ path: `steps.${index}.title`, message: "title must not be empty" });
|
|
3359
|
+
}
|
|
3360
|
+
for (const dep of step.dependsOn ?? []) {
|
|
3361
|
+
if (!stepIds.has(dep) && !plan.steps.some((s) => s.id === dep)) {
|
|
3362
|
+
issues.push({ path: `steps.${index}.dependsOn`, message: `unknown step dependency "${dep}"` });
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
if (plan.steps.length === 0) {
|
|
3367
|
+
issues.push({ path: "steps", message: "a plan needs at least one step" });
|
|
3368
|
+
}
|
|
3369
|
+
if (!plan.summary || plan.summary.trim().length === 0) {
|
|
3370
|
+
issues.push({ path: "summary", message: "summary must not be empty" });
|
|
3371
|
+
}
|
|
3372
|
+
return issues.length > 0 ? { valid: false, issues, reason: `plan failed validation (${issues.length} issue(s))` } : { valid: true, plan };
|
|
3373
|
+
}
|
|
1742
3374
|
|
|
1743
3375
|
// src/planner/context.ts
|
|
1744
3376
|
function buildPlannerContext(request) {
|
|
1745
|
-
const parts = [`#
|
|
3377
|
+
const parts = [`# Task
|
|
1746
3378
|
${request.objective}`];
|
|
3379
|
+
if (request.projectContext) {
|
|
3380
|
+
parts.push(`# Project context
|
|
3381
|
+
${request.projectContext}`);
|
|
3382
|
+
}
|
|
1747
3383
|
if (request.role) {
|
|
1748
3384
|
const roleLines = [`# Role
|
|
1749
3385
|
${request.role.name} (${request.role.id})`];
|
|
@@ -1763,7 +3399,7 @@ ${request.role.name} (${request.role.id})`];
|
|
|
1763
3399
|
)
|
|
1764
3400
|
);
|
|
1765
3401
|
parts.push(
|
|
1766
|
-
`#
|
|
3402
|
+
`# Agent reports (${request.reports.length} by ${agents})
|
|
1767
3403
|
` + (findingLines.length > 0 ? findingLines.join("\n") : "(no findings reported)")
|
|
1768
3404
|
);
|
|
1769
3405
|
const recommendationLines = request.reports.flatMap(
|
|
@@ -1773,9 +3409,20 @@ ${request.role.name} (${request.role.id})`];
|
|
|
1773
3409
|
parts.push(`# Recommendations
|
|
1774
3410
|
${recommendationLines.join("\n")}`);
|
|
1775
3411
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
3412
|
+
}
|
|
3413
|
+
if (request.aggregated) {
|
|
3414
|
+
const aggregatedLines = [
|
|
3415
|
+
`- findings: ${request.aggregated.findings.length} (agreements: ${request.aggregated.agreements.length}, conflicts: ${request.aggregated.conflicts.length})`,
|
|
3416
|
+
...request.aggregated.findings.map(
|
|
3417
|
+
(finding) => `- [${finding.severity ?? "info"}] ${finding.title} (x${finding.occurrences})`
|
|
3418
|
+
),
|
|
3419
|
+
// Conflicts stay visible to the planner — it must plan around disagreement.
|
|
3420
|
+
...request.aggregated.conflicts.map(
|
|
3421
|
+
(conflict) => `- CONFLICT (${conflict.type}) on "${conflict.findingTitle}": ${conflict.note}`
|
|
3422
|
+
)
|
|
3423
|
+
];
|
|
3424
|
+
parts.push(`# Aggregated findings
|
|
3425
|
+
${aggregatedLines.join("\n")}`);
|
|
1779
3426
|
}
|
|
1780
3427
|
if (request.memory && request.memory.length > 0) {
|
|
1781
3428
|
parts.push(
|
|
@@ -1783,7 +3430,11 @@ ${summaryLines.join("\n")}`);
|
|
|
1783
3430
|
` + request.memory.map((entry) => `- [${entry.scope}] ${entry.content}`).join("\n")
|
|
1784
3431
|
);
|
|
1785
3432
|
}
|
|
1786
|
-
if (request.
|
|
3433
|
+
if (request.constraints && request.constraints.length > 0) {
|
|
3434
|
+
parts.push(`# Constraints
|
|
3435
|
+
${request.constraints.map((c) => `- ${c}`).join("\n")}`);
|
|
3436
|
+
}
|
|
3437
|
+
if (request.context) {
|
|
1787
3438
|
parts.push(`# Research context
|
|
1788
3439
|
${request.context}`);
|
|
1789
3440
|
}
|
|
@@ -1791,22 +3442,6 @@ ${request.context}`);
|
|
|
1791
3442
|
}
|
|
1792
3443
|
|
|
1793
3444
|
// src/planner/model.ts
|
|
1794
|
-
var planStepJsonSchema = z2.object({
|
|
1795
|
-
id: z2.string().optional(),
|
|
1796
|
-
title: z2.string().min(1),
|
|
1797
|
-
description: z2.string().optional(),
|
|
1798
|
-
order: z2.number().int().optional(),
|
|
1799
|
-
targets: z2.array(z2.string()).optional(),
|
|
1800
|
-
acceptance_criteria: z2.array(z2.string()).optional(),
|
|
1801
|
-
depends_on: z2.array(z2.string()).optional(),
|
|
1802
|
-
source_findings: z2.array(z2.string()).optional()
|
|
1803
|
-
}).strict();
|
|
1804
|
-
var planJsonSchema = z2.object({
|
|
1805
|
-
summary: z2.string().min(1),
|
|
1806
|
-
steps: z2.array(planStepJsonSchema).min(1),
|
|
1807
|
-
risks: z2.array(z2.string()).optional(),
|
|
1808
|
-
notes: z2.array(z2.string()).optional()
|
|
1809
|
-
}).strict();
|
|
1810
3445
|
var ModelPlanner = class {
|
|
1811
3446
|
constructor(options) {
|
|
1812
3447
|
this.options = options;
|
|
@@ -1820,7 +3455,7 @@ var ModelPlanner = class {
|
|
|
1820
3455
|
const messages = [
|
|
1821
3456
|
{
|
|
1822
3457
|
role: "system",
|
|
1823
|
-
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 ?? "")
|
|
3458
|
+
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 ?? "")
|
|
1824
3459
|
},
|
|
1825
3460
|
{ role: "user", content: context }
|
|
1826
3461
|
];
|
|
@@ -1831,66 +3466,99 @@ var ModelPlanner = class {
|
|
|
1831
3466
|
return this.degrade(request, `planner model call failed: ${describe(error)}`);
|
|
1832
3467
|
}
|
|
1833
3468
|
const parsed = parsePlanJson(responseText);
|
|
1834
|
-
if (!parsed) {
|
|
1835
|
-
return this.degrade(request,
|
|
3469
|
+
if (!parsed.ok) {
|
|
3470
|
+
return this.degrade(request, `planner model output rejected: ${parsed.reason}`, parsed.issues);
|
|
1836
3471
|
}
|
|
1837
3472
|
const plan = {
|
|
1838
|
-
id: `plan-${Date.now().toString(36)}-${
|
|
3473
|
+
id: `plan-${Date.now().toString(36)}-${randomBytes4(3).toString("hex")}`,
|
|
1839
3474
|
objective: request.objective,
|
|
3475
|
+
goal: parsed.goal,
|
|
1840
3476
|
summary: parsed.summary,
|
|
1841
3477
|
steps: parsed.steps,
|
|
1842
3478
|
...parsed.risks !== void 0 ? { risks: parsed.risks } : {},
|
|
1843
3479
|
...parsed.notes !== void 0 ? { notes: parsed.notes } : {},
|
|
3480
|
+
...request.constraints !== void 0 && request.constraints.length > 0 ? { constraints: request.constraints } : {},
|
|
1844
3481
|
generatedBy: `model:${this.options.model}`,
|
|
1845
3482
|
model: this.options.model,
|
|
1846
3483
|
...request.taskId !== void 0 ? { taskId: request.taskId } : {},
|
|
1847
3484
|
...request.pipelineRunId !== void 0 ? { pipelineRunId: request.pipelineRunId } : {},
|
|
1848
3485
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1849
3486
|
};
|
|
1850
|
-
|
|
3487
|
+
const validation = validatePlan(plan);
|
|
3488
|
+
if (!validation.valid) {
|
|
3489
|
+
return this.degrade(request, `generated plan rejected: ${validation.reason}`, validation.issues);
|
|
3490
|
+
}
|
|
3491
|
+
return validation.plan;
|
|
1851
3492
|
}
|
|
1852
|
-
async degrade(request, reason) {
|
|
3493
|
+
async degrade(request, reason, issues = []) {
|
|
1853
3494
|
if (this.options.fallback) {
|
|
1854
3495
|
this.log.warn(`${reason} \u2014 using the heuristic fallback planner.`);
|
|
1855
3496
|
return this.options.fallback.plan(request);
|
|
1856
3497
|
}
|
|
1857
|
-
throw new PlannerError("
|
|
3498
|
+
throw new PlannerError("PLAN_INVALID", `Planner failed: ${reason}.`, {
|
|
3499
|
+
details: { reason, issues }
|
|
3500
|
+
});
|
|
1858
3501
|
}
|
|
1859
3502
|
};
|
|
1860
3503
|
function parsePlanJson(text) {
|
|
1861
3504
|
const unfenced = text.replace(/```(?:json)?/gi, "");
|
|
1862
3505
|
const start = unfenced.indexOf("{");
|
|
1863
3506
|
const end = unfenced.lastIndexOf("}");
|
|
1864
|
-
if (start < 0 || end <= start)
|
|
3507
|
+
if (start < 0 || end <= start) {
|
|
3508
|
+
return { ok: false, reason: "planner output contained no JSON object", issues: [] };
|
|
3509
|
+
}
|
|
3510
|
+
let json;
|
|
1865
3511
|
try {
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
if (!parsed.success) return void 0;
|
|
1869
|
-
const steps = parsed.data.steps.map((step, index) => ({
|
|
1870
|
-
id: step.id ?? `step-${index + 1}`,
|
|
1871
|
-
title: step.title,
|
|
1872
|
-
description: step.description,
|
|
1873
|
-
order: step.order ?? index + 1,
|
|
1874
|
-
targets: step.targets,
|
|
1875
|
-
acceptanceCriteria: step.acceptance_criteria,
|
|
1876
|
-
dependsOn: step.depends_on,
|
|
1877
|
-
sourceFindings: step.source_findings
|
|
1878
|
-
}));
|
|
3512
|
+
json = JSON.parse(unfenced.slice(start, end + 1));
|
|
3513
|
+
} catch (error) {
|
|
1879
3514
|
return {
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
3515
|
+
ok: false,
|
|
3516
|
+
reason: `planner output was not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
3517
|
+
issues: []
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
const parsed = planJsonSchema.safeParse(json);
|
|
3521
|
+
if (!parsed.success) {
|
|
3522
|
+
return {
|
|
3523
|
+
ok: false,
|
|
3524
|
+
reason: "planner output did not match the plan schema",
|
|
3525
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
3526
|
+
path: issue.path.join(".") || "(root)",
|
|
3527
|
+
message: issue.message
|
|
3528
|
+
}))
|
|
1884
3529
|
};
|
|
1885
|
-
} catch {
|
|
1886
|
-
return void 0;
|
|
1887
3530
|
}
|
|
3531
|
+
const steps = parsed.data.steps.map((step, index) => ({
|
|
3532
|
+
id: step.id ?? `step-${index + 1}`,
|
|
3533
|
+
title: step.title,
|
|
3534
|
+
description: step.description,
|
|
3535
|
+
order: step.order ?? index + 1,
|
|
3536
|
+
targets: step.targets,
|
|
3537
|
+
acceptanceCriteria: step.acceptance_criteria,
|
|
3538
|
+
dependsOn: step.depends_on,
|
|
3539
|
+
sourceFindings: step.source_findings
|
|
3540
|
+
})).sort((a, b) => a.order - b.order);
|
|
3541
|
+
return {
|
|
3542
|
+
ok: true,
|
|
3543
|
+
goal: parsed.data.goal,
|
|
3544
|
+
summary: parsed.data.summary,
|
|
3545
|
+
steps,
|
|
3546
|
+
risks: parsed.data.risks,
|
|
3547
|
+
notes: parsed.data.notes
|
|
3548
|
+
};
|
|
1888
3549
|
}
|
|
1889
3550
|
function describe(error) {
|
|
1890
3551
|
return error instanceof Error ? error.message : String(error);
|
|
1891
3552
|
}
|
|
1892
3553
|
|
|
1893
3554
|
// src/pipelines/engine.ts
|
|
3555
|
+
var DEFAULT_MAX_PARALLEL_STEPS = 8;
|
|
3556
|
+
var MAX_COMBINED_CONTEXT_CHARS = 2e4;
|
|
3557
|
+
function budgetContext(context, maxChars = MAX_COMBINED_CONTEXT_CHARS) {
|
|
3558
|
+
if (context.length <= maxChars) return context;
|
|
3559
|
+
const marker = "\n[earlier context trimmed]\n";
|
|
3560
|
+
return marker + context.slice(context.length - (maxChars - marker.length));
|
|
3561
|
+
}
|
|
1894
3562
|
var PipelineEngine = class {
|
|
1895
3563
|
constructor(options) {
|
|
1896
3564
|
this.options = options;
|
|
@@ -1898,9 +3566,23 @@ var PipelineEngine = class {
|
|
|
1898
3566
|
}
|
|
1899
3567
|
options;
|
|
1900
3568
|
log;
|
|
3569
|
+
/** Runs executing in THIS process — cancellation can terminate them directly. */
|
|
3570
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
1901
3571
|
get store() {
|
|
1902
3572
|
return this.options.store;
|
|
1903
3573
|
}
|
|
3574
|
+
/**
|
|
3575
|
+
* Saves the run while preserving fields that may be set by another process
|
|
3576
|
+
* (e.g. a cancellation request) — an in-memory snapshot must never clobber
|
|
3577
|
+
* externally requested cancellation (§9.2).
|
|
3578
|
+
*/
|
|
3579
|
+
async persist(run) {
|
|
3580
|
+
const current = await this.options.store.get(run.id);
|
|
3581
|
+
await this.options.store.save({
|
|
3582
|
+
...run,
|
|
3583
|
+
...current?.cancellationRequested === true ? { cancellationRequested: true, cancellationRequestedAt: current.cancellationRequestedAt } : {}
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
1904
3586
|
/** Structural + semantic validation (§27 Phase 5 — pipeline parser). */
|
|
1905
3587
|
validate(def) {
|
|
1906
3588
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -2000,22 +3682,49 @@ var PipelineEngine = class {
|
|
|
2000
3682
|
);
|
|
2001
3683
|
}
|
|
2002
3684
|
}
|
|
3685
|
+
if (step.model) {
|
|
3686
|
+
const separator = step.model.indexOf(":");
|
|
3687
|
+
if (separator <= 0 || separator === step.model.length - 1) {
|
|
3688
|
+
throw new PipelineError(
|
|
3689
|
+
"PIPELINE_INVALID",
|
|
3690
|
+
`Step "${step.id}" has an invalid model reference "${step.model}" \u2014 use "provider:model-id".`
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
const providerName = step.model.slice(0, separator);
|
|
3694
|
+
if (!this.options.providers.some((p) => p.name === providerName)) {
|
|
3695
|
+
throw new PipelineError(
|
|
3696
|
+
"PIPELINE_INVALID",
|
|
3697
|
+
`Step "${step.id}" uses model "${step.model}" but provider "${providerName}" is not configured.`
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
2003
3701
|
}
|
|
2004
3702
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2005
3703
|
const run = {
|
|
2006
|
-
id: `run-${Date.now().toString(36)}-${
|
|
3704
|
+
id: `run-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2007
3705
|
pipelineId: def.id,
|
|
3706
|
+
attempt: 1,
|
|
2008
3707
|
objective,
|
|
2009
3708
|
status: "running",
|
|
2010
3709
|
startedAt: now,
|
|
2011
3710
|
stepRuns: []
|
|
2012
3711
|
};
|
|
2013
3712
|
await this.options.store.save(run);
|
|
2014
|
-
|
|
3713
|
+
const liveTaskIds = /* @__PURE__ */ new Set();
|
|
3714
|
+
this.activeRuns.set(run.id, {
|
|
3715
|
+
taskIds: liveTaskIds,
|
|
3716
|
+
cancelTasks: async () => {
|
|
3717
|
+
for (const taskId of liveTaskIds) {
|
|
3718
|
+
await this.options.tasks.cancel(taskId).catch(() => void 0);
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
});
|
|
3722
|
+
this.log.info(`Running pipeline ${def.id} as ${run.id} (attempt ${run.attempt}).`);
|
|
2015
3723
|
await this.options.audit?.record({
|
|
2016
3724
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2017
3725
|
action: "pipeline.run.started",
|
|
2018
3726
|
id: run.id,
|
|
3727
|
+
pipelineId: def.id,
|
|
2019
3728
|
detail: { pipeline: def.id, objective }
|
|
2020
3729
|
});
|
|
2021
3730
|
const deps = this.effectiveDependencies(def);
|
|
@@ -2023,28 +3732,57 @@ var PipelineEngine = class {
|
|
|
2023
3732
|
const pending = new Set(def.steps.map((s) => s.id));
|
|
2024
3733
|
const contextParts = [];
|
|
2025
3734
|
const allReports = [];
|
|
3735
|
+
let cancelled = false;
|
|
2026
3736
|
while (pending.size > 0) {
|
|
3737
|
+
const persisted = await this.options.store.get(run.id);
|
|
3738
|
+
if (persisted?.cancellationRequested === true) {
|
|
3739
|
+
cancelled = true;
|
|
3740
|
+
run.cancellationRequested = true;
|
|
3741
|
+
run.cancellationRequestedAt = persisted.cancellationRequestedAt;
|
|
3742
|
+
await this.activeRuns.get(run.id)?.cancelTasks();
|
|
3743
|
+
for (const step of def.steps.filter((s) => pending.has(s.id))) {
|
|
3744
|
+
const stepRun = { stepId: step.id, status: "cancelled" };
|
|
3745
|
+
run.stepRuns.push(stepRun);
|
|
3746
|
+
options.onStep?.(stepRun);
|
|
3747
|
+
}
|
|
3748
|
+
pending.clear();
|
|
3749
|
+
break;
|
|
3750
|
+
}
|
|
2027
3751
|
const ready = def.steps.filter(
|
|
2028
3752
|
(step) => pending.has(step.id) && (deps.get(step.id) ?? []).every((d) => outcomes.has(d))
|
|
2029
3753
|
);
|
|
2030
3754
|
if (ready.length === 0) {
|
|
2031
3755
|
throw new PipelineError("PIPELINE_FAILED", `Pipeline "${def.id}" scheduling stalled.`);
|
|
2032
3756
|
}
|
|
2033
|
-
const settled =
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
3757
|
+
const settled = [];
|
|
3758
|
+
const limit = Math.max(1, this.options.maxParallelSteps ?? DEFAULT_MAX_PARALLEL_STEPS);
|
|
3759
|
+
for (let offset = 0; offset < ready.length; offset += limit) {
|
|
3760
|
+
const batch = ready.slice(offset, offset + limit);
|
|
3761
|
+
settled.push(
|
|
3762
|
+
...await Promise.all(
|
|
3763
|
+
batch.map(async (step) => {
|
|
3764
|
+
if (!this.shouldRun(step, deps.get(step.id) ?? [], outcomes)) {
|
|
3765
|
+
return { status: "skipped", summary: "", taskIds: [], reports: [] };
|
|
3766
|
+
}
|
|
3767
|
+
const ctx = {
|
|
3768
|
+
maxContextChars: this.options.maxContextChars,
|
|
3769
|
+
objective,
|
|
3770
|
+
...role ? { role } : {},
|
|
3771
|
+
combinedContext: budgetContext(
|
|
3772
|
+
contextParts.join("\n\n"),
|
|
3773
|
+
this.options.maxContextChars ?? MAX_COMBINED_CONTEXT_CHARS
|
|
3774
|
+
),
|
|
3775
|
+
reports: [...allReports],
|
|
3776
|
+
pipelineRunId: run.id,
|
|
3777
|
+
runId: run.id,
|
|
3778
|
+
pipelineDefId: def.id,
|
|
3779
|
+
taskId: run.id
|
|
3780
|
+
};
|
|
3781
|
+
return this.executeStep(step, ctx);
|
|
3782
|
+
})
|
|
3783
|
+
)
|
|
3784
|
+
);
|
|
3785
|
+
}
|
|
2048
3786
|
ready.forEach((step, index) => {
|
|
2049
3787
|
const outcome = settled[index];
|
|
2050
3788
|
outcomes.set(step.id, outcome);
|
|
@@ -2057,6 +3795,8 @@ ${contribution}`);
|
|
|
2057
3795
|
}
|
|
2058
3796
|
const stepRun = {
|
|
2059
3797
|
stepId: step.id,
|
|
3798
|
+
attempt: outcome.attempt ?? 1,
|
|
3799
|
+
...outcome.executionIds && outcome.executionIds.length > 0 ? { executionIds: outcome.executionIds } : {},
|
|
2060
3800
|
status: outcome.status === "skipped" ? "cancelled" : outcome.status === "failed" ? "failed" : outcome.status === "partial" ? "partially_completed" : "completed",
|
|
2061
3801
|
...outcome.taskIds.length > 0 ? { taskIds: outcome.taskIds } : {},
|
|
2062
3802
|
...outcome.planId !== void 0 ? { planId: outcome.planId } : {},
|
|
@@ -2066,12 +3806,12 @@ ${contribution}`);
|
|
|
2066
3806
|
options.onStep?.(stepRun);
|
|
2067
3807
|
});
|
|
2068
3808
|
run.reports = [...allReports];
|
|
2069
|
-
await this.
|
|
3809
|
+
await this.persist(run);
|
|
2070
3810
|
}
|
|
2071
3811
|
const allOutcomes = [...outcomes.values()];
|
|
2072
|
-
const failed = allOutcomes.some((o) => o.status === "failed");
|
|
2073
|
-
const partial = !failed && allOutcomes.some((o) => o.status === "partial");
|
|
2074
|
-
const finalStatus = failed ? "failed" : partial ? "partial" : "completed";
|
|
3812
|
+
const failed = !cancelled && allOutcomes.some((o) => o.status === "failed");
|
|
3813
|
+
const partial = !cancelled && !failed && allOutcomes.some((o) => o.status === "partial");
|
|
3814
|
+
const finalStatus = cancelled ? "cancelled" : failed ? "failed" : partial ? "partial" : "completed";
|
|
2075
3815
|
const planId = [...outcomes.values()].find((o) => o.planId !== void 0)?.planId;
|
|
2076
3816
|
let resultSummary;
|
|
2077
3817
|
for (let i = def.steps.length - 1; i >= 0; i -= 1) {
|
|
@@ -2099,25 +3839,46 @@ ${contribution}`);
|
|
|
2099
3839
|
run.reports = collected;
|
|
2100
3840
|
}
|
|
2101
3841
|
}
|
|
3842
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3843
|
+
this.activeRuns.delete(run.id);
|
|
2102
3844
|
const finished = {
|
|
2103
3845
|
...run,
|
|
2104
3846
|
status: finalStatus,
|
|
2105
|
-
finishedAt
|
|
3847
|
+
finishedAt,
|
|
3848
|
+
attempts: [...run.attempts ?? [], { attempt: run.attempt, startedAt: run.startedAt ?? finishedAt, finishedAt, status: finalStatus }],
|
|
2106
3849
|
...run.reports !== void 0 && run.reports.length > 0 ? { reports: run.reports } : {},
|
|
2107
3850
|
...combinedReportId !== void 0 ? { combinedReportId } : {},
|
|
2108
3851
|
...planId !== void 0 ? { planId } : {},
|
|
2109
3852
|
...resultSummary !== void 0 ? { resultSummary } : {}
|
|
2110
3853
|
};
|
|
2111
|
-
await this.
|
|
3854
|
+
await this.persist(finished);
|
|
2112
3855
|
this.log.info(`Pipeline ${def.id} finished: ${finalStatus}.`);
|
|
2113
3856
|
await this.options.audit?.record({
|
|
2114
3857
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2115
|
-
action: "pipeline.run.finished",
|
|
3858
|
+
action: cancelled ? "pipeline.run.cancelled" : finalStatus === "failed" ? "pipeline.run.failed" : "pipeline.run.finished",
|
|
2116
3859
|
id: run.id,
|
|
3860
|
+
pipelineId: def.id,
|
|
2117
3861
|
detail: { pipeline: def.id, status: finalStatus }
|
|
2118
3862
|
});
|
|
2119
3863
|
return finished;
|
|
2120
3864
|
}
|
|
3865
|
+
/** First-class cancellation (§9.2): requests cancellation and terminates
|
|
3866
|
+
* tasks that are running in this process immediately. */
|
|
3867
|
+
async cancel(runId) {
|
|
3868
|
+
const run = await this.options.store.get(runId);
|
|
3869
|
+
if (!run) {
|
|
3870
|
+
throw new PipelineError("PIPELINE_NOT_FOUND", `Pipeline run "${runId}" does not exist.`);
|
|
3871
|
+
}
|
|
3872
|
+
await this.options.store.save({
|
|
3873
|
+
...run,
|
|
3874
|
+
cancellationRequested: true,
|
|
3875
|
+
cancellationRequestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3876
|
+
});
|
|
3877
|
+
const active = this.activeRuns.get(runId);
|
|
3878
|
+
if (active) {
|
|
3879
|
+
await active.cancelTasks();
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
2121
3882
|
async get(runId) {
|
|
2122
3883
|
return this.options.store.get(runId);
|
|
2123
3884
|
}
|
|
@@ -2141,9 +3902,16 @@ ${contribution}`);
|
|
|
2141
3902
|
this.log.warn(`Step "${step.id}" failed \u2014 retry ${attempt}/${retries}.`);
|
|
2142
3903
|
}
|
|
2143
3904
|
last = await this.executeStepOnce(step, ctx);
|
|
2144
|
-
if (last.status !== "failed") return last;
|
|
3905
|
+
if (last.status !== "failed") return { ...last, attempt: attempt + 1 };
|
|
2145
3906
|
}
|
|
2146
|
-
return last;
|
|
3907
|
+
return { ...last, attempt: retries + 1 };
|
|
3908
|
+
}
|
|
3909
|
+
/** Registers tasks created for a step as live, so cancellation can terminate them (§9.2). */
|
|
3910
|
+
trackLiveTasks(runId, taskIds) {
|
|
3911
|
+
if (!runId) return;
|
|
3912
|
+
const active = this.activeRuns.get(runId);
|
|
3913
|
+
if (!active) return;
|
|
3914
|
+
for (const id of taskIds) active.taskIds.add(id);
|
|
2147
3915
|
}
|
|
2148
3916
|
async executeStepOnce(step, ctx) {
|
|
2149
3917
|
switch (step.kind) {
|
|
@@ -2170,13 +3938,16 @@ ${contribution}`);
|
|
|
2170
3938
|
agentNames.map(
|
|
2171
3939
|
(name) => this.options.tasks.create({
|
|
2172
3940
|
objective: ctx.objective,
|
|
3941
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
2173
3942
|
agent: name,
|
|
2174
3943
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
2175
3944
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2176
3945
|
})
|
|
2177
3946
|
)
|
|
2178
3947
|
);
|
|
3948
|
+
this.trackLiveTasks(ctx.runId, created.map((t) => t.id));
|
|
2179
3949
|
const results = await Promise.all(created.map((task) => this.options.tasks.run(task.id)));
|
|
3950
|
+
const executionIds = results.map((r) => r.executions?.at(-1)?.executionId).filter((id) => id !== void 0);
|
|
2180
3951
|
const completedTasks = results.filter((r) => r.status === "completed");
|
|
2181
3952
|
const failedTasks = results.filter((r) => r.status !== "completed");
|
|
2182
3953
|
const lines = results.map((result, index) => {
|
|
@@ -2199,15 +3970,23 @@ ${contribution}`);
|
|
|
2199
3970
|
reports
|
|
2200
3971
|
};
|
|
2201
3972
|
}
|
|
2202
|
-
return {
|
|
3973
|
+
return {
|
|
3974
|
+
status: "completed",
|
|
3975
|
+
summary,
|
|
3976
|
+
taskIds: created.map((t) => t.id),
|
|
3977
|
+
...executionIds.length > 0 ? { executionIds } : {},
|
|
3978
|
+
reports
|
|
3979
|
+
};
|
|
2203
3980
|
}
|
|
2204
3981
|
async runSingle(step, ctx, agentName) {
|
|
2205
3982
|
const task = await this.options.tasks.create({
|
|
2206
3983
|
objective: ctx.objective,
|
|
3984
|
+
...ctx.pipelineRunId !== void 0 ? { pipelineId: ctx.pipelineRunId } : {},
|
|
2207
3985
|
agent: agentName,
|
|
2208
3986
|
...ctx.role ? { role: ctx.role.id } : {},
|
|
2209
3987
|
...ctx.combinedContext.length > 0 ? { context: { background: ctx.combinedContext } } : {}
|
|
2210
3988
|
});
|
|
3989
|
+
this.trackLiveTasks(ctx.runId, [task.id]);
|
|
2211
3990
|
const result = await this.options.tasks.run(task.id);
|
|
2212
3991
|
if (result.status !== "completed") {
|
|
2213
3992
|
return {
|
|
@@ -2228,11 +4007,16 @@ ${contribution}`);
|
|
|
2228
4007
|
};
|
|
2229
4008
|
}
|
|
2230
4009
|
async runPlan(step, ctx) {
|
|
4010
|
+
const aggregated = ctx.reports.length > 0 ? aggregateReports(ctx.reports, {
|
|
4011
|
+
...step.constraints !== void 0 && step.constraints.length > 0 ? { maxFindings: MAX_COMBINED_FINDINGS } : {}
|
|
4012
|
+
}) : void 0;
|
|
2231
4013
|
const request = {
|
|
2232
4014
|
objective: ctx.objective,
|
|
2233
4015
|
reports: ctx.reports,
|
|
4016
|
+
...aggregated !== void 0 ? { aggregated } : {},
|
|
2234
4017
|
...ctx.combinedContext.length > 0 ? { context: ctx.combinedContext } : {},
|
|
2235
4018
|
...ctx.role ? { role: ctx.role } : {},
|
|
4019
|
+
...step.constraints !== void 0 && step.constraints.length > 0 ? { constraints: step.constraints } : {},
|
|
2236
4020
|
...ctx.pipelineRunId !== void 0 ? { pipelineRunId: ctx.pipelineRunId } : {}
|
|
2237
4021
|
};
|
|
2238
4022
|
let planner;
|
|
@@ -2270,7 +4054,13 @@ ${contribution}`);
|
|
|
2270
4054
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2271
4055
|
action: "plan.generated",
|
|
2272
4056
|
id: plan.id,
|
|
2273
|
-
|
|
4057
|
+
...ctx.pipelineDefId !== void 0 ? { pipelineId: ctx.pipelineDefId } : {},
|
|
4058
|
+
detail: {
|
|
4059
|
+
generatedBy: plan.generatedBy,
|
|
4060
|
+
steps: plan.steps.length,
|
|
4061
|
+
run: ctx.pipelineRunId,
|
|
4062
|
+
taskId: ctx.taskId
|
|
4063
|
+
}
|
|
2274
4064
|
});
|
|
2275
4065
|
return {
|
|
2276
4066
|
status: "completed",
|
|
@@ -2304,7 +4094,7 @@ function formatPlan(plan) {
|
|
|
2304
4094
|
}
|
|
2305
4095
|
return lines.join("\n");
|
|
2306
4096
|
}
|
|
2307
|
-
function createDefaultChatFn(providers) {
|
|
4097
|
+
function createDefaultChatFn(providers, options = {}) {
|
|
2308
4098
|
return async (modelRef, messages) => {
|
|
2309
4099
|
const separator = modelRef.indexOf(":");
|
|
2310
4100
|
if (separator <= 0 || separator === modelRef.length - 1) {
|
|
@@ -2322,7 +4112,10 @@ function createDefaultChatFn(providers) {
|
|
|
2322
4112
|
`Provider "${providerName}" (model "${modelRef}") is not configured.`
|
|
2323
4113
|
);
|
|
2324
4114
|
}
|
|
2325
|
-
const provider = createProvider(providerConfig
|
|
4115
|
+
const provider = createProvider(providerConfig, {
|
|
4116
|
+
...options.audit !== void 0 ? { audit: options.audit } : {},
|
|
4117
|
+
...options.env !== void 0 ? { env: options.env } : {}
|
|
4118
|
+
});
|
|
2326
4119
|
const response = await provider.chat({ model: modelId, messages });
|
|
2327
4120
|
return response.content;
|
|
2328
4121
|
};
|
|
@@ -2391,99 +4184,6 @@ function buildPlanner(plannerConfig, chat, log = logger) {
|
|
|
2391
4184
|
});
|
|
2392
4185
|
}
|
|
2393
4186
|
|
|
2394
|
-
// src/reports/aggregate.ts
|
|
2395
|
-
import { randomBytes as randomBytes5 } from "crypto";
|
|
2396
|
-
var SEVERITY_RANK2 = {
|
|
2397
|
-
info: 0,
|
|
2398
|
-
low: 1,
|
|
2399
|
-
medium: 2,
|
|
2400
|
-
high: 3,
|
|
2401
|
-
critical: 4
|
|
2402
|
-
};
|
|
2403
|
-
function aggregateReports(reports, options = {}) {
|
|
2404
|
-
const threshold = options.similarityThreshold ?? 0.7;
|
|
2405
|
-
const findingGroups = [];
|
|
2406
|
-
const recommendationGroups = [];
|
|
2407
|
-
for (const report of reports) {
|
|
2408
|
-
for (const finding of report.findings) {
|
|
2409
|
-
mergeFinding(findingGroups, finding, report, threshold);
|
|
2410
|
-
}
|
|
2411
|
-
for (const text of report.recommendations) {
|
|
2412
|
-
mergeRecommendation(recommendationGroups, text, report, threshold);
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
const sources = [...new Set(reports.map((r) => r.agent))].sort();
|
|
2416
|
-
const totalRawFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
2417
|
-
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).`;
|
|
2418
|
-
return {
|
|
2419
|
-
id: `combined-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`,
|
|
2420
|
-
title: options.objective !== void 0 ? truncate2(options.objective, 120) : "Combined report",
|
|
2421
|
-
...options.objective !== void 0 ? { objective: options.objective } : {},
|
|
2422
|
-
...options.pipelineRunId !== void 0 ? { pipelineRunId: options.pipelineRunId } : {},
|
|
2423
|
-
taskIds: options.taskIds ?? [],
|
|
2424
|
-
summary,
|
|
2425
|
-
findings: findingGroups.sort((a, b) => rank(b) - rank(a)),
|
|
2426
|
-
recommendations: recommendationGroups,
|
|
2427
|
-
sources,
|
|
2428
|
-
reportIds: reports.map((r) => r.id),
|
|
2429
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
function rank(finding) {
|
|
2433
|
-
return finding.severity ? SEVERITY_RANK2[finding.severity] : -1;
|
|
2434
|
-
}
|
|
2435
|
-
function mergeFinding(groups, finding, report, threshold) {
|
|
2436
|
-
const source = { agent: report.agent, reportId: report.id };
|
|
2437
|
-
const group = groups.find((g) => similar(g.title, finding.title, threshold));
|
|
2438
|
-
if (group) {
|
|
2439
|
-
group.occurrences += 1;
|
|
2440
|
-
group.details.push(finding.detail);
|
|
2441
|
-
group.sources.push(source);
|
|
2442
|
-
group.evidence.push(...finding.evidence ?? []);
|
|
2443
|
-
if (finding.severity && (!group.severity || SEVERITY_RANK2[finding.severity] > SEVERITY_RANK2[group.severity])) {
|
|
2444
|
-
group.severity = finding.severity;
|
|
2445
|
-
}
|
|
2446
|
-
return;
|
|
2447
|
-
}
|
|
2448
|
-
groups.push({
|
|
2449
|
-
title: finding.title,
|
|
2450
|
-
details: [finding.detail],
|
|
2451
|
-
sources: [source],
|
|
2452
|
-
occurrences: 1,
|
|
2453
|
-
...finding.severity ? { severity: finding.severity } : {},
|
|
2454
|
-
evidence: [...finding.evidence ?? []]
|
|
2455
|
-
});
|
|
2456
|
-
}
|
|
2457
|
-
function mergeRecommendation(groups, text, report, threshold) {
|
|
2458
|
-
const source = { agent: report.agent, reportId: report.id };
|
|
2459
|
-
const group = groups.find((g) => similar(g.text, text, threshold));
|
|
2460
|
-
if (group) {
|
|
2461
|
-
group.occurrences += 1;
|
|
2462
|
-
group.sources.push(source);
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
groups.push({ text, sources: [source], occurrences: 1 });
|
|
2466
|
-
}
|
|
2467
|
-
function similar(a, b, threshold) {
|
|
2468
|
-
const left = tokens(a);
|
|
2469
|
-
const right = tokens(b);
|
|
2470
|
-
if (left.size === 0 || right.size === 0) return false;
|
|
2471
|
-
let intersection = 0;
|
|
2472
|
-
for (const token of left) {
|
|
2473
|
-
if (right.has(token)) intersection += 1;
|
|
2474
|
-
}
|
|
2475
|
-
const union = left.size + right.size - intersection;
|
|
2476
|
-
return intersection / union >= threshold;
|
|
2477
|
-
}
|
|
2478
|
-
function tokens(text) {
|
|
2479
|
-
return new Set(
|
|
2480
|
-
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3)
|
|
2481
|
-
);
|
|
2482
|
-
}
|
|
2483
|
-
function truncate2(text, max) {
|
|
2484
|
-
return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
4187
|
// src/reports/extract.ts
|
|
2488
4188
|
var BULLET = /^(?:[-*•]|\d+[.)])\s+(.*)$/;
|
|
2489
4189
|
function parseSections(raw) {
|
|
@@ -2533,6 +4233,7 @@ function extractReportFromText(raw, meta) {
|
|
|
2533
4233
|
id: `${meta.taskId}-f-${index + 1}`,
|
|
2534
4234
|
title: truncate3(text, 120),
|
|
2535
4235
|
detail: text,
|
|
4236
|
+
description: text,
|
|
2536
4237
|
severity: guessSeverity(text)
|
|
2537
4238
|
}));
|
|
2538
4239
|
return {
|
|
@@ -2543,7 +4244,8 @@ function extractReportFromText(raw, meta) {
|
|
|
2543
4244
|
summary,
|
|
2544
4245
|
findings,
|
|
2545
4246
|
recommendations: recommendationTexts.map((t) => truncate3(t, 200)),
|
|
2546
|
-
|
|
4247
|
+
...evidenceTexts.length > 0 ? { evidence: evidenceTexts.map((excerpt) => ({ kind: "observation", excerpt })) } : {},
|
|
4248
|
+
metadata: evidenceTexts.length > 0 ? { evidenceCount: evidenceTexts.length } : void 0,
|
|
2547
4249
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2548
4250
|
};
|
|
2549
4251
|
}
|
|
@@ -2552,6 +4254,7 @@ function truncate3(text, max) {
|
|
|
2552
4254
|
}
|
|
2553
4255
|
|
|
2554
4256
|
// src/reports/service.ts
|
|
4257
|
+
var MAX_REPORT_BYTES = 256 * 1024;
|
|
2555
4258
|
var ReportService = class {
|
|
2556
4259
|
constructor(store, log = logger) {
|
|
2557
4260
|
this.store = store;
|
|
@@ -2559,27 +4262,56 @@ var ReportService = class {
|
|
|
2559
4262
|
}
|
|
2560
4263
|
store;
|
|
2561
4264
|
log;
|
|
4265
|
+
/** Rejected reports from the last collection (§13 — malformed never enters aggregation). */
|
|
4266
|
+
lastDiagnostics = { collected: 0, accepted: 0, rejected: [] };
|
|
2562
4267
|
get storeRef() {
|
|
2563
4268
|
return this.store;
|
|
2564
4269
|
}
|
|
2565
|
-
/** Normalizes finished tasks into reports (§17 — normalizer)
|
|
4270
|
+
/** Normalizes finished tasks into reports (§17 — normalizer), validating each
|
|
4271
|
+
* one so malformed reports never reach aggregation (§13). */
|
|
2566
4272
|
async collectFromTasks(tasks) {
|
|
2567
|
-
const
|
|
4273
|
+
const candidates = [];
|
|
2568
4274
|
for (const task of tasks) {
|
|
2569
4275
|
const structured = task.result?.reports ?? [];
|
|
2570
4276
|
if (structured.length > 0) {
|
|
2571
|
-
|
|
4277
|
+
candidates.push(...structured);
|
|
2572
4278
|
continue;
|
|
2573
4279
|
}
|
|
2574
4280
|
const text = task.result?.rawOutput ?? task.result?.summary ?? "";
|
|
2575
4281
|
if (text.trim().length === 0) continue;
|
|
2576
|
-
|
|
4282
|
+
candidates.push(
|
|
2577
4283
|
extractReportFromText(text, {
|
|
2578
4284
|
taskId: task.id,
|
|
2579
4285
|
agent: task.agent ?? "unknown"
|
|
2580
4286
|
})
|
|
2581
4287
|
);
|
|
2582
4288
|
}
|
|
4289
|
+
const reports = [];
|
|
4290
|
+
const rejected = [];
|
|
4291
|
+
for (const candidate of candidates) {
|
|
4292
|
+
const size = Buffer.byteLength(JSON.stringify(candidate), "utf8");
|
|
4293
|
+
if (size > MAX_REPORT_BYTES) {
|
|
4294
|
+
rejected.push({
|
|
4295
|
+
key: candidate?.id ?? "(unknown)",
|
|
4296
|
+
reason: `report exceeds the ${MAX_REPORT_BYTES} byte limit (${size} bytes)`,
|
|
4297
|
+
issues: []
|
|
4298
|
+
});
|
|
4299
|
+
this.log.warn(`Rejected oversized report (${size} bytes).`);
|
|
4300
|
+
continue;
|
|
4301
|
+
}
|
|
4302
|
+
const result = validateReport(candidate);
|
|
4303
|
+
if (result.valid) {
|
|
4304
|
+
reports.push(result.report);
|
|
4305
|
+
} else {
|
|
4306
|
+
rejected.push({
|
|
4307
|
+
key: candidate?.id ?? "(unknown)",
|
|
4308
|
+
reason: result.reason,
|
|
4309
|
+
issues: result.issues.map((issue) => `${issue.path}: ${issue.message}`)
|
|
4310
|
+
});
|
|
4311
|
+
this.log.warn(`Rejected malformed report: ${result.reason}`);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
this.lastDiagnostics = { collected: candidates.length, accepted: reports.length, rejected };
|
|
2583
4315
|
return reports;
|
|
2584
4316
|
}
|
|
2585
4317
|
async saveReports(reports) {
|
|
@@ -2640,14 +4372,19 @@ var FileReportStore = class extends JsonFileStore {
|
|
|
2640
4372
|
|
|
2641
4373
|
// src/pipelines/engine-factory.ts
|
|
2642
4374
|
function buildPipelineEngine(config, log = logger) {
|
|
4375
|
+
const auditLog = new FileAuditLog();
|
|
2643
4376
|
const agents = buildAgentRegistry(config, log);
|
|
2644
4377
|
const roles = buildRoleRegistry(config);
|
|
2645
|
-
const chat = createDefaultChatFn(config.project.providers);
|
|
4378
|
+
const chat = createDefaultChatFn(config.project.providers, { audit: auditLog });
|
|
2646
4379
|
const tasks = new TaskEngine({
|
|
2647
4380
|
agents,
|
|
2648
4381
|
roles,
|
|
2649
4382
|
store: new FileTaskStore(),
|
|
2650
|
-
...config.project.memory !== void 0 ? {
|
|
4383
|
+
...config.project.memory !== void 0 ? {
|
|
4384
|
+
memory: new MemoryService(createMemoryProvider(config.project.memory), log, {
|
|
4385
|
+
required: config.project.memory.required
|
|
4386
|
+
})
|
|
4387
|
+
} : {},
|
|
2651
4388
|
audit: new FileAuditLog(),
|
|
2652
4389
|
log
|
|
2653
4390
|
});
|
|
@@ -2683,14 +4420,68 @@ var ProjectStores = class {
|
|
|
2683
4420
|
this.audit = new FileAuditLog(directory);
|
|
2684
4421
|
}
|
|
2685
4422
|
};
|
|
4423
|
+
var STORE_FILES = [
|
|
4424
|
+
"tasks.json",
|
|
4425
|
+
"pipelines.json",
|
|
4426
|
+
"reports.json",
|
|
4427
|
+
"plans.json",
|
|
4428
|
+
"memory.json"
|
|
4429
|
+
];
|
|
4430
|
+
async function migrateProjectStores(directory = `${process.cwd()}/.omninode`, options = {}) {
|
|
4431
|
+
const reports = [];
|
|
4432
|
+
for (const file of STORE_FILES) {
|
|
4433
|
+
const path2 = `${directory}/${file}`;
|
|
4434
|
+
const state = await inspectDocument(path2);
|
|
4435
|
+
if (state.status === "missing") {
|
|
4436
|
+
reports.push({ file, state: state.status, migrated: false });
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4439
|
+
if (state.status === "current") {
|
|
4440
|
+
reports.push({ file, state: state.status, before: state.count, after: state.count, migrated: false });
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
if (state.status === "future") {
|
|
4444
|
+
reports.push({
|
|
4445
|
+
file,
|
|
4446
|
+
state: state.status,
|
|
4447
|
+
migrated: false,
|
|
4448
|
+
note: `written by a newer OmniNode (schema v${state.schemaVersion}); not touched`
|
|
4449
|
+
});
|
|
4450
|
+
continue;
|
|
4451
|
+
}
|
|
4452
|
+
if (state.status === "corrupt") {
|
|
4453
|
+
reports.push({ file, state: state.status, migrated: false, note: state.detail });
|
|
4454
|
+
continue;
|
|
4455
|
+
}
|
|
4456
|
+
const items = await readDocument(path2, void 0, false);
|
|
4457
|
+
if (!options.checkOnly) {
|
|
4458
|
+
await writeFileAtomic(path2, `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, items }, null, 2)}
|
|
4459
|
+
`);
|
|
4460
|
+
}
|
|
4461
|
+
const beforeCount = items.length;
|
|
4462
|
+
reports.push({
|
|
4463
|
+
file,
|
|
4464
|
+
state: state.status,
|
|
4465
|
+
before: beforeCount,
|
|
4466
|
+
after: items.length,
|
|
4467
|
+
migrated: !options.checkOnly,
|
|
4468
|
+
...options.checkOnly ? { note: "would migrate" } : {}
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
return reports;
|
|
4472
|
+
}
|
|
2686
4473
|
|
|
2687
4474
|
// src/version.ts
|
|
2688
|
-
var OMNINODE_VERSION = "2.0.0-alpha.
|
|
4475
|
+
var OMNINODE_VERSION = "2.0.0-alpha.20";
|
|
2689
4476
|
export {
|
|
4477
|
+
AgentAdapterRegistry,
|
|
2690
4478
|
AgentError,
|
|
2691
4479
|
AgentRegistry,
|
|
2692
4480
|
ConfigError,
|
|
2693
4481
|
ConsoleLogSink,
|
|
4482
|
+
DEFAULT_AUDIT_MAX_BYTES,
|
|
4483
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
4484
|
+
DEFAULT_MAX_PARALLEL_STEPS,
|
|
2694
4485
|
FileAuditLog,
|
|
2695
4486
|
FilePipelineRunStore,
|
|
2696
4487
|
FilePlanStore,
|
|
@@ -2700,6 +4491,11 @@ export {
|
|
|
2700
4491
|
JsonFileStore,
|
|
2701
4492
|
LocalMemoryProvider,
|
|
2702
4493
|
Logger,
|
|
4494
|
+
MAX_COMBINED_CONTEXT_CHARS,
|
|
4495
|
+
MAX_COMBINED_FINDINGS,
|
|
4496
|
+
MAX_CONTEXT_CHARS,
|
|
4497
|
+
MAX_MESSAGE_BYTES,
|
|
4498
|
+
MAX_REPORT_BYTES,
|
|
2703
4499
|
MemoryError,
|
|
2704
4500
|
MemoryService,
|
|
2705
4501
|
ModelPlanner,
|
|
@@ -2709,53 +4505,96 @@ export {
|
|
|
2709
4505
|
OmniHilbrasProvider,
|
|
2710
4506
|
OmniNodeError,
|
|
2711
4507
|
OpenAICompatibleProvider,
|
|
4508
|
+
PROTOCOL_V2,
|
|
2712
4509
|
PipelineEngine,
|
|
2713
4510
|
PipelineError,
|
|
2714
4511
|
PlannerError,
|
|
2715
4512
|
ProcessAgent,
|
|
4513
|
+
ProcessAgentAdapter,
|
|
2716
4514
|
ProjectStores,
|
|
4515
|
+
ProtocolDecoder,
|
|
2717
4516
|
ProtocolError,
|
|
4517
|
+
ProtocolSession,
|
|
2718
4518
|
ProviderError,
|
|
2719
4519
|
RememberaMemoryProvider,
|
|
2720
4520
|
ReportService,
|
|
2721
4521
|
RoleRegistry,
|
|
2722
|
-
|
|
4522
|
+
SCHEMA_VERSION,
|
|
4523
|
+
SUPPORTED_PROTOCOL_VERSIONS,
|
|
2723
4524
|
TaskEngine,
|
|
2724
4525
|
TaskError,
|
|
2725
4526
|
agentConfigSchema,
|
|
2726
4527
|
aggregateReports,
|
|
2727
4528
|
appConfigSchema,
|
|
2728
4529
|
authHeaders,
|
|
4530
|
+
budgetContext,
|
|
2729
4531
|
buildAgentRegistry,
|
|
2730
4532
|
buildChildEnv,
|
|
4533
|
+
buildHello,
|
|
4534
|
+
buildHelloAck,
|
|
2731
4535
|
buildPipelineEngine,
|
|
2732
4536
|
buildPlanner,
|
|
2733
4537
|
buildPlannerContext,
|
|
2734
4538
|
buildRoleRegistry,
|
|
2735
4539
|
composeTaskText,
|
|
4540
|
+
configDiagnostics,
|
|
4541
|
+
configureLogger,
|
|
2736
4542
|
createAgent,
|
|
2737
4543
|
createDefaultChatFn,
|
|
2738
4544
|
createMemoryProvider,
|
|
2739
4545
|
createProvider,
|
|
2740
4546
|
createTaskEngine,
|
|
4547
|
+
decodeMessage,
|
|
4548
|
+
defaultAgentAdapters,
|
|
2741
4549
|
defaultProjectConfigYaml,
|
|
4550
|
+
describeExit,
|
|
4551
|
+
describeSecretFindings,
|
|
4552
|
+
detectConflicts,
|
|
4553
|
+
encodeMessage,
|
|
4554
|
+
envelope,
|
|
2742
4555
|
expandEnvRefs,
|
|
4556
|
+
extractMessage,
|
|
2743
4557
|
extractReportFromText,
|
|
2744
4558
|
findConfigFile,
|
|
4559
|
+
inspectDocument,
|
|
4560
|
+
isNetworkError,
|
|
2745
4561
|
isOmniNodeError,
|
|
2746
|
-
|
|
4562
|
+
isRetryable,
|
|
4563
|
+
isSupportedProtocol,
|
|
4564
|
+
isTimeoutError,
|
|
4565
|
+
killTree,
|
|
4566
|
+
kindFromStatus,
|
|
2747
4567
|
loadConfig,
|
|
4568
|
+
loadConfigDetailed,
|
|
2748
4569
|
logger,
|
|
4570
|
+
migrateProjectStores,
|
|
4571
|
+
newMessageId,
|
|
4572
|
+
normalizeEvidence,
|
|
4573
|
+
normalizeFinding,
|
|
4574
|
+
parseHello,
|
|
2749
4575
|
parsePlanJson,
|
|
4576
|
+
parseRetryAfter,
|
|
2750
4577
|
pipelineConfigSchema,
|
|
2751
4578
|
pipelineStepConfigSchema,
|
|
4579
|
+
planJsonSchema,
|
|
2752
4580
|
plannerConfigSchema,
|
|
2753
4581
|
projectConfigSchema,
|
|
2754
4582
|
providerConfigSchema,
|
|
4583
|
+
providerHttpError,
|
|
4584
|
+
providerTransportError,
|
|
4585
|
+
readDocument,
|
|
2755
4586
|
registryKey,
|
|
2756
4587
|
requestJson,
|
|
2757
4588
|
resolveApiKey,
|
|
4589
|
+
resolveEnvPolicy,
|
|
2758
4590
|
roleConfigSchema,
|
|
2759
|
-
|
|
4591
|
+
sanitizeChunk,
|
|
4592
|
+
scanForInlineSecrets,
|
|
4593
|
+
similar,
|
|
4594
|
+
validateEnvelope,
|
|
4595
|
+
validatePlan,
|
|
4596
|
+
validateReport,
|
|
4597
|
+
validateWorkingDirectory,
|
|
4598
|
+
writeFileAtomic
|
|
2760
4599
|
};
|
|
2761
4600
|
//# sourceMappingURL=index.js.map
|