u-foo 1.2.16 → 1.3.0
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/modules/online/README.md +18 -0
- package/package.json +2 -1
- package/src/agent/cliRunner.js +1 -1
- package/src/agent/launcher.js +23 -4
- package/src/agent/ptyRunner.js +39 -16
- package/src/agent/ufooAgent.js +2 -1
- package/src/assistant/agent.js +2 -1
- package/src/assistant/bridge.js +9 -3
- package/src/assistant/constants.js +15 -0
- package/src/assistant/engine.js +7 -2
- package/src/assistant/ufooEngineCli.js +9 -3
- package/src/chat/commandExecutor.js +188 -13
- package/src/chat/commands.js +11 -0
- package/src/chat/daemonMessageRouter.js +107 -0
- package/src/cli/groupCoreCommands.js +246 -0
- package/src/cli/onlineCoreCommands.js +8 -0
- package/src/cli.js +325 -2
- package/src/daemon/groupOrchestrator.js +557 -0
- package/src/daemon/index.js +319 -1
- package/src/daemon/status.js +48 -0
- package/src/group/diagram.js +222 -0
- package/src/group/templates.js +280 -0
- package/src/group/validateTemplate.js +234 -0
- package/src/online/server.js +193 -14
- package/src/shared/eventContract.js +5 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/dev-basic.json +78 -0
- package/templates/groups/research-quick.json +49 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const TEMPLATE_SOURCE = {
|
|
8
|
+
BUILTIN: "builtin",
|
|
9
|
+
GLOBAL: "global",
|
|
10
|
+
PROJECT: "project",
|
|
11
|
+
PATH: "path",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const SOURCE_PRIORITY = [
|
|
15
|
+
TEMPLATE_SOURCE.BUILTIN,
|
|
16
|
+
TEMPLATE_SOURCE.GLOBAL,
|
|
17
|
+
TEMPLATE_SOURCE.PROJECT,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function defaultBuiltinTemplatesDir() {
|
|
21
|
+
return path.join(path.resolve(__dirname, "..", ".."), "templates", "groups");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultGlobalTemplatesDir() {
|
|
25
|
+
return path.join(os.homedir(), ".ufoo", "templates", "groups");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function defaultProjectTemplatesDir(projectRoot) {
|
|
29
|
+
return path.join(projectRoot, ".ufoo", "templates", "groups");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getTemplateDirs(projectRoot, options = {}) {
|
|
33
|
+
return {
|
|
34
|
+
builtinDir: options.builtinDir || defaultBuiltinTemplatesDir(),
|
|
35
|
+
globalDir: options.globalDir || defaultGlobalTemplatesDir(),
|
|
36
|
+
projectDir: options.projectDir || defaultProjectTemplatesDir(projectRoot),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function asTrimmedString(value) {
|
|
41
|
+
if (typeof value !== "string") return "";
|
|
42
|
+
return value.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isTemplateJsonFile(fileName = "") {
|
|
46
|
+
return String(fileName || "").toLowerCase().endsWith(".json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function templateAliasFromData(data, fallbackAlias) {
|
|
50
|
+
const alias = asTrimmedString(data && data.template && data.template.alias);
|
|
51
|
+
return alias || fallbackAlias;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseTemplateFile(filePath, source) {
|
|
55
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
entry: null,
|
|
62
|
+
error: {
|
|
63
|
+
source,
|
|
64
|
+
filePath,
|
|
65
|
+
error: err.message || String(err),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(raw);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
entry: null,
|
|
76
|
+
error: {
|
|
77
|
+
source,
|
|
78
|
+
filePath,
|
|
79
|
+
error: `invalid JSON: ${err.message || String(err)}`,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
85
|
+
return {
|
|
86
|
+
entry: null,
|
|
87
|
+
error: {
|
|
88
|
+
source,
|
|
89
|
+
filePath,
|
|
90
|
+
error: "template file must contain a JSON object",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const alias = templateAliasFromData(data, baseName);
|
|
96
|
+
const templateInfo = data.template && typeof data.template === "object" ? data.template : {};
|
|
97
|
+
const entry = {
|
|
98
|
+
alias,
|
|
99
|
+
source,
|
|
100
|
+
filePath,
|
|
101
|
+
data,
|
|
102
|
+
templateId: asTrimmedString(templateInfo.id),
|
|
103
|
+
templateName: asTrimmedString(templateInfo.name),
|
|
104
|
+
schemaVersion: Number.isInteger(data.schema_version) ? data.schema_version : null,
|
|
105
|
+
};
|
|
106
|
+
return { entry, error: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatTemplateLoadErrors(errors = []) {
|
|
110
|
+
if (!Array.isArray(errors) || errors.length === 0) return "";
|
|
111
|
+
return errors
|
|
112
|
+
.map((item) => `${item.filePath}: ${item.error}`)
|
|
113
|
+
.join("; ");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loadTemplatesFromDir(dirPath, source) {
|
|
117
|
+
if (!dirPath || !fs.existsSync(dirPath)) {
|
|
118
|
+
return { entries: [], errors: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const entries = [];
|
|
122
|
+
const errors = [];
|
|
123
|
+
const files = fs
|
|
124
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
125
|
+
.filter((entry) => entry.isFile() && isTemplateJsonFile(entry.name))
|
|
126
|
+
.map((entry) => entry.name)
|
|
127
|
+
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
128
|
+
|
|
129
|
+
for (const fileName of files) {
|
|
130
|
+
const filePath = path.join(dirPath, fileName);
|
|
131
|
+
const parsed = parseTemplateFile(filePath, source);
|
|
132
|
+
if (parsed.error) {
|
|
133
|
+
errors.push(parsed.error);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
entries.push(parsed.entry);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { entries, errors };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function loadTemplateRegistry(projectRoot, options = {}) {
|
|
143
|
+
const dirs = getTemplateDirs(projectRoot, options);
|
|
144
|
+
const dirBySource = {
|
|
145
|
+
[TEMPLATE_SOURCE.BUILTIN]: dirs.builtinDir,
|
|
146
|
+
[TEMPLATE_SOURCE.GLOBAL]: dirs.globalDir,
|
|
147
|
+
[TEMPLATE_SOURCE.PROJECT]: dirs.projectDir,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const byAlias = new Map();
|
|
151
|
+
const errors = [];
|
|
152
|
+
|
|
153
|
+
for (const source of SOURCE_PRIORITY) {
|
|
154
|
+
const dirPath = dirBySource[source];
|
|
155
|
+
const loaded = loadTemplatesFromDir(dirPath, source);
|
|
156
|
+
errors.push(...loaded.errors);
|
|
157
|
+
for (const entry of loaded.entries) {
|
|
158
|
+
byAlias.set(entry.alias, entry);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const templates = Array.from(byAlias.values())
|
|
163
|
+
.sort((a, b) => a.alias.localeCompare(b.alias, "en", { sensitivity: "base" }));
|
|
164
|
+
|
|
165
|
+
return { templates, byAlias, errors, dirs };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isLikelyPathReference(reference = "") {
|
|
169
|
+
const value = String(reference || "").trim();
|
|
170
|
+
if (!value) return false;
|
|
171
|
+
return value.includes("/") || value.includes("\\") || value.endsWith(".json") || value.startsWith(".");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveTemplateReference(projectRoot, reference, options = {}) {
|
|
175
|
+
const value = asTrimmedString(reference);
|
|
176
|
+
if (!value) {
|
|
177
|
+
return { entry: null, errors: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const cwd = options.cwd || process.cwd();
|
|
181
|
+
const allowPath = options.allowPath !== false;
|
|
182
|
+
|
|
183
|
+
if (allowPath && isLikelyPathReference(value)) {
|
|
184
|
+
const candidates = [];
|
|
185
|
+
if (path.isAbsolute(value)) {
|
|
186
|
+
candidates.push(value);
|
|
187
|
+
} else {
|
|
188
|
+
candidates.push(path.resolve(cwd, value));
|
|
189
|
+
const fallback = path.resolve(projectRoot, value);
|
|
190
|
+
if (!candidates.includes(fallback)) candidates.push(fallback);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const candidate of candidates) {
|
|
194
|
+
if (!fs.existsSync(candidate)) continue;
|
|
195
|
+
const parsed = parseTemplateFile(candidate, TEMPLATE_SOURCE.PATH);
|
|
196
|
+
return {
|
|
197
|
+
entry: parsed.entry,
|
|
198
|
+
errors: parsed.error ? [parsed.error] : [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const registry = loadTemplateRegistry(projectRoot, options);
|
|
204
|
+
return {
|
|
205
|
+
entry: registry.byAlias.get(value) || null,
|
|
206
|
+
errors: registry.errors,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeTemplateAlias(alias = "") {
|
|
211
|
+
const value = asTrimmedString(alias);
|
|
212
|
+
if (!value) return "";
|
|
213
|
+
const valid = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value);
|
|
214
|
+
return valid ? value : "";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function deepClone(value) {
|
|
218
|
+
return JSON.parse(JSON.stringify(value));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function createTemplateFromBuiltin(projectRoot, alias, fromAlias, options = {}) {
|
|
222
|
+
const nextAlias = normalizeTemplateAlias(alias);
|
|
223
|
+
if (!nextAlias) {
|
|
224
|
+
throw new Error("alias must match /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sourceAlias = asTrimmedString(fromAlias);
|
|
228
|
+
if (!sourceAlias) {
|
|
229
|
+
throw new Error("from alias is required");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const dirs = getTemplateDirs(projectRoot, options);
|
|
233
|
+
const builtinLoaded = loadTemplatesFromDir(dirs.builtinDir, TEMPLATE_SOURCE.BUILTIN);
|
|
234
|
+
const sourceTemplate = builtinLoaded.entries.find((entry) => entry.alias === sourceAlias);
|
|
235
|
+
if (!sourceTemplate) {
|
|
236
|
+
const details = formatTemplateLoadErrors(builtinLoaded.errors);
|
|
237
|
+
if (details) {
|
|
238
|
+
throw new Error(`builtin template not found: ${sourceAlias}; failed to load builtin templates: ${details}`);
|
|
239
|
+
}
|
|
240
|
+
throw new Error(`builtin template not found: ${sourceAlias}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const targetScope = options.scope === "global" ? "global" : "project";
|
|
244
|
+
const targetDir = targetScope === "global" ? dirs.globalDir : dirs.projectDir;
|
|
245
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
246
|
+
|
|
247
|
+
const targetPath = path.join(targetDir, `${nextAlias}.json`);
|
|
248
|
+
if (fs.existsSync(targetPath) && !options.force) {
|
|
249
|
+
throw new Error(`template already exists: ${targetPath} (use --force to overwrite)`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const nextData = deepClone(sourceTemplate.data);
|
|
253
|
+
nextData.template = nextData.template && typeof nextData.template === "object"
|
|
254
|
+
? nextData.template
|
|
255
|
+
: {};
|
|
256
|
+
nextData.template.alias = nextAlias;
|
|
257
|
+
if (!asTrimmedString(nextData.template.id)) nextData.template.id = nextAlias;
|
|
258
|
+
if (!asTrimmedString(nextData.template.name)) nextData.template.name = nextAlias;
|
|
259
|
+
|
|
260
|
+
fs.writeFileSync(targetPath, `${JSON.stringify(nextData, null, 2)}\n`, "utf8");
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
alias: nextAlias,
|
|
264
|
+
from: sourceAlias,
|
|
265
|
+
scope: targetScope,
|
|
266
|
+
filePath: targetPath,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
TEMPLATE_SOURCE,
|
|
272
|
+
defaultBuiltinTemplatesDir,
|
|
273
|
+
defaultGlobalTemplatesDir,
|
|
274
|
+
defaultProjectTemplatesDir,
|
|
275
|
+
getTemplateDirs,
|
|
276
|
+
loadTemplateRegistry,
|
|
277
|
+
resolveTemplateReference,
|
|
278
|
+
createTemplateFromBuiltin,
|
|
279
|
+
normalizeTemplateAlias,
|
|
280
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const ALLOWED_AGENT_TYPES = new Set(["codex", "claude", "ucode"]);
|
|
4
|
+
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function asTrimmedString(value) {
|
|
10
|
+
if (typeof value !== "string") return "";
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function addError(errors, path, message) {
|
|
15
|
+
errors.push({ path, message });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateReferenceArray({
|
|
19
|
+
errors,
|
|
20
|
+
fieldName,
|
|
21
|
+
fieldValue,
|
|
22
|
+
pathPrefix,
|
|
23
|
+
knownNicknames,
|
|
24
|
+
currentNickname,
|
|
25
|
+
}) {
|
|
26
|
+
if (fieldValue === undefined) return;
|
|
27
|
+
if (!Array.isArray(fieldValue)) {
|
|
28
|
+
addError(errors, pathPrefix, `${fieldName} must be an array`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (let i = 0; i < fieldValue.length; i += 1) {
|
|
32
|
+
const value = asTrimmedString(fieldValue[i]);
|
|
33
|
+
const valuePath = `${pathPrefix}[${i}]`;
|
|
34
|
+
if (!value) {
|
|
35
|
+
addError(errors, valuePath, `${fieldName} entry must be a non-empty string`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (fieldName === "depends_on" && currentNickname && value === currentNickname) {
|
|
39
|
+
addError(errors, valuePath, "depends_on cannot reference itself");
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!knownNicknames.has(value)) {
|
|
43
|
+
addError(errors, valuePath, `${fieldName} reference "${value}" does not exist`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectDependsCycle(agents = []) {
|
|
49
|
+
const deps = new Map();
|
|
50
|
+
const nicknameOrder = [];
|
|
51
|
+
|
|
52
|
+
for (const agent of agents) {
|
|
53
|
+
const nickname = asTrimmedString(agent && agent.nickname);
|
|
54
|
+
if (!nickname) continue;
|
|
55
|
+
nicknameOrder.push(nickname);
|
|
56
|
+
const dependsOn = Array.isArray(agent.depends_on)
|
|
57
|
+
? agent.depends_on.map((item) => asTrimmedString(item)).filter(Boolean)
|
|
58
|
+
: [];
|
|
59
|
+
deps.set(nickname, dependsOn);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const state = new Map();
|
|
63
|
+
const stack = [];
|
|
64
|
+
|
|
65
|
+
function dfs(node) {
|
|
66
|
+
const seen = state.get(node) || 0;
|
|
67
|
+
if (seen === 1) {
|
|
68
|
+
const idx = stack.indexOf(node);
|
|
69
|
+
const cycle = idx >= 0 ? stack.slice(idx).concat(node) : [node, node];
|
|
70
|
+
return cycle;
|
|
71
|
+
}
|
|
72
|
+
if (seen === 2) return null;
|
|
73
|
+
|
|
74
|
+
state.set(node, 1);
|
|
75
|
+
stack.push(node);
|
|
76
|
+
|
|
77
|
+
const neighbors = deps.get(node) || [];
|
|
78
|
+
for (const neighbor of neighbors) {
|
|
79
|
+
if (!deps.has(neighbor)) continue;
|
|
80
|
+
const cycle = dfs(neighbor);
|
|
81
|
+
if (cycle) return cycle;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
stack.pop();
|
|
85
|
+
state.set(node, 2);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const nickname of nicknameOrder) {
|
|
90
|
+
const cycle = dfs(nickname);
|
|
91
|
+
if (cycle) return cycle;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateTemplate(doc) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
|
|
99
|
+
if (!isPlainObject(doc)) {
|
|
100
|
+
addError(errors, "$", "template document must be a JSON object");
|
|
101
|
+
return { ok: false, errors };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!Number.isInteger(doc.schema_version) || doc.schema_version < 1) {
|
|
105
|
+
addError(errors, "schema_version", "schema_version must be an integer >= 1");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!isPlainObject(doc.template)) {
|
|
109
|
+
addError(errors, "template", "template must be an object");
|
|
110
|
+
} else {
|
|
111
|
+
if (!asTrimmedString(doc.template.id)) {
|
|
112
|
+
addError(errors, "template.id", "template.id is required");
|
|
113
|
+
}
|
|
114
|
+
if (!asTrimmedString(doc.template.alias)) {
|
|
115
|
+
addError(errors, "template.alias", "template.alias is required");
|
|
116
|
+
}
|
|
117
|
+
if (!asTrimmedString(doc.template.name)) {
|
|
118
|
+
addError(errors, "template.name", "template.name is required");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!Array.isArray(doc.agents) || doc.agents.length === 0) {
|
|
123
|
+
addError(errors, "agents", "agents must be a non-empty array");
|
|
124
|
+
return { ok: false, errors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const knownNicknames = new Set();
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < doc.agents.length; i += 1) {
|
|
130
|
+
const agent = doc.agents[i];
|
|
131
|
+
const basePath = `agents[${i}]`;
|
|
132
|
+
|
|
133
|
+
if (!isPlainObject(agent)) {
|
|
134
|
+
addError(errors, basePath, "agent must be an object");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const nickname = asTrimmedString(agent.nickname);
|
|
139
|
+
if (!nickname) {
|
|
140
|
+
addError(errors, `${basePath}.nickname`, "nickname is required");
|
|
141
|
+
} else if (knownNicknames.has(nickname)) {
|
|
142
|
+
addError(errors, `${basePath}.nickname`, `duplicate nickname "${nickname}"`);
|
|
143
|
+
} else {
|
|
144
|
+
knownNicknames.add(nickname);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const agentType = asTrimmedString(agent.type);
|
|
148
|
+
if (!ALLOWED_AGENT_TYPES.has(agentType)) {
|
|
149
|
+
addError(
|
|
150
|
+
errors,
|
|
151
|
+
`${basePath}.type`,
|
|
152
|
+
`type must be one of: ${Array.from(ALLOWED_AGENT_TYPES).join(", ")}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!Number.isInteger(agent.startup_order) || agent.startup_order < 0) {
|
|
157
|
+
addError(errors, `${basePath}.startup_order`, "startup_order must be an integer >= 0");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < doc.agents.length; i += 1) {
|
|
162
|
+
const agent = doc.agents[i];
|
|
163
|
+
const basePath = `agents[${i}]`;
|
|
164
|
+
if (!isPlainObject(agent)) continue;
|
|
165
|
+
|
|
166
|
+
const nickname = asTrimmedString(agent.nickname);
|
|
167
|
+
validateReferenceArray({
|
|
168
|
+
errors,
|
|
169
|
+
fieldName: "depends_on",
|
|
170
|
+
fieldValue: agent.depends_on,
|
|
171
|
+
pathPrefix: `${basePath}.depends_on`,
|
|
172
|
+
knownNicknames,
|
|
173
|
+
currentNickname: nickname,
|
|
174
|
+
});
|
|
175
|
+
validateReferenceArray({
|
|
176
|
+
errors,
|
|
177
|
+
fieldName: "accept_from",
|
|
178
|
+
fieldValue: agent.accept_from,
|
|
179
|
+
pathPrefix: `${basePath}.accept_from`,
|
|
180
|
+
knownNicknames,
|
|
181
|
+
currentNickname: nickname,
|
|
182
|
+
});
|
|
183
|
+
validateReferenceArray({
|
|
184
|
+
errors,
|
|
185
|
+
fieldName: "report_to",
|
|
186
|
+
fieldValue: agent.report_to,
|
|
187
|
+
pathPrefix: `${basePath}.report_to`,
|
|
188
|
+
knownNicknames,
|
|
189
|
+
currentNickname: nickname,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (doc.edges !== undefined && !Array.isArray(doc.edges)) {
|
|
194
|
+
addError(errors, "edges", "edges must be an array when provided");
|
|
195
|
+
} else if (Array.isArray(doc.edges)) {
|
|
196
|
+
for (let i = 0; i < doc.edges.length; i += 1) {
|
|
197
|
+
const edge = doc.edges[i];
|
|
198
|
+
const basePath = `edges[${i}]`;
|
|
199
|
+
if (!isPlainObject(edge)) {
|
|
200
|
+
addError(errors, basePath, "edge must be an object");
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const from = asTrimmedString(edge.from);
|
|
205
|
+
const to = asTrimmedString(edge.to);
|
|
206
|
+
if (!from) {
|
|
207
|
+
addError(errors, `${basePath}.from`, "from is required");
|
|
208
|
+
} else if (!knownNicknames.has(from)) {
|
|
209
|
+
addError(errors, `${basePath}.from`, `edge source "${from}" does not exist`);
|
|
210
|
+
}
|
|
211
|
+
if (!to) {
|
|
212
|
+
addError(errors, `${basePath}.to`, "to is required");
|
|
213
|
+
} else if (!knownNicknames.has(to)) {
|
|
214
|
+
addError(errors, `${basePath}.to`, `edge target "${to}" does not exist`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const cycle = detectDependsCycle(doc.agents);
|
|
220
|
+
if (cycle) {
|
|
221
|
+
addError(
|
|
222
|
+
errors,
|
|
223
|
+
"agents[*].depends_on",
|
|
224
|
+
`cyclic depends_on detected: ${cycle.join(" -> ")}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { ok: errors.length === 0, errors };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
ALLOWED_AGENT_TYPES,
|
|
233
|
+
validateTemplate,
|
|
234
|
+
};
|