@mcp-abap-adt/llm-agent-server-libs 18.1.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/README.md +42 -0
- package/dist/factories/cyclic-factory.d.ts +48 -0
- package/dist/factories/cyclic-factory.d.ts.map +1 -0
- package/dist/factories/cyclic-factory.js +35 -0
- package/dist/factories/cyclic-factory.js.map +1 -0
- package/dist/factories/dag-factory.d.ts +13 -0
- package/dist/factories/dag-factory.d.ts.map +1 -0
- package/dist/factories/dag-factory.js +14 -0
- package/dist/factories/dag-factory.js.map +1 -0
- package/dist/factories/deep-stepper-factory.d.ts +12 -0
- package/dist/factories/deep-stepper-factory.d.ts.map +1 -0
- package/dist/factories/deep-stepper-factory.js +13 -0
- package/dist/factories/deep-stepper-factory.js.map +1 -0
- package/dist/factories/index.d.ts +7 -0
- package/dist/factories/index.d.ts.map +1 -0
- package/dist/factories/index.js +6 -0
- package/dist/factories/index.js.map +1 -0
- package/dist/factories/linear-factory.d.ts +13 -0
- package/dist/factories/linear-factory.d.ts.map +1 -0
- package/dist/factories/linear-factory.js +14 -0
- package/dist/factories/linear-factory.js.map +1 -0
- package/dist/factories/planned-factory.d.ts +12 -0
- package/dist/factories/planned-factory.d.ts.map +1 -0
- package/dist/factories/planned-factory.js +13 -0
- package/dist/factories/planned-factory.js.map +1 -0
- package/dist/generated/version.d.ts +2 -0
- package/dist/generated/version.d.ts.map +1 -0
- package/dist/generated/version.js +3 -0
- package/dist/generated/version.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- 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 +110 -0
- package/dist/smart-agent/build-stepper-root.d.ts.map +1 -0
- package/dist/smart-agent/build-stepper-root.js +270 -0
- package/dist/smart-agent/build-stepper-root.js.map +1 -0
- package/dist/smart-agent/config.d.ts +282 -0
- package/dist/smart-agent/config.d.ts.map +1 -0
- package/dist/smart-agent/config.js +1109 -0
- package/dist/smart-agent/config.js.map +1 -0
- 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/pipeline.d.ts +89 -0
- package/dist/smart-agent/pipeline.d.ts.map +1 -0
- package/dist/smart-agent/pipeline.js +8 -0
- package/dist/smart-agent/pipeline.js.map +1 -0
- package/dist/smart-agent/resolve-agent-embedder.d.ts +34 -0
- package/dist/smart-agent/resolve-agent-embedder.d.ts.map +1 -0
- package/dist/smart-agent/resolve-agent-embedder.js +48 -0
- package/dist/smart-agent/resolve-agent-embedder.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 +525 -0
- package/dist/smart-agent/smart-server.d.ts.map +1 -0
- package/dist/smart-agent/smart-server.js +2309 -0
- package/dist/smart-agent/smart-server.js.map +1 -0
- 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 +230 -0
- package/dist/smart-agent/stepper-coordinator-handler.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared config utilities for SmartServer.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { AutoActivation, ExplicitActivation, HybridDispatch, LlmFinalizer, OneShotPlanning, PassthroughFinalizer, ReplanOnErrorPlanning, ScoreThresholdToolSelection, SelfDispatch, SkillStepsPlanning, SubAgentDispatch, TemplateFinalizer, TopKToolSelection, } from '@mcp-abap-adt/llm-agent-libs';
|
|
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
|
+
}
|
|
172
|
+
export function resolveCoordinatorPlanning(name, plannerLlm) {
|
|
173
|
+
switch (name) {
|
|
174
|
+
case 'one-shot':
|
|
175
|
+
return new OneShotPlanning(plannerLlm);
|
|
176
|
+
case 'replan-on-error':
|
|
177
|
+
return new ReplanOnErrorPlanning(plannerLlm);
|
|
178
|
+
case 'skill-steps':
|
|
179
|
+
// SkillStepsPlanning reads `ctx.activeSkillMeta` (populated by
|
|
180
|
+
// CoordinatorHandler from `ctx.selectedSkills`). No planner LLM
|
|
181
|
+
// needed — the plan comes directly from the skill's `steps:` block.
|
|
182
|
+
return new SkillStepsPlanning();
|
|
183
|
+
default:
|
|
184
|
+
throw new Error(`Unknown coordinator.planning strategy: '${name}'. Allowed: one-shot, replan-on-error, skill-steps.`);
|
|
185
|
+
}
|
|
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
|
+
}
|
|
196
|
+
export function resolveCoordinatorDispatch(name, fallbackLlm, contextBuilder) {
|
|
197
|
+
switch (name) {
|
|
198
|
+
case 'subagent':
|
|
199
|
+
return new SubAgentDispatch(contextBuilder);
|
|
200
|
+
case 'self':
|
|
201
|
+
if (!fallbackLlm) {
|
|
202
|
+
throw new Error('coordinator.dispatch=self requires a planner or main LLM');
|
|
203
|
+
}
|
|
204
|
+
return new SelfDispatch(fallbackLlm);
|
|
205
|
+
case 'hybrid':
|
|
206
|
+
if (!fallbackLlm) {
|
|
207
|
+
throw new Error('coordinator.dispatch=hybrid requires a planner or main LLM');
|
|
208
|
+
}
|
|
209
|
+
return new HybridDispatch(new SubAgentDispatch(contextBuilder), new SelfDispatch(fallbackLlm));
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`Unknown coordinator.dispatch strategy: '${name}'. Allowed: subagent, self, hybrid.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function resolveCoordinatorActivation(name) {
|
|
215
|
+
switch (name) {
|
|
216
|
+
case 'auto':
|
|
217
|
+
return new AutoActivation();
|
|
218
|
+
case 'explicit':
|
|
219
|
+
return new ExplicitActivation();
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`Unknown coordinator.activation strategy: '${name}'. Allowed: auto, explicit.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export function resolveToolSelectionStrategy(name, params) {
|
|
225
|
+
switch (name) {
|
|
226
|
+
case 'top-k':
|
|
227
|
+
return new TopKToolSelection();
|
|
228
|
+
case 'threshold': {
|
|
229
|
+
const minScore = params?.minScore;
|
|
230
|
+
if (typeof minScore !== 'number') {
|
|
231
|
+
throw new Error("agent.toolSelection.strategy 'threshold' requires a numeric 'minScore'");
|
|
232
|
+
}
|
|
233
|
+
return new ScoreThresholdToolSelection(minScore);
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
throw new Error(`Unknown agent.toolSelection.strategy '${name}'. Allowed: top-k, threshold.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const VALID_PROVIDERS = [
|
|
240
|
+
'openai',
|
|
241
|
+
'anthropic',
|
|
242
|
+
'deepseek',
|
|
243
|
+
'sap-ai-sdk',
|
|
244
|
+
'ollama',
|
|
245
|
+
];
|
|
246
|
+
const VALID_RAG_TYPES = [
|
|
247
|
+
'in-memory',
|
|
248
|
+
'qdrant',
|
|
249
|
+
'hana-vector',
|
|
250
|
+
'pg-vector',
|
|
251
|
+
];
|
|
252
|
+
export class ConfigValidationError extends Error {
|
|
253
|
+
constructor(issues) {
|
|
254
|
+
super(`Configuration error in smart-server.yaml:\n${issues
|
|
255
|
+
.map((i) => ` - ${i}`)
|
|
256
|
+
.join('\n')}\nSet these fields in your YAML and restart.`);
|
|
257
|
+
this.name = 'ConfigValidationError';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export const YAML_TEMPLATE = `port: 4004
|
|
261
|
+
host: 0.0.0.0
|
|
262
|
+
|
|
263
|
+
# Request routing mode:
|
|
264
|
+
# hard — Fully managed context. Ignores client history/system prompt. Uses RAG + internal MCP tools only.
|
|
265
|
+
# pass — Transparent proxy. Logs everything but modifies nothing.
|
|
266
|
+
# smart — Hybrid. Preserves client history but enriches it with RAG context and MCP tools based on analysis. (default)
|
|
267
|
+
mode: smart
|
|
268
|
+
|
|
269
|
+
llm:
|
|
270
|
+
provider: deepseek # deepseek | openai | anthropic | sap-ai-sdk | ollama
|
|
271
|
+
apiKey: \${DEEPSEEK_API_KEY} # not required for ollama / sap-ai-sdk
|
|
272
|
+
model: deepseek-chat
|
|
273
|
+
temperature: 0.7
|
|
274
|
+
classifierTemperature: 0.1
|
|
275
|
+
|
|
276
|
+
rag:
|
|
277
|
+
type: in-memory # in-memory | qdrant | hana-vector | pg-vector
|
|
278
|
+
embedder: ollama # Embedder to use: ollama | openai | sap-ai-core | <custom>
|
|
279
|
+
url: http://localhost:11434
|
|
280
|
+
model: bge-m3
|
|
281
|
+
# resourceGroup: default # SAP AI Core resource group (sap-ai-core embedder)
|
|
282
|
+
# scenario: orchestration # SAP AI Core scenario: orchestration (default) | foundation-models
|
|
283
|
+
# collectionName: llm-agent # Collection/table name (qdrant | hana-vector | pg-vector)
|
|
284
|
+
dedupThreshold: 0.92
|
|
285
|
+
vectorWeight: 0.7 # Semantic similarity weight (0..1)
|
|
286
|
+
keywordWeight: 0.3 # Lexical matching weight (0..1)
|
|
287
|
+
|
|
288
|
+
mcp:
|
|
289
|
+
# type: none | http | stdio
|
|
290
|
+
# To disable MCP, set type to 'none'
|
|
291
|
+
type: http
|
|
292
|
+
url: http://localhost:3001/mcp/stream/http
|
|
293
|
+
|
|
294
|
+
agent:
|
|
295
|
+
externalToolsValidationMode: permissive # permissive | strict
|
|
296
|
+
maxIterations: 10
|
|
297
|
+
maxToolCalls: 30
|
|
298
|
+
toolUnavailableTtlMs: 600000 # Temporary tool blacklist TTL (ms)
|
|
299
|
+
ragQueryK: 10
|
|
300
|
+
# contextBudgetTokens: 4000 # Max tokens for RAG context in system prompt (0 = no limit)
|
|
301
|
+
# semanticHistoryEnabled: false # Enable semantic history via RAG
|
|
302
|
+
# historyRecencyWindow: 4 # Last N messages from client history in LLM context
|
|
303
|
+
# historyTurnSummaryPrompt: "..." # LLM prompt for turn summarization
|
|
304
|
+
showReasoning: false # Explain strategy at start of response
|
|
305
|
+
historyAutoSummarizeLimit: 10 # History length to trigger compression
|
|
306
|
+
queryExpansionEnabled: false # Expand RAG queries with LLM-generated synonyms
|
|
307
|
+
toolResultCacheTtlMs: 300000 # Tool result cache TTL (ms); 0 to disable
|
|
308
|
+
sessionTokenBudget: 0 # Multi-turn token budget; 0 to disable
|
|
309
|
+
# ragTranslateEnabled: true # Translate non-ASCII RAG queries to English (default: true)
|
|
310
|
+
# classificationEnabled: false # Enable for custom pipelines with multi-store routing
|
|
311
|
+
# toolReselectPerIteration: false # Re-select tools via RAG on each tool-loop iteration
|
|
312
|
+
# llmCallStrategy: streaming # streaming | non-streaming | fallback
|
|
313
|
+
# streamMode: full # full | final — streaming behavior for tool loops
|
|
314
|
+
# heartbeatIntervalMs: 5000 # SSE heartbeat interval during tool execution (ms)
|
|
315
|
+
# healthTimeoutMs: 5000 # Health check probe timeout (ms); increase for slow providers (SAP AI Core: 15000)
|
|
316
|
+
# retry: # LLM retry config for 429/5xx errors
|
|
317
|
+
# maxAttempts: 3
|
|
318
|
+
# backoffMs: 1000
|
|
319
|
+
# retryOn: [429, 500, 502, 503]
|
|
320
|
+
# retryOnMidStream: ['SSE stream'] # Substrings triggering mid-stream retry
|
|
321
|
+
|
|
322
|
+
# --- Advanced Multi-Model Pipeline (optional) -------------------------------
|
|
323
|
+
# Use this section to assign different models for different internal tasks.
|
|
324
|
+
# pipeline:
|
|
325
|
+
# llm:
|
|
326
|
+
# main:
|
|
327
|
+
# provider: deepseek # deepseek | openai | anthropic | sap-ai-sdk
|
|
328
|
+
# apiKey: \${DEEPSEEK_API_KEY}
|
|
329
|
+
# model: deepseek-chat
|
|
330
|
+
# temperature: 0.7
|
|
331
|
+
# streaming: true # false to disable streaming for this provider
|
|
332
|
+
# classifier: # optional; if absent, main config is reused
|
|
333
|
+
# provider: deepseek
|
|
334
|
+
# apiKey: \${DEEPSEEK_API_KEY}
|
|
335
|
+
# model: deepseek-chat
|
|
336
|
+
# temperature: 0.1
|
|
337
|
+
# helper: # optional; if absent, main config is reused
|
|
338
|
+
# provider: deepseek
|
|
339
|
+
# apiKey: \${DEEPSEEK_API_KEY}
|
|
340
|
+
# model: deepseek-chat
|
|
341
|
+
# temperature: 0.1
|
|
342
|
+
#
|
|
343
|
+
# rag:
|
|
344
|
+
# tools:
|
|
345
|
+
# type: qdrant
|
|
346
|
+
# url: http://qdrant:6333
|
|
347
|
+
# embedder: openai # ollama | openai | <custom registered name>
|
|
348
|
+
# model: text-embedding-3-small
|
|
349
|
+
# apiKey: \${OPENAI_API_KEY}
|
|
350
|
+
# history:
|
|
351
|
+
# type: in-memory
|
|
352
|
+
#
|
|
353
|
+
# mcp:
|
|
354
|
+
# - type: http
|
|
355
|
+
# url: http://localhost:3001/mcp/stream/http
|
|
356
|
+
|
|
357
|
+
# --- Structured Pipeline (optional) -------------------------------------------
|
|
358
|
+
# Replaces the hardcoded orchestration flow with a YAML-defined stage tree.
|
|
359
|
+
# When absent, the default flow runs unchanged (full backwards compatibility).
|
|
360
|
+
#
|
|
361
|
+
# pipeline:
|
|
362
|
+
# version: "1"
|
|
363
|
+
# stages:
|
|
364
|
+
# - id: classify
|
|
365
|
+
# type: classify
|
|
366
|
+
# - id: summarize
|
|
367
|
+
# type: summarize
|
|
368
|
+
# - id: rag-retrieval
|
|
369
|
+
# type: parallel
|
|
370
|
+
# when: "shouldRetrieve"
|
|
371
|
+
# stages:
|
|
372
|
+
# - { id: translate, type: translate }
|
|
373
|
+
# - { id: expand, type: expand }
|
|
374
|
+
# after:
|
|
375
|
+
# - id: rag-queries
|
|
376
|
+
# type: parallel
|
|
377
|
+
# stages:
|
|
378
|
+
# - { id: tools, type: rag-query, config: { store: tools, k: 10 } }
|
|
379
|
+
# - { id: history, type: rag-query, config: { store: history, k: 5 } }
|
|
380
|
+
# - { id: rerank, type: rerank }
|
|
381
|
+
# - { id: tool-select, type: tool-select }
|
|
382
|
+
# - id: assemble
|
|
383
|
+
# type: assemble
|
|
384
|
+
# - id: tool-loop
|
|
385
|
+
# type: tool-loop
|
|
386
|
+
|
|
387
|
+
# prompts:
|
|
388
|
+
# system: "You are a helpful assistant specialized in SAP ABAP development."
|
|
389
|
+
# classifier: |
|
|
390
|
+
# You are an intent classifier... (see source for full default prompt)
|
|
391
|
+
# reasoning: |
|
|
392
|
+
# IMPORTANT: Always start your response with a brief <reasoning> block...
|
|
393
|
+
# ragTranslate: |
|
|
394
|
+
# Translate the user request to English for search purposes...
|
|
395
|
+
# historySummary: |
|
|
396
|
+
# Summarize the conversation so far...
|
|
397
|
+
|
|
398
|
+
log: smart-server.log # path to log file; omit for stdout
|
|
399
|
+
# logDir: sessions # Directory for detailed session debug logs
|
|
400
|
+
# pluginDir: ./my-plugins # Additional plugin directory (loaded after defaults)
|
|
401
|
+
|
|
402
|
+
# subagents: # Optional: nested agents callable from pipeline
|
|
403
|
+
# - name: code-reviewer # Used as stage config: { agent: code-reviewer }
|
|
404
|
+
# description: | # Optional. Shown to the Coordinator planner LLM
|
|
405
|
+
# Reviews code and returns # so it can pick this agent for the right step.
|
|
406
|
+
# structured JSON.
|
|
407
|
+
# config: ./agents/code-reviewer.yaml
|
|
408
|
+
|
|
409
|
+
# coordinator: # Optional: enable autonomous plan-execute loop
|
|
410
|
+
# planning: one-shot # one-shot | replan-on-error | skill-steps
|
|
411
|
+
# dispatch: subagent # subagent | self | hybrid
|
|
412
|
+
# activation: explicit # explicit (default) | auto
|
|
413
|
+
# plannerLlm: main # main | planner | helper (unused by skill-steps)
|
|
414
|
+
# maxSteps: 12
|
|
415
|
+
# maxRetriesPerStep: 1
|
|
416
|
+
# failPolicy: abort # abort | continue
|
|
417
|
+
# maxLayer: 1 # DEPRECATED — accepted but ignored (nested
|
|
418
|
+
# # dispatch removed; subagents are leaves)
|
|
419
|
+
`;
|
|
420
|
+
export function resolveEnvVars(value, env = process.env) {
|
|
421
|
+
if (typeof value === 'string')
|
|
422
|
+
return value.replace(/\$\{([^}:]+)(?::-(.*?))?\}/g, (_, name, fallback) => env[name] || fallback || '');
|
|
423
|
+
if (Array.isArray(value))
|
|
424
|
+
return value.map((v) => resolveEnvVars(v, env));
|
|
425
|
+
if (value !== null && typeof value === 'object')
|
|
426
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [
|
|
427
|
+
k,
|
|
428
|
+
resolveEnvVars(v, env),
|
|
429
|
+
]));
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
export function loadYamlConfig(filePath, env = process.env) {
|
|
433
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
434
|
+
return resolveEnvVars(parseYaml(raw), env);
|
|
435
|
+
}
|
|
436
|
+
export function generateConfigTemplate(outputPath) {
|
|
437
|
+
fs.writeFileSync(outputPath, YAML_TEMPLATE, 'utf8');
|
|
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
|
+
}
|
|
466
|
+
const get = (obj, ...keys) => keys.reduce((o, k) => {
|
|
467
|
+
if (o !== null && typeof o === 'object' && k in o) {
|
|
468
|
+
return o[k];
|
|
469
|
+
}
|
|
470
|
+
return undefined;
|
|
471
|
+
}, obj);
|
|
472
|
+
function checkLlmRole(label, role, requireModel, env, issues) {
|
|
473
|
+
const provider = role?.provider;
|
|
474
|
+
if (!provider) {
|
|
475
|
+
issues.push(`${label}.provider: required (one of: openai, anthropic, deepseek, sap-ai-sdk, ollama)`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// `as readonly string[]` is required so .includes() accepts an arbitrary
|
|
479
|
+
// string; do not "simplify" — it preserves the const-tuple narrowing.
|
|
480
|
+
if (!VALID_PROVIDERS.includes(provider)) {
|
|
481
|
+
issues.push(`${label}.provider: "${provider}" is invalid (one of: openai, anthropic, deepseek, sap-ai-sdk, ollama)`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (requireModel && !role?.model) {
|
|
485
|
+
issues.push(`${label}.model: required (string)`);
|
|
486
|
+
}
|
|
487
|
+
if (provider === 'openai' ||
|
|
488
|
+
provider === 'anthropic' ||
|
|
489
|
+
provider === 'deepseek') {
|
|
490
|
+
if (!role?.apiKey) {
|
|
491
|
+
issues.push(`${provider} requires ${label}.apiKey to resolve to a non-empty value (typically via \${${provider.toUpperCase()}_API_KEY} env reference).`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (provider === 'sap-ai-sdk') {
|
|
495
|
+
if (!env.AICORE_SERVICE_KEY) {
|
|
496
|
+
issues.push('sap-ai-sdk requires the AICORE_SERVICE_KEY env var to be set with the SAP AI Core service-key JSON content. None found.');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// ollama: no credential check.
|
|
500
|
+
}
|
|
501
|
+
function checkRagStore(label, store, issues) {
|
|
502
|
+
if (!store)
|
|
503
|
+
return;
|
|
504
|
+
const ragType = store.type;
|
|
505
|
+
if (!ragType) {
|
|
506
|
+
issues.push(`${label}.type: required (one of: in-memory, qdrant, hana-vector, pg-vector)`);
|
|
507
|
+
}
|
|
508
|
+
else if (ragType === 'ollama' || ragType === 'openai') {
|
|
509
|
+
issues.push(`${label}.type: "${ragType}" is an embedder, not a store — use \`type: in-memory\` with \`embedder: ${ragType}\` (or a real store: qdrant, hana-vector, pg-vector)`);
|
|
510
|
+
}
|
|
511
|
+
else if (!VALID_RAG_TYPES.includes(ragType)) {
|
|
512
|
+
issues.push(`${label}.type: "${ragType}" is invalid (one of: in-memory, qdrant, hana-vector, pg-vector)`);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
if (ragType === 'qdrant' && !store.url) {
|
|
516
|
+
issues.push(`${label}.url: required for ${label}.type qdrant`);
|
|
517
|
+
}
|
|
518
|
+
if ((ragType === 'hana-vector' || ragType === 'pg-vector') &&
|
|
519
|
+
!store.collectionName) {
|
|
520
|
+
issues.push(`${label}.collectionName: required for ${label}.type ${ragType}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Blocklist (NOT allowlist): consumers can register custom embedder
|
|
524
|
+
// factories, so only known embedder-less providers are hard-rejected here.
|
|
525
|
+
const embedder = store.embedder;
|
|
526
|
+
if (embedder === 'deepseek' || embedder === 'anthropic') {
|
|
527
|
+
issues.push(`${label}.embedder: "${embedder}" provider has no embedder; embedding-capable providers are ollama, openai, sap-ai-core`);
|
|
528
|
+
}
|
|
529
|
+
// Require model when an embedder is used: vector stores always use an
|
|
530
|
+
// embedder; in-memory uses one only when embedder is explicitly set.
|
|
531
|
+
const usesEmbedder = ragType === 'qdrant' ||
|
|
532
|
+
ragType === 'hana-vector' ||
|
|
533
|
+
ragType === 'pg-vector' ||
|
|
534
|
+
(ragType === 'in-memory' && embedder != null);
|
|
535
|
+
if (usesEmbedder && !store.model) {
|
|
536
|
+
issues.push(`${label}.model: required when an embedder is used (e.g. bge-m3 for ollama)`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function validateLlmEntry(label, cfg, required, env, issues) {
|
|
540
|
+
checkLlmRole(label, cfg, required, env, issues);
|
|
541
|
+
}
|
|
542
|
+
function validateResolvedConfig(_resolved, yaml, env) {
|
|
543
|
+
const issues = [];
|
|
544
|
+
const usingPipeline = !!get(yaml, 'pipeline', 'llm', 'main');
|
|
545
|
+
if (usingPipeline) {
|
|
546
|
+
const llmCfg = get(yaml, 'pipeline', 'llm');
|
|
547
|
+
checkLlmRole('pipeline.llm.main', llmCfg?.main, true, env, issues);
|
|
548
|
+
if (llmCfg?.classifier) {
|
|
549
|
+
checkLlmRole('pipeline.llm.classifier', llmCfg.classifier, false, env, issues);
|
|
550
|
+
}
|
|
551
|
+
if (llmCfg?.helper) {
|
|
552
|
+
checkLlmRole('pipeline.llm.helper', llmCfg.helper, false, env, issues);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
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');
|
|
564
|
+
}
|
|
565
|
+
else if (typeof rawLlm.provider === 'string') {
|
|
566
|
+
// Flat shape — existing behaviour.
|
|
567
|
+
validateLlmEntry('llm', rawLlm, true, env, issues);
|
|
568
|
+
}
|
|
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
|
+
}
|
|
583
|
+
}
|
|
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
|
+
}
|
|
604
|
+
if (get(yaml, 'rag')) {
|
|
605
|
+
checkRagStore('rag', get(yaml, 'rag'), issues);
|
|
606
|
+
}
|
|
607
|
+
const pipelineRag = get(yaml, 'pipeline', 'rag');
|
|
608
|
+
if (pipelineRag) {
|
|
609
|
+
for (const [name, store] of Object.entries(pipelineRag)) {
|
|
610
|
+
checkRagStore(`pipeline.rag.${name}`, store, issues);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (issues.length > 0)
|
|
614
|
+
throw new ConfigValidationError([...new Set(issues)]);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Recursively parse the top-level `subagents:` block from a YAML config.
|
|
618
|
+
*
|
|
619
|
+
* Each entry references a sibling YAML file whose resolved config (sans
|
|
620
|
+
* `subagents:` itself — nested orchestration is rejected) becomes a
|
|
621
|
+
* `SmartServerSubAgentConfig`. Relative `config:` paths are resolved
|
|
622
|
+
* against `configPath`'s directory. A `subagents:` block inside a
|
|
623
|
+
* sub-YAML is rejected to guard against unbounded recursion.
|
|
624
|
+
*
|
|
625
|
+
* Returns `undefined` when the parent YAML has no `subagents:` block or
|
|
626
|
+
* when `configPath` is not provided (relative paths cannot be resolved).
|
|
627
|
+
*/
|
|
628
|
+
function parseSubAgents(yaml, configPath, args, env) {
|
|
629
|
+
const raw = yaml.subagents;
|
|
630
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
631
|
+
return undefined;
|
|
632
|
+
if (!configPath) {
|
|
633
|
+
throw new Error("subagents: parent YAML must be loaded from a file path so 'config' entries can be resolved");
|
|
634
|
+
}
|
|
635
|
+
const baseDir = path.dirname(path.resolve(configPath));
|
|
636
|
+
const out = [];
|
|
637
|
+
for (const entry of raw) {
|
|
638
|
+
if (!entry ||
|
|
639
|
+
typeof entry !== 'object' ||
|
|
640
|
+
typeof entry.name !== 'string' ||
|
|
641
|
+
typeof entry.config !== 'string') {
|
|
642
|
+
throw new Error(`subagents[]: each entry needs 'name' and 'config' (got ${JSON.stringify(entry)})`);
|
|
643
|
+
}
|
|
644
|
+
const name = entry.name;
|
|
645
|
+
const cfgRel = entry.config;
|
|
646
|
+
const description = entry.description;
|
|
647
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
648
|
+
throw new Error(`subagents[].description must be a string when present (got ${JSON.stringify(description)})`);
|
|
649
|
+
}
|
|
650
|
+
const subConfigPath = path.isAbsolute(cfgRel)
|
|
651
|
+
? cfgRel
|
|
652
|
+
: path.resolve(baseDir, cfgRel);
|
|
653
|
+
const subYaml = loadYamlConfig(subConfigPath, env);
|
|
654
|
+
if (subYaml.subagents !== undefined) {
|
|
655
|
+
throw new Error(`subagent '${name}' must not define its own 'subagents:' (nested orchestration is not supported)`);
|
|
656
|
+
}
|
|
657
|
+
// Loudly reject fields that the sub-agent builder silently drops today.
|
|
658
|
+
// Keeps the contract honest: if a sub YAML declares these, it gets an
|
|
659
|
+
// error rather than a misleadingly-quiet partial config.
|
|
660
|
+
const unsupported = [];
|
|
661
|
+
if (subYaml.pluginDir !== undefined) {
|
|
662
|
+
unsupported.push('pluginDir');
|
|
663
|
+
}
|
|
664
|
+
if (subYaml.clientAdapter !== undefined) {
|
|
665
|
+
unsupported.push('clientAdapter');
|
|
666
|
+
}
|
|
667
|
+
if (subYaml.circuitBreaker !== undefined) {
|
|
668
|
+
unsupported.push('circuitBreaker');
|
|
669
|
+
}
|
|
670
|
+
const subPipeline = subYaml.pipeline;
|
|
671
|
+
if (subPipeline && typeof subPipeline === 'object') {
|
|
672
|
+
const p = subPipeline;
|
|
673
|
+
if (p.reranker !== undefined)
|
|
674
|
+
unsupported.push('pipeline.reranker');
|
|
675
|
+
if (p.queryExpander !== undefined)
|
|
676
|
+
unsupported.push('pipeline.queryExpander');
|
|
677
|
+
if (p.outputValidator !== undefined)
|
|
678
|
+
unsupported.push('pipeline.outputValidator');
|
|
679
|
+
if (p.rag !== undefined &&
|
|
680
|
+
p.rag !== null &&
|
|
681
|
+
typeof p.rag === 'object' &&
|
|
682
|
+
!Array.isArray(p.rag)) {
|
|
683
|
+
unsupported.push('pipeline.rag');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (unsupported.length > 0) {
|
|
687
|
+
throw new Error(`subagent '${name}': unsupported fields [${unsupported.join(', ')}]`);
|
|
688
|
+
}
|
|
689
|
+
// Recursive call — we just verified the sub YAML has no `subagents:`, so
|
|
690
|
+
// the parseSubAgents call inside will short-circuit to undefined.
|
|
691
|
+
const subResolved = resolveSmartServerConfig(args, subYaml, env, {
|
|
692
|
+
configPath: subConfigPath,
|
|
693
|
+
});
|
|
694
|
+
out.push({ name, description, config: subResolved });
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env, options = {}) {
|
|
699
|
+
const flatApiKey = get(yaml, 'llm', 'apiKey') ?? '';
|
|
700
|
+
const pipelineApiKey = get(yaml, 'pipeline', 'llm', 'main', 'apiKey');
|
|
701
|
+
const apiKey = flatApiKey || pipelineApiKey || '';
|
|
702
|
+
const rawMcp = yaml.mcp;
|
|
703
|
+
const mcpIsArray = Array.isArray(rawMcp);
|
|
704
|
+
const mcpUrl = get(yaml, 'mcp', 'url');
|
|
705
|
+
const mcpCommand = get(yaml, 'mcp', 'command');
|
|
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));
|
|
710
|
+
const mcpType = (mcpTypeRaw === 'none' ? null : mcpTypeRaw);
|
|
711
|
+
const promptSystem = get(yaml, 'prompts', 'system') ?? null;
|
|
712
|
+
const promptClassifier = get(yaml, 'prompts', 'classifier') ?? null;
|
|
713
|
+
const promptReasoning = get(yaml, 'prompts', 'reasoning') ?? null;
|
|
714
|
+
const promptRagTranslate = get(yaml, 'prompts', 'ragTranslate') ?? null;
|
|
715
|
+
const promptHistorySummary = get(yaml, 'prompts', 'historySummary') ?? null;
|
|
716
|
+
const resolved = {
|
|
717
|
+
port: Number(args.port ?? get(yaml, 'port') ?? env.PORT ?? 4004),
|
|
718
|
+
host: args.host ?? get(yaml, 'host') ?? '0.0.0.0',
|
|
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,
|
|
731
|
+
rag: get(yaml, 'rag')
|
|
732
|
+
? {
|
|
733
|
+
type: get(yaml, 'rag', 'type'),
|
|
734
|
+
embedder: get(yaml, 'rag', 'embedder') ?? undefined,
|
|
735
|
+
url: get(yaml, 'rag', 'url'),
|
|
736
|
+
model: get(yaml, 'rag', 'model'),
|
|
737
|
+
collectionName: args['rag-collection-name'] ??
|
|
738
|
+
get(yaml, 'rag', 'collectionName') ??
|
|
739
|
+
undefined,
|
|
740
|
+
dedupThreshold: Number(get(yaml, 'rag', 'dedupThreshold') ?? 0.92),
|
|
741
|
+
vectorWeight: Number(get(yaml, 'rag', 'vectorWeight') ?? 0.7),
|
|
742
|
+
keywordWeight: Number(get(yaml, 'rag', 'keywordWeight') ?? 0.3),
|
|
743
|
+
...(get(yaml, 'rag', 'resourceGroup') !== undefined
|
|
744
|
+
? { resourceGroup: String(get(yaml, 'rag', 'resourceGroup')) }
|
|
745
|
+
: {}),
|
|
746
|
+
...(get(yaml, 'rag', 'scenario') !== undefined
|
|
747
|
+
? {
|
|
748
|
+
scenario: String(get(yaml, 'rag', 'scenario')),
|
|
749
|
+
}
|
|
750
|
+
: {}),
|
|
751
|
+
}
|
|
752
|
+
: undefined,
|
|
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,
|
|
769
|
+
agent: {
|
|
770
|
+
externalToolsValidationMode: (get(yaml, 'agent', 'externalToolsValidationMode') ?? 'permissive'),
|
|
771
|
+
maxIterations: Number(get(yaml, 'agent', 'maxIterations') ?? 10),
|
|
772
|
+
maxToolCalls: Number(get(yaml, 'agent', 'maxToolCalls') ?? 30),
|
|
773
|
+
toolUnavailableTtlMs: Number(get(yaml, 'agent', 'toolUnavailableTtlMs') ?? 600000),
|
|
774
|
+
ragQueryK: Number(get(yaml, 'agent', 'ragQueryK') ?? 10),
|
|
775
|
+
...(get(yaml, 'agent', 'contextBudgetTokens') !== undefined
|
|
776
|
+
? {
|
|
777
|
+
contextBudgetTokens: Number(get(yaml, 'agent', 'contextBudgetTokens')),
|
|
778
|
+
}
|
|
779
|
+
: {}),
|
|
780
|
+
...(get(yaml, 'agent', 'semanticHistoryEnabled') !== undefined
|
|
781
|
+
? {
|
|
782
|
+
semanticHistoryEnabled: Boolean(get(yaml, 'agent', 'semanticHistoryEnabled')),
|
|
783
|
+
}
|
|
784
|
+
: {}),
|
|
785
|
+
...(get(yaml, 'agent', 'historyRecencyWindow') !== undefined
|
|
786
|
+
? {
|
|
787
|
+
historyRecencyWindow: Number(get(yaml, 'agent', 'historyRecencyWindow')),
|
|
788
|
+
}
|
|
789
|
+
: {}),
|
|
790
|
+
...(get(yaml, 'agent', 'historyTurnSummaryPrompt') !== undefined
|
|
791
|
+
? {
|
|
792
|
+
historyTurnSummaryPrompt: String(get(yaml, 'agent', 'historyTurnSummaryPrompt')),
|
|
793
|
+
}
|
|
794
|
+
: {}),
|
|
795
|
+
showReasoning: Boolean(args['agent-show-reasoning'] ??
|
|
796
|
+
get(yaml, 'agent', 'showReasoning') ??
|
|
797
|
+
false),
|
|
798
|
+
historyAutoSummarizeLimit: Number(get(yaml, 'agent', 'historyAutoSummarizeLimit') ?? 10),
|
|
799
|
+
queryExpansionEnabled: Boolean(get(yaml, 'agent', 'queryExpansionEnabled') ?? false),
|
|
800
|
+
toolResultCacheTtlMs: Number(get(yaml, 'agent', 'toolResultCacheTtlMs') ?? 300000),
|
|
801
|
+
sessionTokenBudget: Number(get(yaml, 'agent', 'sessionTokenBudget') ?? 0),
|
|
802
|
+
...(get(yaml, 'agent', 'classificationEnabled') !== undefined
|
|
803
|
+
? {
|
|
804
|
+
classificationEnabled: Boolean(get(yaml, 'agent', 'classificationEnabled')),
|
|
805
|
+
}
|
|
806
|
+
: {}),
|
|
807
|
+
...(get(yaml, 'agent', 'toolReselectPerIteration') !== undefined
|
|
808
|
+
? {
|
|
809
|
+
toolReselectPerIteration: Boolean(get(yaml, 'agent', 'toolReselectPerIteration')),
|
|
810
|
+
}
|
|
811
|
+
: {}),
|
|
812
|
+
...(get(yaml, 'agent', 'ragTranslateEnabled') !== undefined
|
|
813
|
+
? {
|
|
814
|
+
ragTranslateEnabled: Boolean(get(yaml, 'agent', 'ragTranslateEnabled')),
|
|
815
|
+
}
|
|
816
|
+
: {}),
|
|
817
|
+
...(get(yaml, 'agent', 'refreshToolsPerIteration') !== undefined
|
|
818
|
+
? {
|
|
819
|
+
refreshToolsPerIteration: Boolean(get(yaml, 'agent', 'refreshToolsPerIteration')),
|
|
820
|
+
}
|
|
821
|
+
: {}),
|
|
822
|
+
...(get(yaml, 'agent', 'streamMode') !== undefined
|
|
823
|
+
? {
|
|
824
|
+
streamMode: String(get(yaml, 'agent', 'streamMode')),
|
|
825
|
+
}
|
|
826
|
+
: {}),
|
|
827
|
+
...(get(yaml, 'agent', 'llmCallStrategy') !== undefined
|
|
828
|
+
? {
|
|
829
|
+
llmCallStrategy: String(get(yaml, 'agent', 'llmCallStrategy')),
|
|
830
|
+
}
|
|
831
|
+
: {}),
|
|
832
|
+
...(get(yaml, 'agent', 'heartbeatIntervalMs') !== undefined
|
|
833
|
+
? {
|
|
834
|
+
heartbeatIntervalMs: Number(get(yaml, 'agent', 'heartbeatIntervalMs')),
|
|
835
|
+
}
|
|
836
|
+
: {}),
|
|
837
|
+
...(get(yaml, 'agent', 'healthTimeoutMs') !== undefined
|
|
838
|
+
? {
|
|
839
|
+
healthTimeoutMs: Number(get(yaml, 'agent', 'healthTimeoutMs')),
|
|
840
|
+
}
|
|
841
|
+
: {}),
|
|
842
|
+
...(get(yaml, 'agent', 'retry') !== undefined
|
|
843
|
+
? {
|
|
844
|
+
retry: get(yaml, 'agent', 'retry'),
|
|
845
|
+
}
|
|
846
|
+
: {}),
|
|
847
|
+
...(get(yaml, 'agent', 'toolSelection') !== undefined
|
|
848
|
+
? {
|
|
849
|
+
toolSelection: get(yaml, 'agent', 'toolSelection'),
|
|
850
|
+
}
|
|
851
|
+
: {}),
|
|
852
|
+
},
|
|
853
|
+
prompts: promptSystem ||
|
|
854
|
+
promptClassifier ||
|
|
855
|
+
promptReasoning ||
|
|
856
|
+
promptRagTranslate ||
|
|
857
|
+
promptHistorySummary
|
|
858
|
+
? {
|
|
859
|
+
...(promptSystem ? { system: promptSystem } : {}),
|
|
860
|
+
...(promptClassifier ? { classifier: promptClassifier } : {}),
|
|
861
|
+
...(typeof promptReasoning === 'string'
|
|
862
|
+
? { reasoning: promptReasoning }
|
|
863
|
+
: {}),
|
|
864
|
+
...(typeof promptRagTranslate === 'string'
|
|
865
|
+
? { ragTranslate: promptRagTranslate }
|
|
866
|
+
: {}),
|
|
867
|
+
...(typeof promptHistorySummary === 'string'
|
|
868
|
+
? { historySummary: promptHistorySummary }
|
|
869
|
+
: {}),
|
|
870
|
+
}
|
|
871
|
+
: undefined,
|
|
872
|
+
mode: get(yaml, 'mode') ?? undefined,
|
|
873
|
+
logDir: args['log-dir'] ?? get(yaml, 'logDir') ?? null,
|
|
874
|
+
pluginDir: args['plugin-dir'] ?? get(yaml, 'pluginDir') ?? undefined,
|
|
875
|
+
...(() => {
|
|
876
|
+
const subAgentConfigs = parseSubAgents(yaml, options.configPath, args, env);
|
|
877
|
+
return subAgentConfigs ? { subAgentConfigs } : {};
|
|
878
|
+
})(),
|
|
879
|
+
...(() => {
|
|
880
|
+
const coordinatorYaml = yaml
|
|
881
|
+
.coordinator;
|
|
882
|
+
return coordinatorYaml ? { coordinatorYaml } : {};
|
|
883
|
+
})(),
|
|
884
|
+
...(yaml.pipeline ? { pipeline: yaml.pipeline } : {}),
|
|
885
|
+
...(yaml.skills
|
|
886
|
+
? {
|
|
887
|
+
skills: {
|
|
888
|
+
type: (get(yaml, 'skills', 'type') ?? 'claude'),
|
|
889
|
+
dirs: get(yaml, 'skills', 'dirs'),
|
|
890
|
+
projectRoot: get(yaml, 'skills', 'projectRoot'),
|
|
891
|
+
},
|
|
892
|
+
}
|
|
893
|
+
: {}),
|
|
894
|
+
};
|
|
895
|
+
validateResolvedConfig(resolved, yaml, env);
|
|
896
|
+
return resolved;
|
|
897
|
+
}
|
|
898
|
+
const MODES = new Set([
|
|
899
|
+
'cyclic-react',
|
|
900
|
+
'planned-react',
|
|
901
|
+
'deep-stepper',
|
|
902
|
+
]);
|
|
903
|
+
/**
|
|
904
|
+
* Preset expansion: each `mode` maps to a default `flow` composition.
|
|
905
|
+
* An explicit `coordinator.flow` block overrides these per-component.
|
|
906
|
+
* `deep-stepper` = llm planner + RECURSIVE executor (demand-driven recursion),
|
|
907
|
+
* relying on the Evaluator as terminator (enforced below).
|
|
908
|
+
*/
|
|
909
|
+
const MODE_FLOW_PRESET = {
|
|
910
|
+
'cyclic-react': { planner: 'none', executor: 'cyclic-react' },
|
|
911
|
+
'planned-react': { planner: 'llm', executor: 'cyclic-react' },
|
|
912
|
+
'deep-stepper': { planner: 'llm', executor: 'recursive' },
|
|
913
|
+
};
|
|
914
|
+
/** Parse declarative `flow.plan` nodes (for the static planner). */
|
|
915
|
+
function parseFlowPlan(raw) {
|
|
916
|
+
if (!Array.isArray(raw))
|
|
917
|
+
return undefined;
|
|
918
|
+
const nodes = raw
|
|
919
|
+
.filter((n) => !!n && typeof n.goal === 'string')
|
|
920
|
+
.map((n, i) => ({
|
|
921
|
+
id: typeof n.id === 'string' && n.id ? n.id : `n${i}`,
|
|
922
|
+
goal: n.goal,
|
|
923
|
+
...(Array.isArray(n.dependsOn)
|
|
924
|
+
? {
|
|
925
|
+
dependsOn: n.dependsOn.filter((d) => typeof d === 'string'),
|
|
926
|
+
}
|
|
927
|
+
: {}),
|
|
928
|
+
...(typeof n.agent === 'string' ? { agent: n.agent } : {}),
|
|
929
|
+
}));
|
|
930
|
+
return nodes.length > 0 ? nodes : undefined;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Parse a (possibly nested) `flow` block into a full StepperCompositionSpec,
|
|
934
|
+
* inheriting bounds from the root. Mutually recursive with
|
|
935
|
+
* parseCompositionNodes (function declarations are hoisted).
|
|
936
|
+
*/
|
|
937
|
+
function parseNestedFlowSpec(flowCfg, bounds) {
|
|
938
|
+
const plannerType = flowCfg?.planner?.type ?? 'llm';
|
|
939
|
+
if (!['none', 'llm', 'static'].includes(plannerType))
|
|
940
|
+
throw new Error(`flow.planner.type must be none|llm|static`);
|
|
941
|
+
const granularity = flowCfg?.planner?.granularity ?? 'shallow';
|
|
942
|
+
if (!['shallow', 'detailed'].includes(granularity))
|
|
943
|
+
throw new Error(`flow.planner.granularity must be shallow|detailed`);
|
|
944
|
+
const executor = flowCfg?.executor?.type ?? 'cyclic-react';
|
|
945
|
+
if (!['simple', 'cyclic-react', 'recursive'].includes(executor))
|
|
946
|
+
throw new Error(`flow.executor.type must be simple|cyclic-react|recursive`);
|
|
947
|
+
const plannerSystemPrompt = parseSystemPromptOverride(flowCfg?.planner?.systemPrompt, 'flow.planner.systemPrompt');
|
|
948
|
+
const executorSystemPrompt = parseSystemPromptOverride(flowCfg?.executor?.systemPrompt, 'flow.executor.systemPrompt');
|
|
949
|
+
const plan = parseFlowPlan(flowCfg?.plan);
|
|
950
|
+
const nodes = parseCompositionNodes(flowCfg?.nodes, bounds);
|
|
951
|
+
return {
|
|
952
|
+
// Declared nodes ARE the plan ⇒ this level is static (keep the spec honest:
|
|
953
|
+
// buildFromComposition routes a node-bearing level to a StaticPlanner).
|
|
954
|
+
planner: (nodes ? 'static' : plannerType),
|
|
955
|
+
granularity: granularity,
|
|
956
|
+
...(plan ? { plan } : {}),
|
|
957
|
+
...(nodes ? { nodes } : {}),
|
|
958
|
+
executor: executor,
|
|
959
|
+
finalizer: 'llm',
|
|
960
|
+
...(plannerSystemPrompt ? { plannerSystemPrompt } : {}),
|
|
961
|
+
...(executorSystemPrompt ? { executorSystemPrompt } : {}),
|
|
962
|
+
...bounds,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/** Validate an optional system-prompt override: must be a non-empty string. */
|
|
966
|
+
function parseSystemPromptOverride(raw, label) {
|
|
967
|
+
if (raw === undefined || raw === null)
|
|
968
|
+
return undefined;
|
|
969
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
970
|
+
throw new Error(`coordinator.${label} must be a non-empty string`);
|
|
971
|
+
return raw;
|
|
972
|
+
}
|
|
973
|
+
/** Parse composition nodes; a node with a nested `flow` recurses into a sub-spec. */
|
|
974
|
+
function parseCompositionNodes(raw, bounds) {
|
|
975
|
+
if (!Array.isArray(raw))
|
|
976
|
+
return undefined;
|
|
977
|
+
const nodes = raw
|
|
978
|
+
.filter((n) => !!n && typeof n.goal === 'string')
|
|
979
|
+
.map((n, i) => ({
|
|
980
|
+
id: typeof n.id === 'string' && n.id ? n.id : `n${i}`,
|
|
981
|
+
goal: n.goal,
|
|
982
|
+
...(Array.isArray(n.dependsOn)
|
|
983
|
+
? {
|
|
984
|
+
dependsOn: n.dependsOn.filter((d) => typeof d === 'string'),
|
|
985
|
+
}
|
|
986
|
+
: {}),
|
|
987
|
+
...(n.flow ? { flow: parseNestedFlowSpec(n.flow, bounds) } : {}),
|
|
988
|
+
}));
|
|
989
|
+
return nodes.length > 0 ? nodes : undefined;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Parse stepper coordinator configuration from a raw config object.
|
|
993
|
+
*
|
|
994
|
+
* Supports:
|
|
995
|
+
* - `mode` (string) — default 'planned-react'; one of cyclic-react | planned-react
|
|
996
|
+
* - `stepper.maxParallelSteps` (number) — default 4
|
|
997
|
+
* - `stepper.maxDepth` (number) — default 4
|
|
998
|
+
* - `stepper.tokenBudget` (number) — default 1,000,000
|
|
999
|
+
* - `stepper.reviewer.atDepths` (number[] | 'all') — default [0,1]; 'all' means accept any depth
|
|
1000
|
+
*/
|
|
1001
|
+
export function parseStepperCoordinatorConfig(coord) {
|
|
1002
|
+
const mode = coord.mode ?? 'planned-react';
|
|
1003
|
+
if (!MODES.has(mode))
|
|
1004
|
+
throw new Error(`unknown coordinator.mode '${String(coord.mode)}'`);
|
|
1005
|
+
// Tool permissioning is the MCP SERVER's responsibility — whatever it exposes
|
|
1006
|
+
// via tools/list is allowed. The agent does not classify tools (read-only vs
|
|
1007
|
+
// mutating); there is no agent-side gate. The consumer wires the agent to a
|
|
1008
|
+
// server that exposes only the permitted tools (e.g. a read-only MCP proxy).
|
|
1009
|
+
const stepper = coord.stepper ?? {};
|
|
1010
|
+
const reviewerCfg = stepper.reviewer ?? {};
|
|
1011
|
+
const atDepths = reviewerCfg.atDepths ?? [0, 1];
|
|
1012
|
+
const reviewerAtDepths = atDepths === 'all'
|
|
1013
|
+
? { has: () => true }
|
|
1014
|
+
: (() => {
|
|
1015
|
+
const s = new Set(atDepths);
|
|
1016
|
+
return { has: (d) => s.has(d) };
|
|
1017
|
+
})();
|
|
1018
|
+
const knowledgeSeed = Array.isArray(coord.knowledgeSeed)
|
|
1019
|
+
? coord.knowledgeSeed
|
|
1020
|
+
.filter((e) => e && typeof e.content === 'string' && e.content.trim() !== '')
|
|
1021
|
+
.map((e) => ({
|
|
1022
|
+
content: e.content,
|
|
1023
|
+
artifactType: typeof e.artifactType === 'string' && e.artifactType
|
|
1024
|
+
? e.artifactType
|
|
1025
|
+
: 'guidance',
|
|
1026
|
+
}))
|
|
1027
|
+
: [];
|
|
1028
|
+
// Resolve the program flow: explicit `coordinator.flow` overrides the
|
|
1029
|
+
// mode-derived preset per component. `mode` thus becomes a preset alias.
|
|
1030
|
+
const preset = MODE_FLOW_PRESET[mode];
|
|
1031
|
+
const flowCfg = coord.flow;
|
|
1032
|
+
const plannerType = flowCfg?.planner?.type ?? preset.planner;
|
|
1033
|
+
if (!['none', 'llm', 'static'].includes(plannerType))
|
|
1034
|
+
throw new Error(`coordinator.flow.planner.type must be none|llm|static`);
|
|
1035
|
+
const granularity = flowCfg?.planner?.granularity ?? 'shallow';
|
|
1036
|
+
if (!['shallow', 'detailed'].includes(granularity))
|
|
1037
|
+
throw new Error(`coordinator.flow.planner.granularity must be shallow|detailed`);
|
|
1038
|
+
const executorType = flowCfg?.executor?.type ?? preset.executor;
|
|
1039
|
+
if (!['simple', 'cyclic-react', 'recursive'].includes(executorType))
|
|
1040
|
+
throw new Error(`coordinator.flow.executor.type must be simple|cyclic-react|recursive`);
|
|
1041
|
+
const finalizerType = flowCfg?.finalizer?.type ?? 'llm';
|
|
1042
|
+
if (finalizerType !== 'llm')
|
|
1043
|
+
throw new Error(`coordinator.flow.finalizer.type 'passthrough' is not yet implemented (use 'llm')`);
|
|
1044
|
+
const plannerSystemPrompt = parseSystemPromptOverride(flowCfg?.planner?.systemPrompt, 'flow.planner.systemPrompt');
|
|
1045
|
+
const executorSystemPrompt = parseSystemPromptOverride(flowCfg?.executor?.systemPrompt, 'flow.executor.systemPrompt');
|
|
1046
|
+
// 18.1 Evaluator: ON by default at all depths (per design). Disable via
|
|
1047
|
+
// `flow.evaluator.enabled: false`; narrow via `flow.evaluator.atDepths`.
|
|
1048
|
+
const evaluatorEnabled = flowCfg?.evaluator?.enabled !== false;
|
|
1049
|
+
// RUNAWAY GUARD: demand-driven recursion (executor:recursive / deep-stepper)
|
|
1050
|
+
// terminates via the Evaluator (executable → leaf, needs-work → recurse).
|
|
1051
|
+
// Without it recursion has no termination judge — that is exactly the 18.0
|
|
1052
|
+
// runaway (141 spawns). So recursion REQUIRES the Evaluator enabled.
|
|
1053
|
+
if (executorType === 'recursive' && !evaluatorEnabled)
|
|
1054
|
+
throw new Error('coordinator.flow.executor.type "recursive" (deep-stepper) requires the Evaluator ' +
|
|
1055
|
+
'(it is the recursion terminator) — do not set coordinator.flow.evaluator.enabled: false');
|
|
1056
|
+
const evalAtDepths = flowCfg?.evaluator?.atDepths ?? 'all';
|
|
1057
|
+
const evaluatorAtDepths = evalAtDepths === 'all'
|
|
1058
|
+
? { has: () => true }
|
|
1059
|
+
: (() => {
|
|
1060
|
+
const s = new Set(evalAtDepths);
|
|
1061
|
+
return { has: (d) => s.has(d) };
|
|
1062
|
+
})();
|
|
1063
|
+
const evaluatorSystemPrompt = parseSystemPromptOverride(flowCfg?.evaluator?.systemPrompt, 'flow.evaluator.systemPrompt');
|
|
1064
|
+
const plan = parseFlowPlan(flowCfg?.plan);
|
|
1065
|
+
const maxParallelSteps = Number(stepper.maxParallelSteps ?? 4);
|
|
1066
|
+
const maxDepth = Number(stepper.maxDepth ?? 4);
|
|
1067
|
+
const tokenBudget = Number(stepper.tokenBudget ?? 1_000_000);
|
|
1068
|
+
const formalizeTask = coord.formalizeTask === true;
|
|
1069
|
+
// Nested composition nodes inherit the root bounds (a sub-cycle uses the same
|
|
1070
|
+
// parallelism / depth / budget / safety unless the runtime threads otherwise).
|
|
1071
|
+
const bounds = {
|
|
1072
|
+
reviewerAtDepths,
|
|
1073
|
+
evaluatorEnabled,
|
|
1074
|
+
evaluatorAtDepths,
|
|
1075
|
+
...(evaluatorSystemPrompt ? { evaluatorSystemPrompt } : {}),
|
|
1076
|
+
maxParallelSteps,
|
|
1077
|
+
maxDepth,
|
|
1078
|
+
tokenBudget,
|
|
1079
|
+
formalizeTask,
|
|
1080
|
+
};
|
|
1081
|
+
const nodes = parseCompositionNodes(flowCfg?.nodes, bounds);
|
|
1082
|
+
// Static planner needs an explicit plan OR declared nodes (nodes ARE the plan).
|
|
1083
|
+
if (plannerType === 'static' && !plan && !nodes)
|
|
1084
|
+
throw new Error(`coordinator.flow.planner.type 'static' requires coordinator.flow.plan or coordinator.flow.nodes`);
|
|
1085
|
+
return {
|
|
1086
|
+
mode,
|
|
1087
|
+
reviewerAtDepths,
|
|
1088
|
+
maxParallelSteps,
|
|
1089
|
+
maxDepth,
|
|
1090
|
+
tokenBudget,
|
|
1091
|
+
knowledgeSeed,
|
|
1092
|
+
formalizeTask,
|
|
1093
|
+
flow: {
|
|
1094
|
+
// Declared root nodes ARE the plan ⇒ static at the root (honest spec).
|
|
1095
|
+
planner: (nodes ? 'static' : plannerType),
|
|
1096
|
+
granularity: granularity,
|
|
1097
|
+
executor: executorType,
|
|
1098
|
+
finalizer: 'llm',
|
|
1099
|
+
...(plannerSystemPrompt ? { plannerSystemPrompt } : {}),
|
|
1100
|
+
...(executorSystemPrompt ? { executorSystemPrompt } : {}),
|
|
1101
|
+
evaluatorEnabled,
|
|
1102
|
+
evaluatorAtDepths,
|
|
1103
|
+
...(evaluatorSystemPrompt ? { evaluatorSystemPrompt } : {}),
|
|
1104
|
+
...(plan ? { plan } : {}),
|
|
1105
|
+
...(nodes ? { nodes } : {}),
|
|
1106
|
+
},
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
//# sourceMappingURL=config.js.map
|