@mcp-abap-adt/llm-agent-server 16.2.0 → 18.0.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/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/smart-agent/build-dag-coordinator-deps.d.ts +34 -0
- package/dist/smart-agent/build-dag-coordinator-deps.d.ts.map +1 -0
- package/dist/smart-agent/build-dag-coordinator-deps.js +107 -0
- package/dist/smart-agent/build-dag-coordinator-deps.js.map +1 -0
- package/dist/smart-agent/build-stepper-root.d.ts +97 -0
- package/dist/smart-agent/build-stepper-root.d.ts.map +1 -0
- package/dist/smart-agent/build-stepper-root.js +242 -0
- package/dist/smart-agent/build-stepper-root.js.map +1 -0
- package/dist/smart-agent/config.d.ts +207 -3
- package/dist/smart-agent/config.d.ts.map +1 -1
- package/dist/smart-agent/config.js +465 -39
- package/dist/smart-agent/config.js.map +1 -1
- package/dist/smart-agent/jsonl-knowledge-backend.d.ts +19 -0
- package/dist/smart-agent/jsonl-knowledge-backend.d.ts.map +1 -0
- package/dist/smart-agent/jsonl-knowledge-backend.js +46 -0
- package/dist/smart-agent/jsonl-knowledge-backend.js.map +1 -0
- package/dist/smart-agent/session-identity-resolver.d.ts +17 -0
- package/dist/smart-agent/session-identity-resolver.d.ts.map +1 -0
- package/dist/smart-agent/session-identity-resolver.js +37 -0
- package/dist/smart-agent/session-identity-resolver.js.map +1 -0
- package/dist/smart-agent/session-meta-store.d.ts +53 -0
- package/dist/smart-agent/session-meta-store.d.ts.map +1 -0
- package/dist/smart-agent/session-meta-store.js +36 -0
- package/dist/smart-agent/session-meta-store.js.map +1 -0
- package/dist/smart-agent/smart-server.d.ts +288 -4
- package/dist/smart-agent/smart-server.d.ts.map +1 -1
- package/dist/smart-agent/smart-server.js +1325 -111
- package/dist/smart-agent/smart-server.js.map +1 -1
- package/dist/smart-agent/stepper-coordinator-handler.d.ts +36 -0
- package/dist/smart-agent/stepper-coordinator-handler.d.ts.map +1 -0
- package/dist/smart-agent/stepper-coordinator-handler.js +214 -0
- package/dist/smart-agent/stepper-coordinator-handler.js.map +1 -0
- package/package.json +26 -26
|
@@ -3,8 +3,172 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { AutoActivation, ExplicitActivation, HybridDispatch, OneShotPlanning, ReplanOnErrorPlanning, ScoreThresholdToolSelection, SelfDispatch, SkillStepsPlanning, SubAgentDispatch, TopKToolSelection, } from '@mcp-abap-adt/llm-agent-libs';
|
|
6
|
+
import { AutoActivation, ExplicitActivation, HybridDispatch, LlmFinalizer, OneShotPlanning, PassthroughFinalizer, ReplanOnErrorPlanning, ScoreThresholdToolSelection, SelfDispatch, SkillStepsPlanning, SubAgentDispatch, TemplateFinalizer, TopKToolSelection, } from '@mcp-abap-adt/llm-agent-libs';
|
|
7
7
|
import { parse as parseYaml } from 'yaml';
|
|
8
|
+
/**
|
|
9
|
+
* Detect whether an object is a flat SmartServerLlmConfig shape.
|
|
10
|
+
* Flat shape is identified by the presence of ANY of the known flat-shape
|
|
11
|
+
* fields: `provider`, `apiKey`, `model`, or `url`. This covers keyless
|
|
12
|
+
* providers (Ollama, SAP AI Core) that omit `apiKey` — using `apiKey`-only
|
|
13
|
+
* detection silently misclassified those configs as a "map" shape.
|
|
14
|
+
*/
|
|
15
|
+
function isFlatLlmConfig(input) {
|
|
16
|
+
const flat = input;
|
|
17
|
+
return (typeof flat.provider === 'string' ||
|
|
18
|
+
typeof flat.apiKey === 'string' ||
|
|
19
|
+
typeof flat.model === 'string' ||
|
|
20
|
+
typeof flat.url === 'string');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize the optional top-level `llm:` block.
|
|
24
|
+
* - undefined → undefined (pipeline-only configs stay valid)
|
|
25
|
+
* - flat shape (has `provider` | `apiKey` | `model` | `url`) → { main: flat } (backward compat)
|
|
26
|
+
* - map shape → must include `main`; returned as NormalizedLlmMap
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeLlmConfig(input) {
|
|
29
|
+
if (input === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (isFlatLlmConfig(input)) {
|
|
32
|
+
return { main: input };
|
|
33
|
+
}
|
|
34
|
+
const map = input;
|
|
35
|
+
if (!map.main) {
|
|
36
|
+
throw new Error("llm: map must include a 'main' key (default LLM for unspecified roles)");
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Strict lookup: returns map[name] if explicitly present, else undefined.
|
|
42
|
+
* Does NOT fall through to map.main. Use when the caller needs to
|
|
43
|
+
* detect explicit-presence (e.g. to decide between an alias and the
|
|
44
|
+
* named map entry).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveLlmConfigStrict(map, name) {
|
|
47
|
+
if (!map || !name)
|
|
48
|
+
return undefined;
|
|
49
|
+
return map[name];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a per-role LLM config by name from a normalized map.
|
|
53
|
+
* Lookup chain: map[name] → map.main → pipelineFallback.
|
|
54
|
+
* When map is undefined, falls back to pipelineFallback (so pipeline-only
|
|
55
|
+
* configs keep working with no top-level llm: block).
|
|
56
|
+
*
|
|
57
|
+
* The caller decides whether `undefined` is an error.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveLlmConfig(map, name, pipelineFallback) {
|
|
60
|
+
if (!map)
|
|
61
|
+
return pipelineFallback;
|
|
62
|
+
if (!name || name === 'main')
|
|
63
|
+
return map.main;
|
|
64
|
+
return map[name] ?? map.main;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the reviewer block's LLM-name selector, accepting both the
|
|
68
|
+
* preferred `reviewerLlm` field and the deprecated `plannerLlm` alias.
|
|
69
|
+
* When the alias is used, calls `warn(message)`.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveReviewerLlmName(block, warn) {
|
|
72
|
+
if (!block)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (typeof block.reviewerLlm === 'string')
|
|
75
|
+
return block.reviewerLlm;
|
|
76
|
+
if (typeof block.plannerLlm === 'string') {
|
|
77
|
+
warn("coordinator.reviewer.plannerLlm is deprecated; rename to 'reviewerLlm'");
|
|
78
|
+
return block.plannerLlm;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const LINEAR_ONLY = [
|
|
83
|
+
'planning',
|
|
84
|
+
'dispatch',
|
|
85
|
+
'maxSteps',
|
|
86
|
+
'maxRetriesPerStep',
|
|
87
|
+
'failPolicy',
|
|
88
|
+
'maxLayer',
|
|
89
|
+
'plannerLlm',
|
|
90
|
+
];
|
|
91
|
+
const DAG_ONLY = [
|
|
92
|
+
'planner',
|
|
93
|
+
'interpreter',
|
|
94
|
+
'reviewer',
|
|
95
|
+
'errorStrategy',
|
|
96
|
+
'finalizer',
|
|
97
|
+
'stateOracle',
|
|
98
|
+
'maxRoundTrips',
|
|
99
|
+
];
|
|
100
|
+
/** Validate a coordinator role block shape. */
|
|
101
|
+
function assertLlmRoleShape(label, role) {
|
|
102
|
+
if (typeof role !== 'object' || role === null || Array.isArray(role)) {
|
|
103
|
+
throw new Error(`coordinator.${label} must be an object (e.g. { type: llm }), got: ${JSON.stringify(role)}`);
|
|
104
|
+
}
|
|
105
|
+
const kind = role.type;
|
|
106
|
+
if (kind !== undefined &&
|
|
107
|
+
kind !== 'llm' &&
|
|
108
|
+
!(label === 'finalizer' && (kind === 'passthrough' || kind === 'template'))) {
|
|
109
|
+
throw new Error(`coordinator.${label}: unknown type '${String(kind)}'`);
|
|
110
|
+
}
|
|
111
|
+
for (const field of ['plannerLlm', 'reviewerLlm', 'finalizerLlm']) {
|
|
112
|
+
const sel = role[field];
|
|
113
|
+
if (sel !== undefined && typeof sel !== 'string') {
|
|
114
|
+
throw new Error(`coordinator.${label}.${field} must be a string referencing an llm.* key, got: ${String(sel)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const sp = role.systemPrompt;
|
|
118
|
+
if (sp !== undefined && typeof sp !== 'string') {
|
|
119
|
+
throw new Error(`coordinator.${label}.systemPrompt must be a string, got: ${String(sp)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function assertErrorStrategyShape(es) {
|
|
123
|
+
if (typeof es !== 'object' || es === null || Array.isArray(es)) {
|
|
124
|
+
throw new Error(`coordinator.errorStrategy must be an object (e.g. { type: replan }), got: ${JSON.stringify(es)}`);
|
|
125
|
+
}
|
|
126
|
+
const type = es.type;
|
|
127
|
+
if (type !== undefined && type !== 'abort' && type !== 'replan') {
|
|
128
|
+
throw new Error(`coordinator.errorStrategy: unknown type '${String(type)}' (only 'abort' | 'replan')`);
|
|
129
|
+
}
|
|
130
|
+
const mr = es.maxReplans;
|
|
131
|
+
if (mr !== undefined && (typeof mr !== 'number' || mr < 0)) {
|
|
132
|
+
throw new Error(`coordinator.errorStrategy.maxReplans must be a non-negative number, got: ${String(mr)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Fail-loud guard: a coordinator block is either DAG (has `planner`) or linear,
|
|
136
|
+
* never mixed. `activation` is shared and always allowed. */
|
|
137
|
+
export function assertCoordinatorConfigShape(coord) {
|
|
138
|
+
const isDag = coord.planner !== undefined;
|
|
139
|
+
if (isDag) {
|
|
140
|
+
assertLlmRoleShape('planner', coord.planner);
|
|
141
|
+
if (coord.reviewer !== undefined) {
|
|
142
|
+
assertLlmRoleShape('reviewer', coord.reviewer);
|
|
143
|
+
}
|
|
144
|
+
if (coord.finalizer !== undefined) {
|
|
145
|
+
assertLlmRoleShape('finalizer', coord.finalizer);
|
|
146
|
+
}
|
|
147
|
+
if (coord.errorStrategy !== undefined) {
|
|
148
|
+
assertErrorStrategyShape(coord.errorStrategy);
|
|
149
|
+
}
|
|
150
|
+
if (coord.stateOracle !== undefined &&
|
|
151
|
+
typeof coord.stateOracle !== 'string') {
|
|
152
|
+
throw new Error(`coordinator.stateOracle must be a string (a declared subagent name), got: ${JSON.stringify(coord.stateOracle)}`);
|
|
153
|
+
}
|
|
154
|
+
if (coord.maxRoundTrips !== undefined &&
|
|
155
|
+
(typeof coord.maxRoundTrips !== 'number' || coord.maxRoundTrips < 0)) {
|
|
156
|
+
throw new Error(`coordinator.maxRoundTrips must be a non-negative number, got: ${String(coord.maxRoundTrips)}`);
|
|
157
|
+
}
|
|
158
|
+
for (const f of LINEAR_ONLY) {
|
|
159
|
+
if (coord[f] !== undefined) {
|
|
160
|
+
throw new Error(`coordinator: '${f}' is a linear-only field and cannot be combined with 'planner' (DAG mode)`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
for (const f of DAG_ONLY) {
|
|
166
|
+
if (coord[f] !== undefined) {
|
|
167
|
+
throw new Error(`coordinator: '${f}' is a DAG-only field; a linear coordinator uses 'planning'/'dispatch'`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
8
172
|
export function resolveCoordinatorPlanning(name, plannerLlm) {
|
|
9
173
|
switch (name) {
|
|
10
174
|
case 'one-shot':
|
|
@@ -20,6 +184,15 @@ export function resolveCoordinatorPlanning(name, plannerLlm) {
|
|
|
20
184
|
throw new Error(`Unknown coordinator.planning strategy: '${name}'. Allowed: one-shot, replan-on-error, skill-steps.`);
|
|
21
185
|
}
|
|
22
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Default coordinator dispatch kind. Omitted → 'hybrid' for ALL planning kinds:
|
|
189
|
+
* agentless steps — the synthesized answer-directly step (#155) and skill steps
|
|
190
|
+
* without an explicit `agent:` — need a self-LLM fallback. Pin 'subagent'
|
|
191
|
+
* explicitly for strict subagent-only routing.
|
|
192
|
+
*/
|
|
193
|
+
export function resolveCoordinatorDispatchKind(explicit) {
|
|
194
|
+
return explicit ?? 'hybrid';
|
|
195
|
+
}
|
|
23
196
|
export function resolveCoordinatorDispatch(name, fallbackLlm, contextBuilder) {
|
|
24
197
|
switch (name) {
|
|
25
198
|
case 'subagent':
|
|
@@ -241,7 +414,8 @@ log: smart-server.log # path to log file; omit for stdout
|
|
|
241
414
|
# maxSteps: 12
|
|
242
415
|
# maxRetriesPerStep: 1
|
|
243
416
|
# failPolicy: abort # abort | continue
|
|
244
|
-
# maxLayer: 1 #
|
|
417
|
+
# maxLayer: 1 # DEPRECATED — accepted but ignored (nested
|
|
418
|
+
# # dispatch removed; subagents are leaves)
|
|
245
419
|
`;
|
|
246
420
|
export function resolveEnvVars(value, env = process.env) {
|
|
247
421
|
if (typeof value === 'string')
|
|
@@ -262,6 +436,33 @@ export function loadYamlConfig(filePath, env = process.env) {
|
|
|
262
436
|
export function generateConfigTemplate(outputPath) {
|
|
263
437
|
fs.writeFileSync(outputPath, YAML_TEMPLATE, 'utf8');
|
|
264
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Build the IFinalizer impl from `coordinator.finalizer:` YAML.
|
|
441
|
+
*
|
|
442
|
+
* Lookup chain for `type: llm`:
|
|
443
|
+
* resolveLlmConfig(llmMap, cfg.finalizerLlm, pipelineFallback)
|
|
444
|
+
* → top-level llm.<name> → llm.main → pipelineFallback (pipeline.llm.main)
|
|
445
|
+
* → ConfigError if all three are missing.
|
|
446
|
+
*
|
|
447
|
+
* Absent block / `type: passthrough` → PassthroughFinalizer.
|
|
448
|
+
* `type: template` → TemplateFinalizer.
|
|
449
|
+
*/
|
|
450
|
+
export async function buildFinalizer(cfg, llmMap, pipelineFallback, makeLlm) {
|
|
451
|
+
const kind = cfg?.type ?? 'passthrough';
|
|
452
|
+
if (kind === 'passthrough')
|
|
453
|
+
return new PassthroughFinalizer();
|
|
454
|
+
if (kind === 'template')
|
|
455
|
+
return new TemplateFinalizer();
|
|
456
|
+
// kind === 'llm'
|
|
457
|
+
const resolved = resolveLlmConfig(llmMap, cfg?.finalizerLlm, pipelineFallback);
|
|
458
|
+
if (!resolved) {
|
|
459
|
+
throw new Error('coordinator.finalizer (type: llm) requires an LLM config: provide top-level llm.<name>, llm.main, or pipeline.llm.main');
|
|
460
|
+
}
|
|
461
|
+
const llm = await makeLlm(resolved);
|
|
462
|
+
return new LlmFinalizer(llm, {
|
|
463
|
+
systemPrompt: cfg?.systemPrompt,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
265
466
|
const get = (obj, ...keys) => keys.reduce((o, k) => {
|
|
266
467
|
if (o !== null && typeof o === 'object' && k in o) {
|
|
267
468
|
return o[k];
|
|
@@ -335,7 +536,10 @@ function checkRagStore(label, store, issues) {
|
|
|
335
536
|
issues.push(`${label}.model: required when an embedder is used (e.g. bge-m3 for ollama)`);
|
|
336
537
|
}
|
|
337
538
|
}
|
|
338
|
-
function
|
|
539
|
+
function validateLlmEntry(label, cfg, required, env, issues) {
|
|
540
|
+
checkLlmRole(label, cfg, required, env, issues);
|
|
541
|
+
}
|
|
542
|
+
function validateResolvedConfig(_resolved, yaml, env) {
|
|
339
543
|
const issues = [];
|
|
340
544
|
const usingPipeline = !!get(yaml, 'pipeline', 'llm', 'main');
|
|
341
545
|
if (usingPipeline) {
|
|
@@ -349,24 +553,54 @@ function validateResolvedConfig(resolved, yaml, env) {
|
|
|
349
553
|
}
|
|
350
554
|
}
|
|
351
555
|
else {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (mcpType && !['http', 'stdio', 'none'].includes(mcpType)) {
|
|
361
|
-
issues.push(`mcp.type: "${mcpType}" is invalid (one of: http, stdio, none)`);
|
|
556
|
+
// Read from the raw YAML so we can distinguish flat vs map shape.
|
|
557
|
+
// `resolved.llm` is always constructed as a flat object by resolveSmartServerConfig,
|
|
558
|
+
// so it cannot be used to detect the map shape.
|
|
559
|
+
const rawLlm = get(yaml, 'llm');
|
|
560
|
+
if (rawLlm === undefined) {
|
|
561
|
+
// No top-level llm: AND no pipeline.llm.main → would have been caught
|
|
562
|
+
// by `usingPipeline` branch. Defensive: surface a clear issue.
|
|
563
|
+
issues.push('llm: required when pipeline.llm.main is not configured');
|
|
362
564
|
}
|
|
363
|
-
if (
|
|
364
|
-
|
|
565
|
+
else if (typeof rawLlm.provider === 'string') {
|
|
566
|
+
// Flat shape — existing behaviour.
|
|
567
|
+
validateLlmEntry('llm', rawLlm, true, env, issues);
|
|
365
568
|
}
|
|
366
|
-
|
|
367
|
-
|
|
569
|
+
else {
|
|
570
|
+
// Map shape — llm.main is required; every named entry is validated.
|
|
571
|
+
const map = rawLlm;
|
|
572
|
+
if (!map.main) {
|
|
573
|
+
issues.push("llm.main: required when 'llm' is a named map");
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
validateLlmEntry('llm.main', map.main, true, env, issues);
|
|
577
|
+
}
|
|
578
|
+
for (const [name, entry] of Object.entries(map)) {
|
|
579
|
+
if (name === 'main')
|
|
580
|
+
continue;
|
|
581
|
+
validateLlmEntry(`llm.${name}`, entry, true, env, issues);
|
|
582
|
+
}
|
|
368
583
|
}
|
|
369
584
|
}
|
|
585
|
+
if (get(yaml, 'mcp')) {
|
|
586
|
+
const rawMcpVal = yaml.mcp;
|
|
587
|
+
const mcpEntries = Array.isArray(rawMcpVal)
|
|
588
|
+
? rawMcpVal
|
|
589
|
+
: [rawMcpVal];
|
|
590
|
+
mcpEntries.forEach((entry, i) => {
|
|
591
|
+
const label = Array.isArray(rawMcpVal) ? `mcp[${i}]` : 'mcp';
|
|
592
|
+
const mcpType = entry?.type;
|
|
593
|
+
if (mcpType && !['http', 'stdio', 'none'].includes(mcpType)) {
|
|
594
|
+
issues.push(`${label}.type: "${mcpType}" is invalid (one of: http, stdio, none)`);
|
|
595
|
+
}
|
|
596
|
+
if (mcpType === 'http' && !entry?.url) {
|
|
597
|
+
issues.push(`${label}.url: required when ${label}.type is http`);
|
|
598
|
+
}
|
|
599
|
+
if (mcpType === 'stdio' && !entry?.command) {
|
|
600
|
+
issues.push(`${label}.command: required when ${label}.type is stdio`);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
370
604
|
if (get(yaml, 'rag')) {
|
|
371
605
|
checkRagStore('rag', get(yaml, 'rag'), issues);
|
|
372
606
|
}
|
|
@@ -465,10 +699,14 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
|
|
|
465
699
|
const flatApiKey = get(yaml, 'llm', 'apiKey') ?? '';
|
|
466
700
|
const pipelineApiKey = get(yaml, 'pipeline', 'llm', 'main', 'apiKey');
|
|
467
701
|
const apiKey = flatApiKey || pipelineApiKey || '';
|
|
702
|
+
const rawMcp = yaml.mcp;
|
|
703
|
+
const mcpIsArray = Array.isArray(rawMcp);
|
|
468
704
|
const mcpUrl = get(yaml, 'mcp', 'url');
|
|
469
705
|
const mcpCommand = get(yaml, 'mcp', 'command');
|
|
470
|
-
const mcpTypeRaw =
|
|
471
|
-
|
|
706
|
+
const mcpTypeRaw = mcpIsArray
|
|
707
|
+
? null // array form: type resolved per-entry inside connectMcpClientsFromConfig
|
|
708
|
+
: (get(yaml, 'mcp', 'type') ??
|
|
709
|
+
(mcpUrl ? 'http' : mcpCommand ? 'stdio' : null));
|
|
472
710
|
const mcpType = (mcpTypeRaw === 'none' ? null : mcpTypeRaw);
|
|
473
711
|
const promptSystem = get(yaml, 'prompts', 'system') ?? null;
|
|
474
712
|
const promptClassifier = get(yaml, 'prompts', 'classifier') ?? null;
|
|
@@ -478,14 +716,18 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
|
|
|
478
716
|
const resolved = {
|
|
479
717
|
port: Number(args.port ?? get(yaml, 'port') ?? env.PORT ?? 4004),
|
|
480
718
|
host: args.host ?? get(yaml, 'host') ?? '0.0.0.0',
|
|
481
|
-
llm:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
719
|
+
llm: get(yaml, 'llm')
|
|
720
|
+
? typeof get(yaml, 'llm', 'provider') === 'string'
|
|
721
|
+
? {
|
|
722
|
+
provider: get(yaml, 'llm', 'provider'),
|
|
723
|
+
apiKey,
|
|
724
|
+
url: get(yaml, 'llm', 'url'),
|
|
725
|
+
model: get(yaml, 'llm', 'model'),
|
|
726
|
+
temperature: Number(get(yaml, 'llm', 'temperature') ?? 0.7),
|
|
727
|
+
classifierTemperature: Number(get(yaml, 'llm', 'classifierTemperature') ?? 0.1),
|
|
728
|
+
}
|
|
729
|
+
: get(yaml, 'llm')
|
|
730
|
+
: undefined,
|
|
489
731
|
rag: get(yaml, 'rag')
|
|
490
732
|
? {
|
|
491
733
|
type: get(yaml, 'rag', 'type'),
|
|
@@ -508,18 +750,22 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
|
|
|
508
750
|
: {}),
|
|
509
751
|
}
|
|
510
752
|
: undefined,
|
|
511
|
-
mcp:
|
|
512
|
-
?
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
: undefined,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
753
|
+
mcp: mcpIsArray
|
|
754
|
+
? // Array form: pass through as-is so connectMcpClientsFromConfig can
|
|
755
|
+
// iterate and connect each entry. Typed as SmartServerMcpConfig[].
|
|
756
|
+
rawMcp
|
|
757
|
+
: mcpType
|
|
758
|
+
? {
|
|
759
|
+
type: mcpType,
|
|
760
|
+
url: mcpUrl || undefined,
|
|
761
|
+
command: mcpCommand || undefined,
|
|
762
|
+
args: args['mcp-args'] || get(yaml, 'mcp', 'args')
|
|
763
|
+
? String(args['mcp-args'] || get(yaml, 'mcp', 'args')).split(' ')
|
|
764
|
+
: undefined,
|
|
765
|
+
headers: get(yaml, 'mcp', 'headers') ||
|
|
766
|
+
undefined,
|
|
767
|
+
}
|
|
768
|
+
: undefined,
|
|
523
769
|
agent: {
|
|
524
770
|
externalToolsValidationMode: (get(yaml, 'agent', 'externalToolsValidationMode') ?? 'permissive'),
|
|
525
771
|
maxIterations: Number(get(yaml, 'agent', 'maxIterations') ?? 10),
|
|
@@ -649,4 +895,184 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
|
|
|
649
895
|
validateResolvedConfig(resolved, yaml, env);
|
|
650
896
|
return resolved;
|
|
651
897
|
}
|
|
898
|
+
const MODES = new Set(['cyclic-react', 'planned-react']);
|
|
899
|
+
/**
|
|
900
|
+
* Preset expansion: each `mode` maps to a default `flow` composition.
|
|
901
|
+
* An explicit `coordinator.flow` block overrides these per-component.
|
|
902
|
+
*/
|
|
903
|
+
const MODE_FLOW_PRESET = {
|
|
904
|
+
'cyclic-react': { planner: 'none', executor: 'cyclic-react' },
|
|
905
|
+
'planned-react': { planner: 'llm', executor: 'cyclic-react' },
|
|
906
|
+
};
|
|
907
|
+
/** Parse declarative `flow.plan` nodes (for the static planner). */
|
|
908
|
+
function parseFlowPlan(raw) {
|
|
909
|
+
if (!Array.isArray(raw))
|
|
910
|
+
return undefined;
|
|
911
|
+
const nodes = raw
|
|
912
|
+
.filter((n) => !!n && typeof n.goal === 'string')
|
|
913
|
+
.map((n, i) => ({
|
|
914
|
+
id: typeof n.id === 'string' && n.id ? n.id : `n${i}`,
|
|
915
|
+
goal: n.goal,
|
|
916
|
+
...(Array.isArray(n.dependsOn)
|
|
917
|
+
? {
|
|
918
|
+
dependsOn: n.dependsOn.filter((d) => typeof d === 'string'),
|
|
919
|
+
}
|
|
920
|
+
: {}),
|
|
921
|
+
...(typeof n.agent === 'string' ? { agent: n.agent } : {}),
|
|
922
|
+
}));
|
|
923
|
+
return nodes.length > 0 ? nodes : undefined;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Parse a (possibly nested) `flow` block into a full StepperCompositionSpec,
|
|
927
|
+
* inheriting bounds from the root. Mutually recursive with
|
|
928
|
+
* parseCompositionNodes (function declarations are hoisted).
|
|
929
|
+
*/
|
|
930
|
+
function parseNestedFlowSpec(flowCfg, bounds) {
|
|
931
|
+
const plannerType = flowCfg?.planner?.type ?? 'llm';
|
|
932
|
+
if (!['none', 'llm', 'static'].includes(plannerType))
|
|
933
|
+
throw new Error(`flow.planner.type must be none|llm|static`);
|
|
934
|
+
const granularity = flowCfg?.planner?.granularity ?? 'shallow';
|
|
935
|
+
if (!['shallow', 'detailed'].includes(granularity))
|
|
936
|
+
throw new Error(`flow.planner.granularity must be shallow|detailed`);
|
|
937
|
+
const executor = flowCfg?.executor?.type ?? 'cyclic-react';
|
|
938
|
+
if (!['simple', 'cyclic-react'].includes(executor))
|
|
939
|
+
throw new Error(`flow.executor.type must be simple|cyclic-react (recursive is deferred to 18.1)`);
|
|
940
|
+
const plannerSystemPrompt = parseSystemPromptOverride(flowCfg?.planner?.systemPrompt, 'flow.planner.systemPrompt');
|
|
941
|
+
const executorSystemPrompt = parseSystemPromptOverride(flowCfg?.executor?.systemPrompt, 'flow.executor.systemPrompt');
|
|
942
|
+
const plan = parseFlowPlan(flowCfg?.plan);
|
|
943
|
+
const nodes = parseCompositionNodes(flowCfg?.nodes, bounds);
|
|
944
|
+
return {
|
|
945
|
+
// Declared nodes ARE the plan ⇒ this level is static (keep the spec honest:
|
|
946
|
+
// buildFromComposition routes a node-bearing level to a StaticPlanner).
|
|
947
|
+
planner: (nodes ? 'static' : plannerType),
|
|
948
|
+
granularity: granularity,
|
|
949
|
+
...(plan ? { plan } : {}),
|
|
950
|
+
...(nodes ? { nodes } : {}),
|
|
951
|
+
executor: executor,
|
|
952
|
+
finalizer: 'llm',
|
|
953
|
+
...(plannerSystemPrompt ? { plannerSystemPrompt } : {}),
|
|
954
|
+
...(executorSystemPrompt ? { executorSystemPrompt } : {}),
|
|
955
|
+
...bounds,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/** Validate an optional system-prompt override: must be a non-empty string. */
|
|
959
|
+
function parseSystemPromptOverride(raw, label) {
|
|
960
|
+
if (raw === undefined || raw === null)
|
|
961
|
+
return undefined;
|
|
962
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
963
|
+
throw new Error(`coordinator.${label} must be a non-empty string`);
|
|
964
|
+
return raw;
|
|
965
|
+
}
|
|
966
|
+
/** Parse composition nodes; a node with a nested `flow` recurses into a sub-spec. */
|
|
967
|
+
function parseCompositionNodes(raw, bounds) {
|
|
968
|
+
if (!Array.isArray(raw))
|
|
969
|
+
return undefined;
|
|
970
|
+
const nodes = raw
|
|
971
|
+
.filter((n) => !!n && typeof n.goal === 'string')
|
|
972
|
+
.map((n, i) => ({
|
|
973
|
+
id: typeof n.id === 'string' && n.id ? n.id : `n${i}`,
|
|
974
|
+
goal: n.goal,
|
|
975
|
+
...(Array.isArray(n.dependsOn)
|
|
976
|
+
? {
|
|
977
|
+
dependsOn: n.dependsOn.filter((d) => typeof d === 'string'),
|
|
978
|
+
}
|
|
979
|
+
: {}),
|
|
980
|
+
...(n.flow ? { flow: parseNestedFlowSpec(n.flow, bounds) } : {}),
|
|
981
|
+
}));
|
|
982
|
+
return nodes.length > 0 ? nodes : undefined;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Parse stepper coordinator configuration from a raw config object.
|
|
986
|
+
*
|
|
987
|
+
* Supports:
|
|
988
|
+
* - `mode` (string) — default 'planned-react'; one of cyclic-react | planned-react
|
|
989
|
+
* - `stepper.maxParallelSteps` (number) — default 4
|
|
990
|
+
* - `stepper.maxDepth` (number) — default 4
|
|
991
|
+
* - `stepper.tokenBudget` (number) — default 1,000,000
|
|
992
|
+
* - `stepper.reviewer.atDepths` (number[] | 'all') — default [0,1]; 'all' means accept any depth
|
|
993
|
+
*/
|
|
994
|
+
export function parseStepperCoordinatorConfig(coord) {
|
|
995
|
+
const mode = coord.mode ?? 'planned-react';
|
|
996
|
+
if (!MODES.has(mode))
|
|
997
|
+
throw new Error(`unknown coordinator.mode '${String(coord.mode)}'`);
|
|
998
|
+
// Tool permissioning is the MCP SERVER's responsibility — whatever it exposes
|
|
999
|
+
// via tools/list is allowed. The agent does not classify tools (read-only vs
|
|
1000
|
+
// mutating); there is no agent-side gate. The consumer wires the agent to a
|
|
1001
|
+
// server that exposes only the permitted tools (e.g. a read-only MCP proxy).
|
|
1002
|
+
const stepper = coord.stepper ?? {};
|
|
1003
|
+
const reviewerCfg = stepper.reviewer ?? {};
|
|
1004
|
+
const atDepths = reviewerCfg.atDepths ?? [0, 1];
|
|
1005
|
+
const reviewerAtDepths = atDepths === 'all'
|
|
1006
|
+
? { has: () => true }
|
|
1007
|
+
: (() => {
|
|
1008
|
+
const s = new Set(atDepths);
|
|
1009
|
+
return { has: (d) => s.has(d) };
|
|
1010
|
+
})();
|
|
1011
|
+
const knowledgeSeed = Array.isArray(coord.knowledgeSeed)
|
|
1012
|
+
? coord.knowledgeSeed
|
|
1013
|
+
.filter((e) => e && typeof e.content === 'string' && e.content.trim() !== '')
|
|
1014
|
+
.map((e) => ({
|
|
1015
|
+
content: e.content,
|
|
1016
|
+
artifactType: typeof e.artifactType === 'string' && e.artifactType
|
|
1017
|
+
? e.artifactType
|
|
1018
|
+
: 'guidance',
|
|
1019
|
+
}))
|
|
1020
|
+
: [];
|
|
1021
|
+
// Resolve the program flow: explicit `coordinator.flow` overrides the
|
|
1022
|
+
// mode-derived preset per component. `mode` thus becomes a preset alias.
|
|
1023
|
+
const preset = MODE_FLOW_PRESET[mode];
|
|
1024
|
+
const flowCfg = coord.flow;
|
|
1025
|
+
const plannerType = flowCfg?.planner?.type ?? preset.planner;
|
|
1026
|
+
if (!['none', 'llm', 'static'].includes(plannerType))
|
|
1027
|
+
throw new Error(`coordinator.flow.planner.type must be none|llm|static`);
|
|
1028
|
+
const granularity = flowCfg?.planner?.granularity ?? 'shallow';
|
|
1029
|
+
if (!['shallow', 'detailed'].includes(granularity))
|
|
1030
|
+
throw new Error(`coordinator.flow.planner.granularity must be shallow|detailed`);
|
|
1031
|
+
const executorType = flowCfg?.executor?.type ?? preset.executor;
|
|
1032
|
+
if (!['simple', 'cyclic-react'].includes(executorType))
|
|
1033
|
+
throw new Error(`coordinator.flow.executor.type must be simple|cyclic-react (recursive is deferred to 18.1)`);
|
|
1034
|
+
const finalizerType = flowCfg?.finalizer?.type ?? 'llm';
|
|
1035
|
+
if (finalizerType !== 'llm')
|
|
1036
|
+
throw new Error(`coordinator.flow.finalizer.type 'passthrough' is not yet implemented (use 'llm')`);
|
|
1037
|
+
const plannerSystemPrompt = parseSystemPromptOverride(flowCfg?.planner?.systemPrompt, 'flow.planner.systemPrompt');
|
|
1038
|
+
const executorSystemPrompt = parseSystemPromptOverride(flowCfg?.executor?.systemPrompt, 'flow.executor.systemPrompt');
|
|
1039
|
+
const plan = parseFlowPlan(flowCfg?.plan);
|
|
1040
|
+
const maxParallelSteps = Number(stepper.maxParallelSteps ?? 4);
|
|
1041
|
+
const maxDepth = Number(stepper.maxDepth ?? 4);
|
|
1042
|
+
const tokenBudget = Number(stepper.tokenBudget ?? 1_000_000);
|
|
1043
|
+
const formalizeTask = coord.formalizeTask === true;
|
|
1044
|
+
// Nested composition nodes inherit the root bounds (a sub-cycle uses the same
|
|
1045
|
+
// parallelism / depth / budget / safety unless the runtime threads otherwise).
|
|
1046
|
+
const bounds = {
|
|
1047
|
+
reviewerAtDepths,
|
|
1048
|
+
maxParallelSteps,
|
|
1049
|
+
maxDepth,
|
|
1050
|
+
tokenBudget,
|
|
1051
|
+
formalizeTask,
|
|
1052
|
+
};
|
|
1053
|
+
const nodes = parseCompositionNodes(flowCfg?.nodes, bounds);
|
|
1054
|
+
// Static planner needs an explicit plan OR declared nodes (nodes ARE the plan).
|
|
1055
|
+
if (plannerType === 'static' && !plan && !nodes)
|
|
1056
|
+
throw new Error(`coordinator.flow.planner.type 'static' requires coordinator.flow.plan or coordinator.flow.nodes`);
|
|
1057
|
+
return {
|
|
1058
|
+
mode,
|
|
1059
|
+
reviewerAtDepths,
|
|
1060
|
+
maxParallelSteps,
|
|
1061
|
+
maxDepth,
|
|
1062
|
+
tokenBudget,
|
|
1063
|
+
knowledgeSeed,
|
|
1064
|
+
formalizeTask,
|
|
1065
|
+
flow: {
|
|
1066
|
+
// Declared root nodes ARE the plan ⇒ static at the root (honest spec).
|
|
1067
|
+
planner: (nodes ? 'static' : plannerType),
|
|
1068
|
+
granularity: granularity,
|
|
1069
|
+
executor: executorType,
|
|
1070
|
+
finalizer: 'llm',
|
|
1071
|
+
...(plannerSystemPrompt ? { plannerSystemPrompt } : {}),
|
|
1072
|
+
...(executorSystemPrompt ? { executorSystemPrompt } : {}),
|
|
1073
|
+
...(plan ? { plan } : {}),
|
|
1074
|
+
...(nodes ? { nodes } : {}),
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
652
1078
|
//# sourceMappingURL=config.js.map
|