@ariaflowagents/config 0.2.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/dist/loader.js ADDED
@@ -0,0 +1,1109 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { pathToFileURL } from 'url';
6
+ import fg from 'fast-glob';
7
+ import matter from 'gray-matter';
8
+ import { parse as parseJsonc } from 'jsonc-parser';
9
+ import { minimatch } from 'minimatch';
10
+ import { tool as aiTool } from 'ai';
11
+ import { z } from 'zod';
12
+ import { createRuntime, } from '@ariaflowagents/core';
13
+ let _warnings = [];
14
+ let _logger = null;
15
+ let _silent = false;
16
+ function initObservability(options) {
17
+ _warnings = [];
18
+ _logger = options.logger ?? null;
19
+ _silent = options.silent ?? false;
20
+ }
21
+ function warn(type, file, message, severity = 'warn') {
22
+ const warning = { type, file, message, severity };
23
+ _warnings.push(warning);
24
+ if (_logger) {
25
+ _logger(severity, message, { type, file });
26
+ }
27
+ else if (!_silent) {
28
+ const prefix = severity === 'error' ? '❌' : '⚠️';
29
+ console[severity === 'error' ? 'error' : 'warn'](`${prefix} ${message} (${file})`);
30
+ }
31
+ }
32
+ function getWarnings() {
33
+ return [..._warnings];
34
+ }
35
+ function clearWarnings() {
36
+ _warnings = [];
37
+ }
38
+ // ============================================
39
+ const toolJsonSchema = z.object({
40
+ name: z.string().min(1),
41
+ description: z.string().min(1),
42
+ type: z.enum(['builtin', 'module', 'skill']),
43
+ entry: z.string().optional(),
44
+ inputSchema: z.record(z.unknown()).optional(),
45
+ outputSchema: z.record(z.unknown()).optional(),
46
+ timeoutMs: z.number().optional(),
47
+ metadata: z.record(z.string()).optional(),
48
+ });
49
+ const skillFrontmatterSchema = z.object({
50
+ name: z.string().min(1),
51
+ description: z.string().min(1),
52
+ license: z.string().optional(),
53
+ metadata: z.record(z.string()).optional(),
54
+ });
55
+ const triagePromptTemplate = (basePrompt, routes, defaultAgent) => {
56
+ const routeDescriptions = routes
57
+ .map(route => `- **${route.agentId}**: ${route.description}`)
58
+ .join('\n');
59
+ const defaultNote = defaultAgent
60
+ ? `\n- Use "${defaultAgent}" when no specialist applies.`
61
+ : '';
62
+ return `${basePrompt}\n\n## Available Specialists\n${routeDescriptions}\n\n## Instructions\n- For general questions, answer directly\n- When the customer needs specialized help, use the handoff tool\n- Always provide a brief reason for the handoff${defaultNote}`;
63
+ };
64
+ function log(logger, level, message, meta) {
65
+ logger?.(level, message, meta);
66
+ }
67
+ function parseJsoncContent(content, sourcePath) {
68
+ const errors = [];
69
+ const parsed = parseJsonc(content, errors);
70
+ if (errors.length > 0) {
71
+ const message = errors.map(err => `Offset ${err.offset}: ${err.error}`).join(', ');
72
+ throw new Error(`Failed to parse JSONC (${sourcePath}): ${message}`);
73
+ }
74
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
75
+ throw new Error(`Config file ${sourcePath} must be a JSON object`);
76
+ }
77
+ return parsed;
78
+ }
79
+ async function readJsoncFile(filePath) {
80
+ const content = await readFile(filePath, 'utf8');
81
+ return parseJsoncContent(content, filePath);
82
+ }
83
+ function toArray(value) {
84
+ if (!value) {
85
+ return [];
86
+ }
87
+ return Array.isArray(value) ? value : [value];
88
+ }
89
+ function deepMerge(base, override) {
90
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
91
+ return override ?? base;
92
+ }
93
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
94
+ return override;
95
+ }
96
+ const result = { ...base };
97
+ for (const [key, value] of Object.entries(override)) {
98
+ const baseValue = base[key];
99
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
100
+ result[key] = deepMerge(baseValue, value);
101
+ }
102
+ else {
103
+ result[key] = value;
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+ function resolveConfigFile(target, baseDir) {
109
+ const resolved = path.isAbsolute(target) ? target : path.resolve(baseDir, target);
110
+ if (existsSync(resolved) && !resolved.endsWith('.json') && !resolved.endsWith('.jsonc')) {
111
+ const jsonc = path.join(resolved, 'ariaflow.jsonc');
112
+ const json = path.join(resolved, 'ariaflow.json');
113
+ if (existsSync(jsonc)) {
114
+ return jsonc;
115
+ }
116
+ if (existsSync(json)) {
117
+ return json;
118
+ }
119
+ }
120
+ return resolved;
121
+ }
122
+ async function loadConfigFile(configPath, visited = new Set()) {
123
+ const resolvedPath = path.resolve(configPath);
124
+ if (visited.has(resolvedPath)) {
125
+ throw new Error(`Detected circular config extends reference at ${resolvedPath}`);
126
+ }
127
+ if (!existsSync(resolvedPath)) {
128
+ throw new Error(`Config file not found: ${resolvedPath}`);
129
+ }
130
+ visited.add(resolvedPath);
131
+ const config = await readJsoncFile(resolvedPath);
132
+ const layers = [];
133
+ const extendsList = toArray(config.extends);
134
+ const baseDir = path.dirname(resolvedPath);
135
+ for (const extendTarget of extendsList) {
136
+ const extendPath = resolveConfigFile(extendTarget, baseDir);
137
+ const nestedLayers = await loadConfigFile(extendPath, visited);
138
+ layers.push(...nestedLayers);
139
+ }
140
+ const { extends: _ignored, ...rest } = config;
141
+ layers.push({ config: rest, dir: baseDir, path: resolvedPath });
142
+ return layers;
143
+ }
144
+ function findProjectRoot(start) {
145
+ let current = path.resolve(start);
146
+ while (true) {
147
+ const gitDir = path.join(current, '.git');
148
+ if (existsSync(gitDir)) {
149
+ return current;
150
+ }
151
+ const parent = path.dirname(current);
152
+ if (parent === current) {
153
+ return start;
154
+ }
155
+ current = parent;
156
+ }
157
+ }
158
+ function findProjectConfig(projectRoot) {
159
+ const jsonc = path.join(projectRoot, 'ariaflow.jsonc');
160
+ const json = path.join(projectRoot, 'ariaflow.json');
161
+ if (existsSync(jsonc)) {
162
+ return jsonc;
163
+ }
164
+ if (existsSync(json)) {
165
+ return json;
166
+ }
167
+ return null;
168
+ }
169
+ function findGlobalConfig() {
170
+ const configDir = path.join(os.homedir(), '.config', 'ariaflow');
171
+ const jsonc = path.join(configDir, 'ariaflow.jsonc');
172
+ const json = path.join(configDir, 'ariaflow.json');
173
+ if (existsSync(jsonc)) {
174
+ return jsonc;
175
+ }
176
+ if (existsSync(json)) {
177
+ return json;
178
+ }
179
+ return null;
180
+ }
181
+ function findAriaflowDirs(cwd, projectRoot) {
182
+ const dirs = [];
183
+ let current = path.resolve(cwd);
184
+ const root = path.resolve(projectRoot);
185
+ while (true) {
186
+ const candidate = path.join(current, '.ariaflow');
187
+ if (existsSync(candidate)) {
188
+ dirs.push(candidate);
189
+ }
190
+ if (current === root) {
191
+ break;
192
+ }
193
+ const parent = path.dirname(current);
194
+ if (parent === current) {
195
+ break;
196
+ }
197
+ current = parent;
198
+ }
199
+ return dirs.reverse();
200
+ }
201
+ async function loadMarkdownAgents(root, sources) {
202
+ const pattern = path.join(root, 'agent', '*.md');
203
+ const files = await fg(pattern, { onlyFiles: true, unique: true });
204
+ const agents = {};
205
+ for (const file of files) {
206
+ const content = await readFile(file, 'utf8');
207
+ const parsed = matter(content);
208
+ const id = path.basename(file, path.extname(file));
209
+ const data = parsed.data;
210
+ const type = data.type ?? 'llm';
211
+ const description = typeof data.description === 'string' ? data.description : undefined;
212
+ const name = typeof data.name === 'string' ? data.name : id;
213
+ const modelId = typeof data.model === 'string' ? data.model : undefined;
214
+ const tools = normalizeToolSelection(data.tools);
215
+ const mode = data.mode === 'hybrid'
216
+ ? 'hybrid'
217
+ : data.mode === 'strict'
218
+ ? 'strict'
219
+ : undefined;
220
+ const prompt = await resolvePrompt(data.prompt ?? data.systemPrompt ?? parsed.content, path.dirname(file));
221
+ if (!prompt) {
222
+ throw new Error(`Agent ${id} is missing a prompt (${file})`);
223
+ }
224
+ const baseAgent = {
225
+ id,
226
+ name,
227
+ description,
228
+ systemPrompt: prompt,
229
+ modelId,
230
+ toolNames: tools,
231
+ maxTurns: typeof data.maxTurns === 'number' ? data.maxTurns : undefined,
232
+ maxSteps: typeof data.maxSteps === 'number' ? data.maxSteps : undefined,
233
+ };
234
+ if (type === 'triage') {
235
+ const routes = Array.isArray(data.routes) ? data.routes : [];
236
+ const defaultAgent = typeof data.defaultAgent === 'string' ? data.defaultAgent : undefined;
237
+ const basePrompt = typeof data.systemPrompt === 'string' ? data.systemPrompt : prompt;
238
+ baseAgent.systemPrompt = basePrompt;
239
+ agents[id] = {
240
+ ...baseAgent,
241
+ type: 'triage',
242
+ routes,
243
+ defaultAgent,
244
+ };
245
+ }
246
+ else if (type === 'flow') {
247
+ if (!data.flow) {
248
+ throw new Error(`Flow agent ${id} is missing flow definition (${file})`);
249
+ }
250
+ const initialNode = typeof data.initialNode === 'string' ? data.initialNode : '';
251
+ if (!initialNode) {
252
+ throw new Error(`Flow agent ${id} is missing initialNode (${file})`);
253
+ }
254
+ agents[id] = {
255
+ ...baseAgent,
256
+ type: 'flow',
257
+ flow: data.flow,
258
+ initialNode,
259
+ mode,
260
+ };
261
+ }
262
+ else {
263
+ agents[id] = {
264
+ ...baseAgent,
265
+ type: 'llm',
266
+ };
267
+ }
268
+ sources.agentFiles.push(file);
269
+ }
270
+ return agents;
271
+ }
272
+ function normalizeToolSelection(value) {
273
+ if (!value) {
274
+ return undefined;
275
+ }
276
+ if (typeof value === 'string') {
277
+ return [value];
278
+ }
279
+ if (Array.isArray(value)) {
280
+ return value.filter(item => typeof item === 'string');
281
+ }
282
+ if (typeof value === 'object') {
283
+ return Object.entries(value)
284
+ .filter(([, enabled]) => Boolean(enabled))
285
+ .map(([name]) => name);
286
+ }
287
+ return undefined;
288
+ }
289
+ async function resolvePrompt(value, baseDir) {
290
+ if (!value) {
291
+ return null;
292
+ }
293
+ if (typeof value === 'string') {
294
+ return value.trim().length > 0 ? value : null;
295
+ }
296
+ if (typeof value === 'object' && value !== null && 'file' in value) {
297
+ const fileValue = value.file;
298
+ if (!fileValue) {
299
+ return null;
300
+ }
301
+ const resolvedPath = path.isAbsolute(fileValue)
302
+ ? fileValue
303
+ : path.resolve(baseDir, fileValue);
304
+ if (!existsSync(resolvedPath)) {
305
+ warn('missing_prompt', resolvedPath, `Prompt file not found`);
306
+ return null;
307
+ }
308
+ return readFile(resolvedPath, 'utf8');
309
+ }
310
+ return null;
311
+ }
312
+ async function loadFlowFiles(root, sources) {
313
+ const pattern = path.join(root, 'flow', '*.json');
314
+ const files = await fg(pattern, { onlyFiles: true, unique: true });
315
+ const flows = {};
316
+ const meta = {};
317
+ for (const file of files) {
318
+ const content = await readFile(file, 'utf8');
319
+ const data = parseJsoncContent(content, file);
320
+ const id = data.id ?? path.basename(file, path.extname(file));
321
+ const { id: _id, entry, initialNode, ...rest } = data;
322
+ const flowConfig = {
323
+ nodes: rest.nodes,
324
+ transitions: rest.transitions,
325
+ defaultRolePrompt: rest.defaultRolePrompt,
326
+ contextStrategy: rest.contextStrategy,
327
+ };
328
+ flows[id] = flowConfig;
329
+ meta[id] = {
330
+ id,
331
+ initialNode: initialNode ?? entry,
332
+ path: file,
333
+ };
334
+ sources.flowFiles.push(file);
335
+ }
336
+ return { flows, meta };
337
+ }
338
+ async function loadToolFiles(root, sources) {
339
+ const pattern = path.join(root, 'tools', '*', 'tool.json');
340
+ const files = await fg(pattern, { onlyFiles: true, unique: true });
341
+ const tools = {};
342
+ for (const file of files) {
343
+ const raw = await readJsoncFile(file);
344
+ const parsed = toolJsonSchema.safeParse(raw);
345
+ if (!parsed.success) {
346
+ throw new Error(`Invalid tool.json at ${file}: ${parsed.error.message}`);
347
+ }
348
+ const toolConfig = parsed.data;
349
+ const toolDir = path.dirname(file);
350
+ const toolName = toolConfig.name;
351
+ if (path.basename(toolDir) !== toolName) {
352
+ throw new Error(`Tool name mismatch at ${file}: expected ${path.basename(toolDir)} got ${toolName}`);
353
+ }
354
+ if (toolConfig.type !== 'module') {
355
+ continue;
356
+ }
357
+ if (!toolConfig.entry) {
358
+ throw new Error(`Tool ${toolName} is missing entry path (${file})`);
359
+ }
360
+ const entryPath = path.isAbsolute(toolConfig.entry)
361
+ ? toolConfig.entry
362
+ : path.resolve(toolDir, toolConfig.entry);
363
+ const toolExport = await import(pathToFileURL(entryPath).toString());
364
+ const toolInstance = normalizeToolExport(toolExport, toolName);
365
+ tools[toolName] = toolInstance;
366
+ sources.toolFiles.push(file);
367
+ }
368
+ return tools;
369
+ }
370
+ function normalizeToolExport(module, name) {
371
+ const candidate = (module.tool ?? module.default ?? module);
372
+ if (!candidate || typeof candidate !== 'object') {
373
+ throw new Error(`Tool module for ${name} did not export a tool`);
374
+ }
375
+ if ('execute' in candidate && 'inputSchema' in candidate) {
376
+ const execute = candidate.execute;
377
+ const inputSchema = candidate.inputSchema;
378
+ const description = candidate.description ?? `Tool ${name}`;
379
+ return aiTool({ description, inputSchema, execute });
380
+ }
381
+ if ('execute' in candidate && 'parameters' in candidate) {
382
+ return candidate;
383
+ }
384
+ throw new Error(`Tool module for ${name} is missing execute/inputSchema or parameters`);
385
+ }
386
+ async function parseSkillFile(file, sources) {
387
+ const content = await readFile(file, 'utf8');
388
+ const parsed = matter(content);
389
+ const frontmatter = skillFrontmatterSchema.safeParse(parsed.data ?? {});
390
+ if (!frontmatter.success) {
391
+ warn('invalid_frontmatter', file, `Invalid SKILL.md frontmatter: ${frontmatter.error.message}`);
392
+ }
393
+ const data = frontmatter.data ?? { name: 'unknown', description: 'Unknown skill' };
394
+ const name = data.name ?? path.basename(path.dirname(file));
395
+ const dirName = path.basename(path.dirname(file));
396
+ if (name !== dirName) {
397
+ warn('name_mismatch', file, `Skill name mismatch: expected "${dirName}" got "${name}"`);
398
+ }
399
+ sources.skillFiles.push(file);
400
+ return {
401
+ name,
402
+ description: data.description ?? 'No description',
403
+ license: data.license,
404
+ metadata: data.metadata,
405
+ content: parsed.content.trim(),
406
+ path: file,
407
+ };
408
+ }
409
+ async function loadSkills(root, sources) {
410
+ const pattern = path.join(root, 'skill', '*', 'SKILL.md');
411
+ const files = await fg(pattern, { onlyFiles: true, unique: true });
412
+ const skills = [];
413
+ for (const file of files) {
414
+ skills.push(await parseSkillFile(file, sources));
415
+ }
416
+ return skills;
417
+ }
418
+ async function loadSkillsFromPaths(pathsList, baseDir, sources) {
419
+ const skills = [];
420
+ for (const target of pathsList) {
421
+ const resolved = path.isAbsolute(target) ? target : path.resolve(baseDir, target);
422
+ const statPath = resolved.endsWith('SKILL.md') ? resolved : path.join(resolved, '*', 'SKILL.md');
423
+ const files = await fg(statPath, { onlyFiles: true, unique: true });
424
+ for (const file of files) {
425
+ skills.push(await parseSkillFile(file, sources));
426
+ }
427
+ }
428
+ return skills;
429
+ }
430
+ function applyPermissions(skills, permissions) {
431
+ const rule = permissions?.skill;
432
+ if (!rule) {
433
+ return [];
434
+ }
435
+ if (typeof rule === 'string') {
436
+ return rule === 'allow' ? skills : [];
437
+ }
438
+ return skills.filter(skill => {
439
+ const result = evaluatePermission(skill.name, rule);
440
+ return result === 'allow' || result === 'ask';
441
+ });
442
+ }
443
+ function evaluatePermission(name, rules) {
444
+ if (rules[name]) {
445
+ return rules[name];
446
+ }
447
+ for (const [pattern, value] of Object.entries(rules)) {
448
+ if (pattern === name) {
449
+ return value;
450
+ }
451
+ if (pattern.includes('*') && minimatch(name, pattern)) {
452
+ return value;
453
+ }
454
+ }
455
+ return 'deny';
456
+ }
457
+ function createSkillTool(skills) {
458
+ if (skills.length === 0) {
459
+ return aiTool({
460
+ description: 'No skills available.',
461
+ inputSchema: z.object({}),
462
+ execute: async () => ({ error: 'No skills available' }),
463
+ });
464
+ }
465
+ const names = skills.map(skill => skill.name);
466
+ const descriptions = skills
467
+ .map(skill => `- **${skill.name}**: ${skill.description}`)
468
+ .join('\n');
469
+ return aiTool({
470
+ description: `Load a reusable skill by name.\n\nAvailable skills:\n${descriptions}`,
471
+ inputSchema: z.object({
472
+ name: z.enum(names),
473
+ }),
474
+ execute: async ({ name }) => {
475
+ const skill = skills.find(item => item.name === name);
476
+ if (!skill) {
477
+ return { error: `Skill not found: ${name}` };
478
+ }
479
+ return {
480
+ name: skill.name,
481
+ description: skill.description,
482
+ license: skill.license,
483
+ metadata: skill.metadata,
484
+ content: skill.content,
485
+ };
486
+ },
487
+ });
488
+ }
489
+ function resolveModel(modelId, aliasMap, registry, defaultModel) {
490
+ if (!modelId) {
491
+ return defaultModel ?? registry?.default;
492
+ }
493
+ const alias = aliasMap?.[modelId] ?? modelId;
494
+ return registry?.[alias] ?? registry?.[modelId];
495
+ }
496
+ function buildToolSet(toolRegistry, selection) {
497
+ if (!selection || selection.length === 0) {
498
+ return undefined;
499
+ }
500
+ const tools = {};
501
+ const missing = [];
502
+ for (const name of selection) {
503
+ const toolInstance = toolRegistry[name];
504
+ if (toolInstance) {
505
+ tools[name] = toolInstance;
506
+ }
507
+ else {
508
+ missing.push(name);
509
+ }
510
+ }
511
+ if (missing.length > 0) {
512
+ warn('tool_not_found', missing.join(', '), `Tool(s) not found: ${missing.join(', ')}`);
513
+ }
514
+ return Object.keys(tools).length > 0 ? tools : undefined;
515
+ }
516
+ function resolveFlowNodeTools(value, toolRegistry) {
517
+ if (!value) {
518
+ return undefined;
519
+ }
520
+ if (Array.isArray(value)) {
521
+ const names = value.filter(item => typeof item === 'string');
522
+ return buildToolSet(toolRegistry, names);
523
+ }
524
+ if (typeof value === 'object') {
525
+ const entries = Object.entries(value);
526
+ const hasToolShape = entries.some(([, toolValue]) => {
527
+ if (!toolValue || typeof toolValue !== 'object') {
528
+ return false;
529
+ }
530
+ return 'execute' in toolValue || 'parameters' in toolValue;
531
+ });
532
+ if (hasToolShape) {
533
+ return value;
534
+ }
535
+ const enabledNames = entries
536
+ .filter(([, enabled]) => Boolean(enabled))
537
+ .map(([name]) => name);
538
+ return buildToolSet(toolRegistry, enabledNames);
539
+ }
540
+ return undefined;
541
+ }
542
+ function hydrateFlowTools(flow, toolRegistry) {
543
+ return {
544
+ ...flow,
545
+ nodes: flow.nodes.map(node => ({
546
+ ...node,
547
+ tools: resolveFlowNodeTools(node.tools, toolRegistry),
548
+ })),
549
+ };
550
+ }
551
+ async function loadToolConfigEntries(toolsConfig, baseDir, registry, builtinTools, skills, permissions, sources) {
552
+ if (!toolsConfig) {
553
+ return;
554
+ }
555
+ for (const [name, value] of Object.entries(toolsConfig)) {
556
+ if (value === false) {
557
+ delete registry[name];
558
+ continue;
559
+ }
560
+ if (value === true) {
561
+ continue;
562
+ }
563
+ const config = value;
564
+ if (config.type === 'builtin') {
565
+ const builtin = builtinTools?.[name];
566
+ if (!builtin) {
567
+ throw new Error(`Builtin tool "${name}" not found in registry`);
568
+ }
569
+ registry[name] = builtin;
570
+ continue;
571
+ }
572
+ if (config.type === 'module') {
573
+ if (!config.entry) {
574
+ throw new Error(`Tool "${name}" is missing entry path`);
575
+ }
576
+ const entryPath = path.isAbsolute(config.entry)
577
+ ? config.entry
578
+ : path.resolve(baseDir, config.entry);
579
+ const module = await import(pathToFileURL(entryPath).toString());
580
+ registry[name] = normalizeToolExport(module, name);
581
+ continue;
582
+ }
583
+ if (config.type === 'skill-loader') {
584
+ const skillSources = config.paths?.length
585
+ ? await loadSkillsFromPaths(config.paths, baseDir, sources)
586
+ : skills;
587
+ const allowedSkills = applyPermissions(skillSources, permissions);
588
+ registry[name] = createSkillTool(allowedSkills);
589
+ continue;
590
+ }
591
+ }
592
+ }
593
+ export async function loadAriaflowConfig(options = {}) {
594
+ const startTime = Date.now();
595
+ initObservability(options);
596
+ const cwd = options.cwd ?? process.cwd();
597
+ const configFiles = [];
598
+ const sources = {
599
+ configFiles,
600
+ agentFiles: [],
601
+ flowFiles: [],
602
+ toolFiles: [],
603
+ skillFiles: [],
604
+ };
605
+ const baseLayers = [];
606
+ const globalConfig = findGlobalConfig();
607
+ if (globalConfig) {
608
+ const layers = await loadConfigFile(globalConfig);
609
+ baseLayers.push(...layers);
610
+ configFiles.push(...layers.map(layer => layer.path));
611
+ }
612
+ const customPath = options.configPath ?? process.env.ARIAFLOW_CONFIG;
613
+ if (customPath) {
614
+ const layers = await loadConfigFile(customPath);
615
+ baseLayers.push(...layers);
616
+ configFiles.push(...layers.map(layer => layer.path));
617
+ }
618
+ const projectRoot = findProjectRoot(cwd);
619
+ const projectConfig = findProjectConfig(projectRoot);
620
+ if (projectConfig) {
621
+ const layers = await loadConfigFile(projectConfig);
622
+ baseLayers.push(...layers);
623
+ configFiles.push(...layers.map(layer => layer.path));
624
+ }
625
+ const inlineContent = options.configContent ?? process.env.ARIAFLOW_CONFIG_CONTENT;
626
+ const inlineLayer = inlineContent
627
+ ? { config: parseJsoncContent(inlineContent, 'inline'), dir: cwd, path: 'inline' }
628
+ : null;
629
+ let mergedConfig = {};
630
+ for (const layer of baseLayers) {
631
+ mergedConfig = deepMerge(mergedConfig, layer.config);
632
+ }
633
+ if (inlineLayer) {
634
+ mergedConfig = deepMerge(mergedConfig, inlineLayer.config);
635
+ }
636
+ const roots = [];
637
+ for (const layer of baseLayers) {
638
+ const root = path.join(layer.dir, '.ariaflow');
639
+ if (existsSync(root)) {
640
+ roots.push(root);
641
+ }
642
+ }
643
+ const ariaflowDirs = findAriaflowDirs(cwd, projectRoot);
644
+ for (const dir of ariaflowDirs) {
645
+ if (!roots.includes(dir)) {
646
+ roots.push(dir);
647
+ }
648
+ }
649
+ const flows = {};
650
+ const flowMeta = {};
651
+ for (const layer of baseLayers) {
652
+ if (!layer.config.flows) {
653
+ continue;
654
+ }
655
+ for (const [id, flowInput] of Object.entries(layer.config.flows)) {
656
+ flows[id] = {
657
+ nodes: flowInput.nodes,
658
+ transitions: flowInput.transitions,
659
+ defaultRolePrompt: flowInput.defaultRolePrompt,
660
+ contextStrategy: flowInput.contextStrategy,
661
+ };
662
+ flowMeta[id] = { id, initialNode: flowInput.initialNode ?? flowInput.entry, path: layer.path };
663
+ }
664
+ }
665
+ for (const root of roots) {
666
+ const loaded = await loadFlowFiles(root, sources);
667
+ Object.assign(flows, loaded.flows);
668
+ Object.assign(flowMeta, loaded.meta);
669
+ }
670
+ if (inlineLayer?.config.flows) {
671
+ for (const [id, flowInput] of Object.entries(inlineLayer.config.flows)) {
672
+ flows[id] = {
673
+ nodes: flowInput.nodes,
674
+ transitions: flowInput.transitions,
675
+ defaultRolePrompt: flowInput.defaultRolePrompt,
676
+ contextStrategy: flowInput.contextStrategy,
677
+ };
678
+ flowMeta[id] = { id, initialNode: flowInput.initialNode ?? flowInput.entry, path: inlineLayer.path };
679
+ }
680
+ }
681
+ const agentDefs = {};
682
+ for (const layer of baseLayers) {
683
+ const agentInputs = layer.config.agents ?? {};
684
+ for (const [id, input] of Object.entries(agentInputs)) {
685
+ const normalized = await normalizeAgentInput(id, input, layer.dir, flows, flowMeta);
686
+ agentDefs[id] = normalized;
687
+ }
688
+ }
689
+ for (const root of roots) {
690
+ const markdownAgents = await loadMarkdownAgents(root, sources);
691
+ Object.assign(agentDefs, markdownAgents);
692
+ }
693
+ if (inlineLayer?.config.agents) {
694
+ for (const [id, input] of Object.entries(inlineLayer.config.agents)) {
695
+ const normalized = await normalizeAgentInput(id, input, inlineLayer.dir, flows, flowMeta);
696
+ agentDefs[id] = normalized;
697
+ }
698
+ }
699
+ const toolsRegistry = { ...(options.builtinTools ?? {}) };
700
+ for (const root of roots) {
701
+ const loadedTools = await loadToolFiles(root, sources);
702
+ Object.assign(toolsRegistry, loadedTools);
703
+ }
704
+ const skills = [];
705
+ for (const root of roots) {
706
+ skills.push(...(await loadSkills(root, sources)));
707
+ }
708
+ const mergedPermissions = mergedConfig.permissions;
709
+ for (const layer of baseLayers) {
710
+ await loadToolConfigEntries(layer.config.tools, layer.dir, toolsRegistry, options.builtinTools, skills, mergedPermissions, sources);
711
+ }
712
+ if (inlineLayer) {
713
+ await loadToolConfigEntries(inlineLayer.config.tools, inlineLayer.dir, toolsRegistry, options.builtinTools, skills, mergedPermissions, sources);
714
+ }
715
+ const agents = [];
716
+ const aliasMap = mergedConfig.models ?? {};
717
+ const defaultModel = resolveModel(mergedConfig.runtime?.defaultModel, aliasMap, options.modelRegistry, options.defaultModel);
718
+ for (const agent of Object.values(agentDefs)) {
719
+ const model = resolveModel(agent.modelId, aliasMap, options.modelRegistry, defaultModel);
720
+ if (!model) {
721
+ throw new Error(`Agent "${agent.id}" is missing a model (resolved from config)`);
722
+ }
723
+ const tools = buildToolSet(toolsRegistry, agent.toolNames);
724
+ if (agent.type === 'triage') {
725
+ agents.push({
726
+ id: agent.id,
727
+ name: agent.name,
728
+ description: agent.description,
729
+ systemPrompt: triagePromptTemplate(agent.systemPrompt, agent.routes, agent.defaultAgent),
730
+ model: model,
731
+ tools: tools,
732
+ maxTurns: agent.maxTurns,
733
+ maxSteps: agent.maxSteps,
734
+ type: 'triage',
735
+ routes: agent.routes.map(route => ({
736
+ agentId: route.agentId,
737
+ description: route.description,
738
+ })),
739
+ defaultAgent: agent.defaultAgent,
740
+ });
741
+ continue;
742
+ }
743
+ if (agent.type === 'flow') {
744
+ const flowWithTools = hydrateFlowTools(agent.flow, toolsRegistry);
745
+ agents.push({
746
+ id: agent.id,
747
+ name: agent.name,
748
+ description: agent.description,
749
+ systemPrompt: agent.systemPrompt,
750
+ model: model,
751
+ tools: tools,
752
+ maxTurns: agent.maxTurns,
753
+ maxSteps: agent.maxSteps,
754
+ type: 'flow',
755
+ flow: flowWithTools,
756
+ initialNode: agent.initialNode,
757
+ mode: agent.mode,
758
+ });
759
+ continue;
760
+ }
761
+ agents.push({
762
+ id: agent.id,
763
+ name: agent.name,
764
+ description: agent.description,
765
+ systemPrompt: agent.systemPrompt,
766
+ model: model,
767
+ tools: tools,
768
+ maxTurns: agent.maxTurns,
769
+ maxSteps: agent.maxSteps,
770
+ type: 'llm',
771
+ });
772
+ }
773
+ const runtimeConfig = mergedConfig.runtime;
774
+ const defaultAgentId = runtimeConfig?.defaultAgent ?? agents[0]?.id;
775
+ if (!defaultAgentId) {
776
+ throw new Error('No default agent configured');
777
+ }
778
+ if (!agents.find(agent => agent.id === defaultAgentId)) {
779
+ throw new Error(`Default agent "${defaultAgentId}" not found in loaded agents`);
780
+ }
781
+ return {
782
+ runtime: {
783
+ defaultAgentId,
784
+ defaultModel: defaultModel,
785
+ maxSteps: runtimeConfig?.maxSteps,
786
+ maxTurns: runtimeConfig?.maxTurns,
787
+ maxHandoffs: runtimeConfig?.maxHandoffs,
788
+ },
789
+ agents,
790
+ flows,
791
+ tools: toolsRegistry,
792
+ sources,
793
+ };
794
+ }
795
+ export async function loadAriaflowConfigWithResult(options = {}) {
796
+ const startTime = Date.now();
797
+ initObservability(options);
798
+ const cwd = options.cwd ?? process.cwd();
799
+ const configFiles = [];
800
+ const sources = {
801
+ configFiles,
802
+ agentFiles: [],
803
+ flowFiles: [],
804
+ toolFiles: [],
805
+ skillFiles: [],
806
+ };
807
+ const baseLayers = [];
808
+ const globalConfig = findGlobalConfig();
809
+ if (globalConfig) {
810
+ const layers = await loadConfigFile(globalConfig);
811
+ baseLayers.push(...layers);
812
+ configFiles.push(...layers.map(layer => layer.path));
813
+ }
814
+ const customPath = options.configPath ?? process.env.ARIAFLOW_CONFIG;
815
+ if (customPath) {
816
+ const layers = await loadConfigFile(customPath);
817
+ baseLayers.push(...layers);
818
+ configFiles.push(...layers.map(layer => layer.path));
819
+ }
820
+ const projectRoot = findProjectRoot(cwd);
821
+ const projectConfig = findProjectConfig(projectRoot);
822
+ if (projectConfig) {
823
+ const layers = await loadConfigFile(projectConfig);
824
+ baseLayers.push(...layers);
825
+ configFiles.push(...layers.map(layer => layer.path));
826
+ }
827
+ const inlineContent = options.configContent ?? process.env.ARIAFLOW_CONFIG_CONTENT;
828
+ const inlineLayer = inlineContent
829
+ ? { config: parseJsoncContent(inlineContent, 'inline'), dir: cwd, path: 'inline' }
830
+ : null;
831
+ let mergedConfig = {};
832
+ for (const layer of baseLayers) {
833
+ mergedConfig = deepMerge(mergedConfig, layer.config);
834
+ }
835
+ if (inlineLayer) {
836
+ mergedConfig = deepMerge(mergedConfig, inlineLayer.config);
837
+ }
838
+ const roots = [];
839
+ for (const layer of baseLayers) {
840
+ const root = path.join(layer.dir, '.ariaflow');
841
+ if (existsSync(root)) {
842
+ roots.push(root);
843
+ }
844
+ }
845
+ const ariaflowDirs = findAriaflowDirs(cwd, projectRoot);
846
+ for (const dir of ariaflowDirs) {
847
+ if (!roots.includes(dir)) {
848
+ roots.push(dir);
849
+ }
850
+ }
851
+ const flows = {};
852
+ const flowMeta = {};
853
+ for (const layer of baseLayers) {
854
+ if (!layer.config.flows)
855
+ continue;
856
+ for (const [id, flowInput] of Object.entries(layer.config.flows)) {
857
+ flows[id] = {
858
+ nodes: flowInput.nodes,
859
+ transitions: flowInput.transitions,
860
+ defaultRolePrompt: flowInput.defaultRolePrompt,
861
+ contextStrategy: flowInput.contextStrategy,
862
+ };
863
+ flowMeta[id] = { id, initialNode: flowInput.initialNode ?? flowInput.entry, path: layer.path };
864
+ }
865
+ }
866
+ for (const root of roots) {
867
+ const loaded = await loadFlowFiles(root, sources);
868
+ Object.assign(flows, loaded.flows);
869
+ Object.assign(flowMeta, loaded.meta);
870
+ }
871
+ if (inlineLayer?.config.flows) {
872
+ for (const [id, flowInput] of Object.entries(inlineLayer.config.flows)) {
873
+ flows[id] = {
874
+ nodes: flowInput.nodes,
875
+ transitions: flowInput.transitions,
876
+ defaultRolePrompt: flowInput.defaultRolePrompt,
877
+ contextStrategy: flowInput.contextStrategy,
878
+ };
879
+ flowMeta[id] = { id, initialNode: flowInput.initialNode ?? flowInput.entry, path: inlineLayer.path };
880
+ }
881
+ }
882
+ const agentDefs = {};
883
+ for (const layer of baseLayers) {
884
+ const agentInputs = layer.config.agents ?? {};
885
+ for (const [id, input] of Object.entries(agentInputs)) {
886
+ const normalized = await normalizeAgentInput(id, input, layer.dir, flows, flowMeta);
887
+ agentDefs[id] = normalized;
888
+ }
889
+ }
890
+ for (const root of roots) {
891
+ const markdownAgents = await loadMarkdownAgents(root, sources);
892
+ Object.assign(agentDefs, markdownAgents);
893
+ }
894
+ if (inlineLayer?.config.agents) {
895
+ for (const [id, input] of Object.entries(inlineLayer.config.agents)) {
896
+ const normalized = await normalizeAgentInput(id, input, inlineLayer.dir, flows, flowMeta);
897
+ agentDefs[id] = normalized;
898
+ }
899
+ }
900
+ const toolsRegistry = { ...(options.builtinTools ?? {}) };
901
+ for (const root of roots) {
902
+ const loadedTools = await loadToolFiles(root, sources);
903
+ Object.assign(toolsRegistry, loadedTools);
904
+ }
905
+ const skills = [];
906
+ for (const root of roots) {
907
+ skills.push(...(await loadSkills(root, sources)));
908
+ }
909
+ const mergedPermissions = mergedConfig.permissions;
910
+ for (const layer of baseLayers) {
911
+ await loadToolConfigEntries(layer.config.tools, layer.dir, toolsRegistry, options.builtinTools, skills, mergedPermissions, sources);
912
+ }
913
+ if (inlineLayer) {
914
+ await loadToolConfigEntries(inlineLayer.config.tools, inlineLayer.dir, toolsRegistry, options.builtinTools, skills, mergedPermissions, sources);
915
+ }
916
+ const agents = [];
917
+ const aliasMap = mergedConfig.models ?? {};
918
+ const defaultModel = resolveModel(mergedConfig.runtime?.defaultModel, aliasMap, options.modelRegistry, options.defaultModel);
919
+ for (const agent of Object.values(agentDefs)) {
920
+ const model = resolveModel(agent.modelId, aliasMap, options.modelRegistry, defaultModel);
921
+ if (!model) {
922
+ throw new Error(`Agent "${agent.id}" is missing a model (resolved from config)`);
923
+ }
924
+ const tools = buildToolSet(toolsRegistry, agent.toolNames);
925
+ if (agent.type === 'triage') {
926
+ agents.push({
927
+ id: agent.id,
928
+ name: agent.name,
929
+ description: agent.description,
930
+ systemPrompt: triagePromptTemplate(agent.systemPrompt, agent.routes, agent.defaultAgent),
931
+ model: model,
932
+ tools: tools,
933
+ maxTurns: agent.maxTurns,
934
+ maxSteps: agent.maxSteps,
935
+ type: 'triage',
936
+ routes: agent.routes.map(route => ({
937
+ agentId: route.agentId,
938
+ description: route.description,
939
+ })),
940
+ defaultAgent: agent.defaultAgent,
941
+ });
942
+ continue;
943
+ }
944
+ if (agent.type === 'flow') {
945
+ const flowWithTools = hydrateFlowTools(agent.flow, toolsRegistry);
946
+ agents.push({
947
+ id: agent.id,
948
+ name: agent.name,
949
+ description: agent.description,
950
+ systemPrompt: agent.systemPrompt,
951
+ model: model,
952
+ tools: tools,
953
+ maxTurns: agent.maxTurns,
954
+ maxSteps: agent.maxSteps,
955
+ type: 'flow',
956
+ flow: flowWithTools,
957
+ initialNode: agent.initialNode,
958
+ mode: agent.mode,
959
+ });
960
+ continue;
961
+ }
962
+ agents.push({
963
+ id: agent.id,
964
+ name: agent.name,
965
+ description: agent.description,
966
+ systemPrompt: agent.systemPrompt,
967
+ model: model,
968
+ tools: tools,
969
+ maxTurns: agent.maxTurns,
970
+ maxSteps: agent.maxSteps,
971
+ type: 'llm',
972
+ });
973
+ }
974
+ const runtimeConfig = mergedConfig.runtime;
975
+ const defaultAgentId = runtimeConfig?.defaultAgent ?? agents[0]?.id;
976
+ if (!defaultAgentId) {
977
+ throw new Error('No default agent configured');
978
+ }
979
+ if (!agents.find(agent => agent.id === defaultAgentId)) {
980
+ throw new Error(`Default agent "${defaultAgentId}" not found in loaded agents`);
981
+ }
982
+ const warnings = getWarnings();
983
+ const errorCount = warnings.filter(w => w.severity === 'error').length;
984
+ const durationMs = Date.now() - startTime;
985
+ if (!options.silent && warnings.length > 0) {
986
+ console.log(`\n⚠️ Loaded with ${errorCount} error(s) and ${warnings.length - errorCount} warning(s)`);
987
+ }
988
+ return {
989
+ config: {
990
+ runtime: {
991
+ defaultAgentId,
992
+ defaultModel: defaultModel,
993
+ maxSteps: runtimeConfig?.maxSteps,
994
+ maxTurns: runtimeConfig?.maxTurns,
995
+ maxHandoffs: runtimeConfig?.maxHandoffs,
996
+ },
997
+ agents,
998
+ flows,
999
+ tools: toolsRegistry,
1000
+ sources,
1001
+ },
1002
+ summary: {
1003
+ agents: agents.length,
1004
+ flows: Object.keys(flows).length,
1005
+ tools: Object.keys(toolsRegistry).length,
1006
+ skills: skills.length,
1007
+ layers: configFiles,
1008
+ durationMs,
1009
+ warningCount: warnings.length - errorCount,
1010
+ errorCount,
1011
+ },
1012
+ warnings,
1013
+ };
1014
+ }
1015
+ export function printLoadSummary(summary) {
1016
+ console.log('\n✅ Config loaded successfully');
1017
+ console.log(` • ${summary.agents} agent(s)`);
1018
+ console.log(` • ${summary.flows} flow(s)`);
1019
+ console.log(` • ${summary.tools} tool(s)`);
1020
+ console.log(` • ${summary.skills} skill(s)`);
1021
+ if (summary.layers.length > 0) {
1022
+ console.log(` • Layers: ${summary.layers.map(l => path.basename(l)).join(', ')}`);
1023
+ }
1024
+ if (summary.warningCount > 0 || summary.errorCount > 0) {
1025
+ console.log(` • ${summary.warningCount} warning(s), ${summary.errorCount} error(s)`);
1026
+ }
1027
+ console.log(` • ${summary.durationMs}ms`);
1028
+ }
1029
+ async function normalizeAgentInput(id, input, baseDir, flows, flowMeta) {
1030
+ const type = (input.type ?? 'llm');
1031
+ const name = input.name ?? id;
1032
+ const description = input.description;
1033
+ const modelId = input.model;
1034
+ const promptValue = await resolvePrompt(input.prompt ?? input.systemPrompt, baseDir);
1035
+ if (!promptValue) {
1036
+ warn('missing_prompt', id, `Agent "${id}" is missing a prompt`);
1037
+ }
1038
+ const baseAgent = {
1039
+ id,
1040
+ name,
1041
+ description,
1042
+ systemPrompt: promptValue ?? `[Missing prompt for agent: ${id}]`,
1043
+ modelId,
1044
+ toolNames: normalizeToolSelection(input.tools),
1045
+ maxTurns: input.maxTurns,
1046
+ maxSteps: input.maxSteps,
1047
+ };
1048
+ if (type === 'triage') {
1049
+ const triage = input;
1050
+ const routes = triage.routes ?? [];
1051
+ const basePrompt = input.systemPrompt ? (await resolvePrompt(input.systemPrompt, baseDir)) : promptValue;
1052
+ return {
1053
+ ...baseAgent,
1054
+ type: 'triage',
1055
+ routes,
1056
+ defaultAgent: triage.defaultAgent,
1057
+ systemPrompt: basePrompt ?? promptValue ?? `[Missing prompt for agent: ${id}]`,
1058
+ };
1059
+ }
1060
+ if (type === 'flow') {
1061
+ const flowInput = input;
1062
+ const mode = flowInput.mode === 'hybrid' || flowInput.mode === 'strict'
1063
+ ? flowInput.mode
1064
+ : undefined;
1065
+ const flowId = flowInput.flowRef ?? flowInput.flow?.id;
1066
+ const flowConfig = flowInput.flow
1067
+ ? {
1068
+ nodes: flowInput.flow.nodes,
1069
+ transitions: flowInput.flow.transitions,
1070
+ defaultRolePrompt: flowInput.flow.defaultRolePrompt,
1071
+ contextStrategy: flowInput.flow.contextStrategy,
1072
+ }
1073
+ : flowId
1074
+ ? flows[flowId]
1075
+ : undefined;
1076
+ if (!flowConfig) {
1077
+ throw new Error(`Flow agent "${id}" references missing flow`);
1078
+ }
1079
+ const initialNode = flowInput.initialNode ??
1080
+ flowInput.flow?.entry ??
1081
+ flowMeta[flowId ?? '']?.initialNode ??
1082
+ flowConfig.nodes?.[0]?.id;
1083
+ if (!initialNode) {
1084
+ throw new Error(`Flow agent "${id}" is missing initialNode`);
1085
+ }
1086
+ return {
1087
+ ...baseAgent,
1088
+ type: 'flow',
1089
+ flow: flowConfig,
1090
+ initialNode,
1091
+ mode,
1092
+ };
1093
+ }
1094
+ return {
1095
+ ...baseAgent,
1096
+ type: 'llm',
1097
+ };
1098
+ }
1099
+ export function createRuntimeFromConfig(config, overrides = {}) {
1100
+ return createRuntime({
1101
+ agents: config.agents,
1102
+ defaultAgentId: config.runtime.defaultAgentId,
1103
+ defaultModel: config.runtime.defaultModel,
1104
+ maxSteps: config.runtime.maxSteps,
1105
+ maxHandoffs: config.runtime.maxHandoffs,
1106
+ ...overrides,
1107
+ });
1108
+ }
1109
+ //# sourceMappingURL=loader.js.map