@mcp-abap-adt/llm-agent-server-libs 19.2.0 → 20.4.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.
Files changed (179) hide show
  1. package/dist/builders/controller-skill-pipeline-builder.d.ts +57 -0
  2. package/dist/builders/controller-skill-pipeline-builder.d.ts.map +1 -0
  3. package/dist/builders/controller-skill-pipeline-builder.js +175 -0
  4. package/dist/builders/controller-skill-pipeline-builder.js.map +1 -0
  5. package/dist/factories/controller-factory.d.ts +2 -2
  6. package/dist/factories/controller-factory.d.ts.map +1 -1
  7. package/dist/factories/controller-factory.js +2 -1
  8. package/dist/factories/controller-factory.js.map +1 -1
  9. package/dist/generated/version.d.ts +1 -1
  10. package/dist/generated/version.js +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/pipelines/controller.d.ts +4 -2
  16. package/dist/pipelines/controller.d.ts.map +1 -1
  17. package/dist/pipelines/controller.js +33 -8
  18. package/dist/pipelines/controller.js.map +1 -1
  19. package/dist/pipelines/coordinator-resolvers.d.ts +68 -0
  20. package/dist/pipelines/coordinator-resolvers.d.ts.map +1 -0
  21. package/dist/pipelines/coordinator-resolvers.js +97 -0
  22. package/dist/pipelines/coordinator-resolvers.js.map +1 -0
  23. package/dist/pipelines/dag.d.ts +2 -2
  24. package/dist/pipelines/dag.js +2 -2
  25. package/dist/pipelines/parsers.d.ts +1 -1
  26. package/dist/pipelines/parsers.d.ts.map +1 -1
  27. package/dist/pipelines/parsers.js +5 -4
  28. package/dist/pipelines/parsers.js.map +1 -1
  29. package/dist/pipelines/register-skill-sources.d.ts.map +1 -1
  30. package/dist/pipelines/register-skill-sources.js +5 -0
  31. package/dist/pipelines/register-skill-sources.js.map +1 -1
  32. package/dist/pipelines/stepper.d.ts +2 -2
  33. package/dist/pipelines/stepper.js +2 -2
  34. package/dist/smart-agent/build-stepper-root.d.ts +3 -2
  35. package/dist/smart-agent/build-stepper-root.d.ts.map +1 -1
  36. package/dist/smart-agent/build-stepper-root.js +2 -1
  37. package/dist/smart-agent/build-stepper-root.js.map +1 -1
  38. package/dist/smart-agent/config-reload-watcher.d.ts +30 -0
  39. package/dist/smart-agent/config-reload-watcher.d.ts.map +1 -0
  40. package/dist/smart-agent/config-reload-watcher.js +101 -0
  41. package/dist/smart-agent/config-reload-watcher.js.map +1 -0
  42. package/dist/smart-agent/config-validator.d.ts +21 -0
  43. package/dist/smart-agent/config-validator.d.ts.map +1 -0
  44. package/dist/smart-agent/config-validator.js +196 -0
  45. package/dist/smart-agent/config-validator.js.map +1 -0
  46. package/dist/smart-agent/config.d.ts +16 -254
  47. package/dist/smart-agent/config.d.ts.map +1 -1
  48. package/dist/smart-agent/config.js +17 -904
  49. package/dist/smart-agent/config.js.map +1 -1
  50. package/dist/smart-agent/controller/board.d.ts +6 -3
  51. package/dist/smart-agent/controller/board.d.ts.map +1 -1
  52. package/dist/smart-agent/controller/board.js +21 -0
  53. package/dist/smart-agent/controller/board.js.map +1 -1
  54. package/dist/smart-agent/controller/controller-coordinator-handler.d.ts +16 -50
  55. package/dist/smart-agent/controller/controller-coordinator-handler.d.ts.map +1 -1
  56. package/dist/smart-agent/controller/controller-coordinator-handler.js +83 -371
  57. package/dist/smart-agent/controller/controller-coordinator-handler.js.map +1 -1
  58. package/dist/smart-agent/controller/finalizer.d.ts +5 -0
  59. package/dist/smart-agent/controller/finalizer.d.ts.map +1 -1
  60. package/dist/smart-agent/controller/finalizer.js +12 -5
  61. package/dist/smart-agent/controller/finalizer.js.map +1 -1
  62. package/dist/smart-agent/controller/parser.d.ts +11 -0
  63. package/dist/smart-agent/controller/parser.d.ts.map +1 -0
  64. package/dist/smart-agent/controller/parser.js +77 -0
  65. package/dist/smart-agent/controller/parser.js.map +1 -0
  66. package/dist/smart-agent/controller/planner.d.ts +20 -16
  67. package/dist/smart-agent/controller/planner.d.ts.map +1 -1
  68. package/dist/smart-agent/controller/planner.js +58 -64
  69. package/dist/smart-agent/controller/planner.js.map +1 -1
  70. package/dist/smart-agent/controller/recall.d.ts +48 -0
  71. package/dist/smart-agent/controller/recall.d.ts.map +1 -0
  72. package/dist/smart-agent/controller/recall.js +196 -0
  73. package/dist/smart-agent/controller/recall.js.map +1 -0
  74. package/dist/smart-agent/controller/reviewer.d.ts.map +1 -1
  75. package/dist/smart-agent/controller/reviewer.js +1 -1
  76. package/dist/smart-agent/controller/reviewer.js.map +1 -1
  77. package/dist/smart-agent/controller/types.d.ts +8 -9
  78. package/dist/smart-agent/controller/types.d.ts.map +1 -1
  79. package/dist/smart-agent/controller/usage-logging.d.ts +16 -0
  80. package/dist/smart-agent/controller/usage-logging.d.ts.map +1 -0
  81. package/dist/smart-agent/controller/usage-logging.js +39 -0
  82. package/dist/smart-agent/controller/usage-logging.js.map +1 -0
  83. package/dist/smart-agent/http/adapter-route-handler.d.ts +13 -0
  84. package/dist/smart-agent/http/adapter-route-handler.d.ts.map +1 -0
  85. package/dist/smart-agent/http/adapter-route-handler.js +76 -0
  86. package/dist/smart-agent/http/adapter-route-handler.js.map +1 -0
  87. package/dist/smart-agent/http/chat-route-handler.d.ts +15 -0
  88. package/dist/smart-agent/http/chat-route-handler.d.ts.map +1 -0
  89. package/dist/smart-agent/http/chat-route-handler.js +327 -0
  90. package/dist/smart-agent/http/chat-route-handler.js.map +1 -0
  91. package/dist/smart-agent/http/config-route-handler.d.ts +21 -0
  92. package/dist/smart-agent/http/config-route-handler.d.ts.map +1 -0
  93. package/dist/smart-agent/http/config-route-handler.js +149 -0
  94. package/dist/smart-agent/http/config-route-handler.js.map +1 -0
  95. package/dist/smart-agent/http/health-route-handler.d.ts +12 -0
  96. package/dist/smart-agent/http/health-route-handler.d.ts.map +1 -0
  97. package/dist/smart-agent/http/health-route-handler.js +19 -0
  98. package/dist/smart-agent/http/health-route-handler.js.map +1 -0
  99. package/dist/smart-agent/http/models-route-handler.d.ts +17 -0
  100. package/dist/smart-agent/http/models-route-handler.d.ts.map +1 -0
  101. package/dist/smart-agent/http/models-route-handler.js +65 -0
  102. package/dist/smart-agent/http/models-route-handler.js.map +1 -0
  103. package/dist/smart-agent/http/response-helpers.d.ts +31 -0
  104. package/dist/smart-agent/http/response-helpers.d.ts.map +1 -0
  105. package/dist/smart-agent/http/response-helpers.js +59 -0
  106. package/dist/smart-agent/http/response-helpers.js.map +1 -0
  107. package/dist/smart-agent/http/route-table.d.ts +44 -0
  108. package/dist/smart-agent/http/route-table.d.ts.map +1 -0
  109. package/dist/smart-agent/http/route-table.js +35 -0
  110. package/dist/smart-agent/http/route-table.js.map +1 -0
  111. package/dist/smart-agent/http/session-cookie.d.ts +10 -0
  112. package/dist/smart-agent/http/session-cookie.d.ts.map +1 -0
  113. package/dist/smart-agent/http/session-cookie.js +16 -0
  114. package/dist/smart-agent/http/session-cookie.js.map +1 -0
  115. package/dist/smart-agent/http/sessions-route-handler.d.ts +8 -0
  116. package/dist/smart-agent/http/sessions-route-handler.d.ts.map +1 -0
  117. package/dist/smart-agent/http/sessions-route-handler.js +54 -0
  118. package/dist/smart-agent/http/sessions-route-handler.js.map +1 -0
  119. package/dist/smart-agent/http/usage-route-handler.d.ts +12 -0
  120. package/dist/smart-agent/http/usage-route-handler.d.ts.map +1 -0
  121. package/dist/smart-agent/http/usage-route-handler.js +28 -0
  122. package/dist/smart-agent/http/usage-route-handler.js.map +1 -0
  123. package/dist/smart-agent/knowledge/make-knowledge-backend.d.ts +13 -0
  124. package/dist/smart-agent/knowledge/make-knowledge-backend.d.ts.map +1 -0
  125. package/dist/smart-agent/knowledge/make-knowledge-backend.js +18 -0
  126. package/dist/smart-agent/knowledge/make-knowledge-backend.js.map +1 -0
  127. package/dist/smart-agent/llm/role-llm-resolver.d.ts +30 -0
  128. package/dist/smart-agent/llm/role-llm-resolver.d.ts.map +1 -0
  129. package/dist/smart-agent/llm/role-llm-resolver.js +44 -0
  130. package/dist/smart-agent/llm/role-llm-resolver.js.map +1 -0
  131. package/dist/smart-agent/llm-config-map.d.ts +38 -0
  132. package/dist/smart-agent/llm-config-map.d.ts.map +1 -0
  133. package/dist/smart-agent/llm-config-map.js +75 -0
  134. package/dist/smart-agent/llm-config-map.js.map +1 -0
  135. package/dist/smart-agent/mcp-readiness-monitor.d.ts +38 -0
  136. package/dist/smart-agent/mcp-readiness-monitor.d.ts.map +1 -0
  137. package/dist/smart-agent/mcp-readiness-monitor.js +81 -0
  138. package/dist/smart-agent/mcp-readiness-monitor.js.map +1 -0
  139. package/dist/smart-agent/mcp-readiness-registry.d.ts +37 -0
  140. package/dist/smart-agent/mcp-readiness-registry.d.ts.map +1 -0
  141. package/dist/smart-agent/mcp-readiness-registry.js +55 -0
  142. package/dist/smart-agent/mcp-readiness-registry.js.map +1 -0
  143. package/dist/smart-agent/resolve-config-sections.d.ts +15 -0
  144. package/dist/smart-agent/resolve-config-sections.d.ts.map +1 -0
  145. package/dist/smart-agent/resolve-config-sections.js +212 -0
  146. package/dist/smart-agent/resolve-config-sections.js.map +1 -0
  147. package/dist/smart-agent/session-lifecycle/index.d.ts +117 -0
  148. package/dist/smart-agent/session-lifecycle/index.d.ts.map +1 -0
  149. package/dist/smart-agent/session-lifecycle/index.js +152 -0
  150. package/dist/smart-agent/session-lifecycle/index.js.map +1 -0
  151. package/dist/smart-agent/skill-plugins-config.d.ts +9 -1
  152. package/dist/smart-agent/skill-plugins-config.d.ts.map +1 -1
  153. package/dist/smart-agent/skill-plugins-config.js +27 -1
  154. package/dist/smart-agent/skill-plugins-config.js.map +1 -1
  155. package/dist/smart-agent/skill-plugins-host-factory.d.ts +14 -2
  156. package/dist/smart-agent/skill-plugins-host-factory.d.ts.map +1 -1
  157. package/dist/smart-agent/skill-plugins-host-factory.js +16 -4
  158. package/dist/smart-agent/skill-plugins-host-factory.js.map +1 -1
  159. package/dist/smart-agent/smart-server.d.ts +151 -222
  160. package/dist/smart-agent/smart-server.d.ts.map +1 -1
  161. package/dist/smart-agent/smart-server.js +529 -1385
  162. package/dist/smart-agent/smart-server.js.map +1 -1
  163. package/dist/smart-agent/stepper-config.d.ts +138 -0
  164. package/dist/smart-agent/stepper-config.d.ts.map +1 -0
  165. package/dist/smart-agent/stepper-config.js +216 -0
  166. package/dist/smart-agent/stepper-config.js.map +1 -0
  167. package/dist/smart-agent/tools-rag-handle.d.ts +10 -0
  168. package/dist/smart-agent/tools-rag-handle.d.ts.map +1 -0
  169. package/dist/smart-agent/tools-rag-handle.js +76 -0
  170. package/dist/smart-agent/tools-rag-handle.js.map +1 -0
  171. package/dist/smart-agent/workers/worker-registry.d.ts +159 -0
  172. package/dist/smart-agent/workers/worker-registry.d.ts.map +1 -0
  173. package/dist/smart-agent/workers/worker-registry.js +203 -0
  174. package/dist/smart-agent/workers/worker-registry.js.map +1 -0
  175. package/dist/smart-agent/yaml-loader.d.ts +7 -0
  176. package/dist/smart-agent/yaml-loader.d.ts.map +1 -0
  177. package/dist/smart-agent/yaml-loader.js +146 -0
  178. package/dist/smart-agent/yaml-loader.js.map +1 -0
  179. package/package.json +7 -7
@@ -1,510 +1,15 @@
1
1
  /**
2
2
  * Shared config utilities for SmartServer.
3
3
  */
4
- import fs from 'node:fs';
5
4
  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';
5
+ import { assertNoLegacyPipelineConfig, validateResolvedConfig, } from './config-validator.js';
6
+ import { resolveAgentSection, resolveLlmSection, resolveMcpSection, resolvePipelineSelection, resolvePromptsSection, resolveRagSection, } from './resolve-config-sections.js';
8
7
  import { parseSkillPluginsConfig } from './skill-plugins-config.js';
9
- /**
10
- * Detect whether an object is a flat SmartServerLlmConfig shape.
11
- * Flat shape is identified by the presence of ANY of the known flat-shape
12
- * fields: `provider`, `apiKey`, `model`, or `url`. This covers keyless
13
- * providers (Ollama, SAP AI Core) that omit `apiKey` — using `apiKey`-only
14
- * detection silently misclassified those configs as a "map" shape.
15
- */
16
- function isFlatLlmConfig(input) {
17
- const flat = input;
18
- return (typeof flat.provider === 'string' ||
19
- typeof flat.apiKey === 'string' ||
20
- typeof flat.model === 'string' ||
21
- typeof flat.url === 'string');
22
- }
23
- /**
24
- * Normalize the optional top-level `llm:` block.
25
- * - undefined → undefined (pipeline-only configs stay valid)
26
- * - flat shape (has `provider` | `apiKey` | `model` | `url`) → { main: flat } (backward compat)
27
- * - map shape → must include `main`; returned as NormalizedLlmMap
28
- */
29
- export function normalizeLlmConfig(input) {
30
- if (input === undefined)
31
- return undefined;
32
- if (isFlatLlmConfig(input)) {
33
- return { main: input };
34
- }
35
- const map = input;
36
- if (!map.main) {
37
- throw new Error("llm: map must include a 'main' key (default LLM for unspecified roles)");
38
- }
39
- return map;
40
- }
41
- /**
42
- * Strict lookup: returns map[name] if explicitly present, else undefined.
43
- * Does NOT fall through to map.main. Use when the caller needs to
44
- * detect explicit-presence (e.g. to decide between an alias and the
45
- * named map entry).
46
- */
47
- export function resolveLlmConfigStrict(map, name) {
48
- if (!map || !name)
49
- return undefined;
50
- return map[name];
51
- }
52
- /**
53
- * Resolve a per-role LLM config by name from a normalized map.
54
- * Lookup chain: map[name] → map.main → pipelineFallback.
55
- * When map is undefined, falls back to pipelineFallback (so pipeline-only
56
- * configs keep working with no top-level llm: block).
57
- *
58
- * The caller decides whether `undefined` is an error.
59
- */
60
- export function resolveLlmConfig(map, name, pipelineFallback) {
61
- if (!map)
62
- return pipelineFallback;
63
- if (!name || name === 'main')
64
- return map.main;
65
- return map[name] ?? map.main;
66
- }
67
- /**
68
- * Read the reviewer block's LLM-name selector, accepting both the
69
- * preferred `reviewerLlm` field and the deprecated `plannerLlm` alias.
70
- * When the alias is used, calls `warn(message)`.
71
- */
72
- export function resolveReviewerLlmName(block, warn) {
73
- if (!block)
74
- return undefined;
75
- if (typeof block.reviewerLlm === 'string')
76
- return block.reviewerLlm;
77
- if (typeof block.plannerLlm === 'string') {
78
- warn("coordinator.reviewer.plannerLlm is deprecated; rename to 'reviewerLlm'");
79
- return block.plannerLlm;
80
- }
81
- return undefined;
82
- }
83
- export function resolveCoordinatorPlanning(name, plannerLlm) {
84
- switch (name) {
85
- case 'one-shot':
86
- return new OneShotPlanning(plannerLlm);
87
- case 'replan-on-error':
88
- return new ReplanOnErrorPlanning(plannerLlm);
89
- case 'skill-steps':
90
- // SkillStepsPlanning reads `ctx.activeSkillMeta` (populated by
91
- // CoordinatorHandler from `ctx.selectedSkills`). No planner LLM
92
- // needed — the plan comes directly from the skill's `steps:` block.
93
- return new SkillStepsPlanning();
94
- default:
95
- throw new Error(`Unknown coordinator.planning strategy: '${name}'. Allowed: one-shot, replan-on-error, skill-steps.`);
96
- }
97
- }
98
- /**
99
- * Default coordinator dispatch kind. Omitted → 'hybrid' for ALL planning kinds:
100
- * agentless steps — the synthesized answer-directly step (#155) and skill steps
101
- * without an explicit `agent:` — need a self-LLM fallback. Pin 'subagent'
102
- * explicitly for strict subagent-only routing.
103
- */
104
- export function resolveCoordinatorDispatchKind(explicit) {
105
- return explicit ?? 'hybrid';
106
- }
107
- export function resolveCoordinatorDispatch(name, fallbackLlm, contextBuilder) {
108
- switch (name) {
109
- case 'subagent':
110
- return new SubAgentDispatch(contextBuilder);
111
- case 'self':
112
- if (!fallbackLlm) {
113
- throw new Error('coordinator.dispatch=self requires a planner or main LLM');
114
- }
115
- return new SelfDispatch(fallbackLlm);
116
- case 'hybrid':
117
- if (!fallbackLlm) {
118
- throw new Error('coordinator.dispatch=hybrid requires a planner or main LLM');
119
- }
120
- return new HybridDispatch(new SubAgentDispatch(contextBuilder), new SelfDispatch(fallbackLlm));
121
- default:
122
- throw new Error(`Unknown coordinator.dispatch strategy: '${name}'. Allowed: subagent, self, hybrid.`);
123
- }
124
- }
125
- export function resolveCoordinatorActivation(name) {
126
- switch (name) {
127
- case 'auto':
128
- return new AutoActivation();
129
- case 'explicit':
130
- return new ExplicitActivation();
131
- default:
132
- throw new Error(`Unknown coordinator.activation strategy: '${name}'. Allowed: auto, explicit.`);
133
- }
134
- }
135
- export function resolveToolSelectionStrategy(name, params) {
136
- switch (name) {
137
- case 'top-k':
138
- return new TopKToolSelection();
139
- case 'threshold': {
140
- const minScore = params?.minScore;
141
- if (typeof minScore !== 'number') {
142
- throw new Error("agent.toolSelection.strategy 'threshold' requires a numeric 'minScore'");
143
- }
144
- return new ScoreThresholdToolSelection(minScore);
145
- }
146
- default:
147
- throw new Error(`Unknown agent.toolSelection.strategy '${name}'. Allowed: top-k, threshold.`);
148
- }
149
- }
150
- const VALID_PROVIDERS = [
151
- 'openai',
152
- 'anthropic',
153
- 'deepseek',
154
- 'sap-ai-sdk',
155
- 'ollama',
156
- ];
157
- const VALID_RAG_TYPES = [
158
- 'in-memory',
159
- 'qdrant',
160
- 'hana-vector',
161
- 'pg-vector',
162
- ];
163
- export class ConfigValidationError extends Error {
164
- constructor(issues) {
165
- super(`Configuration error in smart-server.yaml:\n${issues
166
- .map((i) => ` - ${i}`)
167
- .join('\n')}\nSet these fields in your YAML and restart.`);
168
- this.name = 'ConfigValidationError';
169
- }
170
- }
171
- export const YAML_TEMPLATE = `port: 4004
172
- host: 0.0.0.0
173
-
174
- # Request routing mode:
175
- # hard — Fully managed context. Ignores client history/system prompt. Uses RAG + internal MCP tools only.
176
- # pass — Transparent proxy. Logs everything but modifies nothing.
177
- # smart — Hybrid. Preserves client history but enriches it with RAG context and MCP tools based on analysis. (default)
178
- mode: smart
179
-
180
- llm:
181
- provider: deepseek # deepseek | openai | anthropic | sap-ai-sdk | ollama
182
- apiKey: \${DEEPSEEK_API_KEY} # not required for ollama / sap-ai-sdk
183
- model: deepseek-chat
184
- temperature: 0.7
185
- classifierTemperature: 0.1
186
-
187
- rag:
188
- type: in-memory # in-memory | qdrant | hana-vector | pg-vector
189
- embedder: ollama # Embedder to use: ollama | openai | sap-ai-core | <custom>
190
- url: http://localhost:11434
191
- model: bge-m3
192
- # resourceGroup: default # SAP AI Core resource group (sap-ai-core embedder)
193
- # scenario: orchestration # SAP AI Core scenario: orchestration (default) | foundation-models
194
- # collectionName: llm-agent # Collection/table name (qdrant | hana-vector | pg-vector)
195
- dedupThreshold: 0.92
196
- vectorWeight: 0.7 # Semantic similarity weight (0..1)
197
- keywordWeight: 0.3 # Lexical matching weight (0..1)
198
-
199
- mcp:
200
- # type: none | http | stdio
201
- # To disable MCP, set type to 'none'
202
- type: http
203
- url: http://localhost:3001/mcp/stream/http
204
-
205
- agent:
206
- externalToolsValidationMode: permissive # permissive | strict
207
- maxIterations: 10
208
- maxToolCalls: 30
209
- toolUnavailableTtlMs: 600000 # Temporary tool blacklist TTL (ms)
210
- ragQueryK: 10
211
- # contextBudgetTokens: 4000 # Max tokens for RAG context in system prompt (0 = no limit)
212
- # semanticHistoryEnabled: false # Enable semantic history via RAG
213
- # historyRecencyWindow: 4 # Last N messages from client history in LLM context
214
- # historyTurnSummaryPrompt: "..." # LLM prompt for turn summarization
215
- showReasoning: false # Explain strategy at start of response
216
- historyAutoSummarizeLimit: 10 # History length to trigger compression
217
- queryExpansionEnabled: false # Expand RAG queries with LLM-generated synonyms
218
- toolResultCacheTtlMs: 300000 # Tool result cache TTL (ms); 0 to disable
219
- sessionTokenBudget: 0 # Multi-turn token budget; 0 to disable
220
- # ragTranslateEnabled: true # Translate non-ASCII RAG queries to English (default: true)
221
- # classificationEnabled: false # Enable for custom pipelines with multi-store routing
222
- # toolReselectPerIteration: false # Re-select tools via RAG on each tool-loop iteration
223
- # llmCallStrategy: streaming # streaming | non-streaming | fallback
224
- # streamMode: full # full | final — streaming behavior for tool loops
225
- # heartbeatIntervalMs: 5000 # SSE heartbeat interval during tool execution (ms)
226
- # healthTimeoutMs: 5000 # Health check probe timeout (ms); increase for slow providers (SAP AI Core: 15000)
227
- # retry: # LLM retry config for 429/5xx errors
228
- # maxAttempts: 3
229
- # backoffMs: 1000
230
- # retryOn: [429, 500, 502, 503]
231
- # retryOnMidStream: ['SSE stream'] # Substrings triggering mid-stream retry
232
-
233
- # --- Advanced Multi-Model LLM (optional) ------------------------------------
234
- # Assign different models for different internal roles via a top-level llm: map.
235
- # (The legacy pipeline.llm override was removed — use the top-level llm: map now.)
236
- # llm:
237
- # main:
238
- # provider: deepseek # deepseek | openai | anthropic | sap-ai-sdk
239
- # apiKey: \${DEEPSEEK_API_KEY}
240
- # model: deepseek-chat
241
- # temperature: 0.7
242
- # classifier: # optional; if absent, main config is reused
243
- # provider: deepseek
244
- # apiKey: \${DEEPSEEK_API_KEY}
245
- # model: deepseek-chat
246
- # temperature: 0.1
247
- # helper: # optional; if absent, main config is reused
248
- # provider: deepseek
249
- # apiKey: \${DEEPSEEK_API_KEY}
250
- # model: deepseek-chat
251
- # temperature: 0.1
252
-
253
- # prompts:
254
- # system: "You are a helpful assistant specialized in SAP ABAP development."
255
- # classifier: |
256
- # You are an intent classifier... (see source for full default prompt)
257
- # reasoning: |
258
- # IMPORTANT: Always start your response with a brief <reasoning> block...
259
- # ragTranslate: |
260
- # Translate the user request to English for search purposes...
261
- # historySummary: |
262
- # Summarize the conversation so far...
263
-
264
- log: smart-server.log # path to log file; omit for stdout
265
- # logDir: sessions # Directory for detailed session debug logs
266
- # pluginDir: ./my-plugins # Additional plugin directory (loaded after defaults)
267
- # plugins: # Explicit plugin module specifiers (npm packages or paths)
268
- # - "@scope/my-pipeline-plugin" # bare specifier resolved from cwd
269
- # - ./local-plugin.mjs # relative path resolved against cwd
270
-
271
- # subagents: # Optional: nested agents callable from pipeline
272
- # - name: code-reviewer # Used as stage config: { agent: code-reviewer }
273
- # description: | # Optional. Shown to the Coordinator planner LLM
274
- # Reviews code and returns # so it can pick this agent for the right step.
275
- # structured JSON.
276
- # config: ./agents/code-reviewer.yaml
277
-
278
- # pipeline: # Optional: select the request pipeline.
279
- # name: flat # flat (default) | linear | dag | stepper | <plugin>
280
- # config: # Opaque per-pipeline dialect (validated by the plugin)
281
- # mode: planned-react # e.g. stepper: cyclic-react | planned-react | deep-stepper
282
- # knowledgeSeed: [] # stepper: deployment-supplied tool guidance
283
- #
284
- # NOTE: the legacy 'coordinator:' block and the old 'pipeline:' shape
285
- # (mcp/rag/stages/llm overrides) were REMOVED in this major. A config that
286
- # still uses them fails loud at startup. Top-level 'llm:', 'mcp:', 'rag:' now
287
- # own those concerns; pipeline behavior moves under 'pipeline.config'.
288
- `;
289
- export function resolveEnvVars(value, env = process.env) {
290
- if (typeof value === 'string')
291
- return value.replace(/\$\{([^}:]+)(?::-(.*?))?\}/g, (_, name, fallback) => env[name] || fallback || '');
292
- if (Array.isArray(value))
293
- return value.map((v) => resolveEnvVars(v, env));
294
- if (value !== null && typeof value === 'object')
295
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [
296
- k,
297
- resolveEnvVars(v, env),
298
- ]));
299
- return value;
300
- }
301
- export function loadYamlConfig(filePath, env = process.env) {
302
- const raw = fs.readFileSync(filePath, 'utf8');
303
- return resolveEnvVars(parseYaml(raw), env);
304
- }
305
- export function generateConfigTemplate(outputPath) {
306
- fs.writeFileSync(outputPath, YAML_TEMPLATE, 'utf8');
307
- }
308
- /**
309
- * Build the IFinalizer impl from `coordinator.finalizer:` YAML.
310
- *
311
- * Lookup chain for `type: llm`:
312
- * resolveLlmConfig(llmMap, cfg.finalizerLlm, pipelineFallback)
313
- * → top-level llm.<name> → llm.main → pipelineFallback (pipeline.llm.main)
314
- * → ConfigError if all three are missing.
315
- *
316
- * Absent block / `type: passthrough` → PassthroughFinalizer.
317
- * `type: template` → TemplateFinalizer.
318
- */
319
- export async function buildFinalizer(cfg, llmMap, pipelineFallback, makeLlm) {
320
- const kind = cfg?.type ?? 'passthrough';
321
- if (kind === 'passthrough')
322
- return new PassthroughFinalizer();
323
- if (kind === 'template')
324
- return new TemplateFinalizer();
325
- // kind === 'llm'
326
- const resolved = resolveLlmConfig(llmMap, cfg?.finalizerLlm, pipelineFallback);
327
- if (!resolved) {
328
- throw new Error('coordinator.finalizer (type: llm) requires an LLM config: provide top-level llm.<name>, llm.main, or pipeline.llm.main');
329
- }
330
- const llm = await makeLlm(resolved);
331
- return new LlmFinalizer(llm, {
332
- systemPrompt: cfg?.systemPrompt,
333
- });
334
- }
335
- const get = (obj, ...keys) => keys.reduce((o, k) => {
336
- if (o !== null && typeof o === 'object' && k in o) {
337
- return o[k];
338
- }
339
- return undefined;
340
- }, obj);
341
- function checkLlmRole(label, role, requireModel, env, issues) {
342
- const provider = role?.provider;
343
- if (!provider) {
344
- issues.push(`${label}.provider: required (one of: openai, anthropic, deepseek, sap-ai-sdk, ollama)`);
345
- return;
346
- }
347
- // `as readonly string[]` is required so .includes() accepts an arbitrary
348
- // string; do not "simplify" — it preserves the const-tuple narrowing.
349
- if (!VALID_PROVIDERS.includes(provider)) {
350
- issues.push(`${label}.provider: "${provider}" is invalid (one of: openai, anthropic, deepseek, sap-ai-sdk, ollama)`);
351
- return;
352
- }
353
- if (requireModel && !role?.model) {
354
- issues.push(`${label}.model: required (string)`);
355
- }
356
- if (provider === 'openai' ||
357
- provider === 'anthropic' ||
358
- provider === 'deepseek') {
359
- if (!role?.apiKey) {
360
- issues.push(`${provider} requires ${label}.apiKey to resolve to a non-empty value (typically via \${${provider.toUpperCase()}_API_KEY} env reference).`);
361
- }
362
- }
363
- else if (provider === 'sap-ai-sdk') {
364
- if (!env.AICORE_SERVICE_KEY) {
365
- 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.');
366
- }
367
- }
368
- // ollama: no credential check.
369
- }
370
- function checkRagStore(label, store, issues) {
371
- if (!store)
372
- return;
373
- const ragType = store.type;
374
- if (!ragType) {
375
- issues.push(`${label}.type: required (one of: in-memory, qdrant, hana-vector, pg-vector)`);
376
- }
377
- else if (ragType === 'ollama' || ragType === 'openai') {
378
- 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)`);
379
- }
380
- else if (!VALID_RAG_TYPES.includes(ragType)) {
381
- issues.push(`${label}.type: "${ragType}" is invalid (one of: in-memory, qdrant, hana-vector, pg-vector)`);
382
- }
383
- else {
384
- if (ragType === 'qdrant' && !store.url) {
385
- issues.push(`${label}.url: required for ${label}.type qdrant`);
386
- }
387
- if ((ragType === 'hana-vector' || ragType === 'pg-vector') &&
388
- !store.collectionName) {
389
- issues.push(`${label}.collectionName: required for ${label}.type ${ragType}`);
390
- }
391
- }
392
- // Blocklist (NOT allowlist): consumers can register custom embedder
393
- // factories, so only known embedder-less providers are hard-rejected here.
394
- const embedder = store.embedder;
395
- if (embedder === 'deepseek' || embedder === 'anthropic') {
396
- issues.push(`${label}.embedder: "${embedder}" provider has no embedder; embedding-capable providers are ollama, openai, sap-ai-core`);
397
- }
398
- // Require model when an embedder is used: vector stores always use an
399
- // embedder; in-memory uses one only when embedder is explicitly set.
400
- const usesEmbedder = ragType === 'qdrant' ||
401
- ragType === 'hana-vector' ||
402
- ragType === 'pg-vector' ||
403
- (ragType === 'in-memory' && embedder != null);
404
- if (usesEmbedder && !store.model) {
405
- issues.push(`${label}.model: required when an embedder is used (e.g. bge-m3 for ollama)`);
406
- }
407
- }
408
- function validateLlmEntry(label, cfg, required, env, issues) {
409
- checkLlmRole(label, cfg, required, env, issues);
410
- }
411
- /**
412
- * Fail-loud migration guard for the clean break to `pipeline: { name, config }`.
413
- *
414
- * Throws when the raw YAML still carries either:
415
- * - a `coordinator:` block (the old runtime dispatch, removed in this major), OR
416
- * - a `pipeline:` value in the LEGACY PipelineConfig shape — an object that
417
- * carries `mcp`/`rag`/`stages`/`llm` but NO `name`.
418
- *
419
- * A new `pipeline: { name, ... }` object or a bare string shorthand
420
- * (`pipeline: stepper`) passes untouched.
421
- */
422
- export function assertNoLegacyPipelineConfig(yaml) {
423
- const hasCoordinator = yaml.coordinator !== undefined;
424
- const rawPipeline = yaml.pipeline;
425
- const isLegacyPipeline = rawPipeline !== undefined &&
426
- rawPipeline !== null &&
427
- typeof rawPipeline === 'object' &&
428
- !Array.isArray(rawPipeline) &&
429
- typeof rawPipeline.name !== 'string' &&
430
- ['mcp', 'rag', 'stages', 'llm'].some((k) => rawPipeline[k] !== undefined);
431
- if (hasCoordinator || isLegacyPipeline) {
432
- throw new Error("Legacy 'coordinator:' / 'pipeline:' config is no longer supported (removed in this major). " +
433
- 'Migrate to: pipeline: { name: <flat|linear|dag|stepper>, config: { ... } }. ' +
434
- "(Stepper's knowledgeSeed moves under pipeline.config.knowledgeSeed.) " +
435
- 'Pin a version <= 18 for the old behavior.');
436
- }
437
- }
438
- function validateResolvedConfig(_resolved, yaml, env) {
439
- const issues = [];
440
- // LLM is always sourced from the top-level `llm:` block now — the legacy
441
- // `pipeline.llm.*` override has been removed with the `pipeline: {name,config}`
442
- // migration. Read from the raw YAML so we can distinguish flat vs map shape.
443
- // `resolved.llm` is always constructed as a flat object by
444
- // resolveSmartServerConfig, so it cannot be used to detect the map shape.
445
- const rawLlm = get(yaml, 'llm');
446
- if (rawLlm === undefined) {
447
- issues.push('llm: required (top-level llm.main or a flat llm block)');
448
- }
449
- else if (typeof rawLlm.provider === 'string') {
450
- // Flat shape — existing behaviour.
451
- validateLlmEntry('llm', rawLlm, true, env, issues);
452
- }
453
- else {
454
- // Map shape — llm.main is required; every named entry is validated.
455
- const map = rawLlm;
456
- if (!map.main) {
457
- issues.push("llm.main: required when 'llm' is a named map");
458
- }
459
- else {
460
- validateLlmEntry('llm.main', map.main, true, env, issues);
461
- }
462
- for (const [name, entry] of Object.entries(map)) {
463
- if (name === 'main')
464
- continue;
465
- validateLlmEntry(`llm.${name}`, entry, true, env, issues);
466
- }
467
- }
468
- // Pipeline selection shape: `pipeline:` must name a pipeline (string or
469
- // { name } object). The plugin validates its own `config` dialect at build
470
- // time — we only enforce the presence of a name here.
471
- const rawPipeline = yaml.pipeline;
472
- if (rawPipeline !== undefined && rawPipeline !== null) {
473
- const ok = typeof rawPipeline === 'string' ||
474
- (typeof rawPipeline === 'object' &&
475
- typeof rawPipeline.name === 'string');
476
- if (!ok) {
477
- issues.push("pipeline: requires a 'name' (string, or { name, config }); built-ins: flat, linear, dag, stepper");
478
- }
479
- }
480
- if (get(yaml, 'mcp')) {
481
- const rawMcpVal = yaml.mcp;
482
- const mcpEntries = Array.isArray(rawMcpVal)
483
- ? rawMcpVal
484
- : [rawMcpVal];
485
- mcpEntries.forEach((entry, i) => {
486
- const label = Array.isArray(rawMcpVal) ? `mcp[${i}]` : 'mcp';
487
- const mcpType = entry?.type;
488
- if (mcpType && !['http', 'stdio', 'none'].includes(mcpType)) {
489
- issues.push(`${label}.type: "${mcpType}" is invalid (one of: http, stdio, none)`);
490
- }
491
- if (mcpType === 'http' && !entry?.url) {
492
- issues.push(`${label}.url: required when ${label}.type is http`);
493
- }
494
- if (mcpType === 'stdio' && !entry?.command) {
495
- issues.push(`${label}.command: required when ${label}.type is stdio`);
496
- }
497
- });
498
- }
499
- if (get(yaml, 'rag')) {
500
- checkRagStore('rag', get(yaml, 'rag'), issues);
501
- }
502
- // NOTE: the legacy `pipeline.rag.{name}` multistore was removed with the
503
- // `pipeline: {name,config}` migration; the top-level `rag:` block is the sole
504
- // RAG source, validated above.
505
- if (issues.length > 0)
506
- throw new ConfigValidationError([...new Set(issues)]);
507
- }
8
+ import { get, loadYamlConfig } from './yaml-loader.js';
9
+ export { buildFinalizer, resolveCoordinatorActivation, resolveCoordinatorDispatch, resolveCoordinatorDispatchKind, resolveCoordinatorPlanning, resolveToolSelectionStrategy, } from '../pipelines/coordinator-resolvers.js';
10
+ export { assertNoLegacyPipelineConfig, ConfigValidationError, } from './config-validator.js';
11
+ export { normalizeLlmConfig, resolveLlmConfig, resolveLlmConfigStrict, resolveReviewerLlmName, } from './llm-config-map.js';
12
+ export { generateConfigTemplate, loadYamlConfig, resolveEnvVars, YAML_TEMPLATE, } from './yaml-loader.js';
508
13
  /**
509
14
  * Recursively parse the top-level `subagents:` block from a YAML config.
510
15
  *
@@ -585,176 +90,14 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
585
90
  // API key derives solely from the top-level `llm:` block now (the legacy
586
91
  // `pipeline.llm.main.apiKey` override was removed with the schema migration).
587
92
  const apiKey = get(yaml, 'llm', 'apiKey') ?? '';
588
- const rawMcp = yaml.mcp;
589
- const mcpIsArray = Array.isArray(rawMcp);
590
- const mcpUrl = get(yaml, 'mcp', 'url');
591
- const mcpCommand = get(yaml, 'mcp', 'command');
592
- const mcpTypeRaw = mcpIsArray
593
- ? null // array form: type resolved per-entry inside connectMcpClientsFromConfig
594
- : (get(yaml, 'mcp', 'type') ??
595
- (mcpUrl ? 'http' : mcpCommand ? 'stdio' : null));
596
- const mcpType = (mcpTypeRaw === 'none' ? null : mcpTypeRaw);
597
- const promptSystem = get(yaml, 'prompts', 'system') ?? null;
598
- const promptClassifier = get(yaml, 'prompts', 'classifier') ?? null;
599
- const promptReasoning = get(yaml, 'prompts', 'reasoning') ?? null;
600
- const promptRagTranslate = get(yaml, 'prompts', 'ragTranslate') ?? null;
601
- const promptHistorySummary = get(yaml, 'prompts', 'historySummary') ?? null;
602
93
  const resolved = {
603
94
  port: Number(args.port ?? get(yaml, 'port') ?? env.PORT ?? 4004),
604
95
  host: args.host ?? get(yaml, 'host') ?? '0.0.0.0',
605
- llm: get(yaml, 'llm')
606
- ? typeof get(yaml, 'llm', 'provider') === 'string'
607
- ? {
608
- provider: get(yaml, 'llm', 'provider'),
609
- apiKey,
610
- url: get(yaml, 'llm', 'url'),
611
- model: get(yaml, 'llm', 'model'),
612
- temperature: Number(get(yaml, 'llm', 'temperature') ?? 0.7),
613
- classifierTemperature: Number(get(yaml, 'llm', 'classifierTemperature') ?? 0.1),
614
- }
615
- : get(yaml, 'llm')
616
- : undefined,
617
- rag: get(yaml, 'rag')
618
- ? {
619
- type: get(yaml, 'rag', 'type'),
620
- embedder: get(yaml, 'rag', 'embedder') ?? undefined,
621
- url: get(yaml, 'rag', 'url'),
622
- model: get(yaml, 'rag', 'model'),
623
- collectionName: args['rag-collection-name'] ??
624
- get(yaml, 'rag', 'collectionName') ??
625
- undefined,
626
- dedupThreshold: Number(get(yaml, 'rag', 'dedupThreshold') ?? 0.92),
627
- vectorWeight: Number(get(yaml, 'rag', 'vectorWeight') ?? 0.7),
628
- keywordWeight: Number(get(yaml, 'rag', 'keywordWeight') ?? 0.3),
629
- ...(get(yaml, 'rag', 'resourceGroup') !== undefined
630
- ? { resourceGroup: String(get(yaml, 'rag', 'resourceGroup')) }
631
- : {}),
632
- ...(get(yaml, 'rag', 'scenario') !== undefined
633
- ? {
634
- scenario: String(get(yaml, 'rag', 'scenario')),
635
- }
636
- : {}),
637
- }
638
- : undefined,
639
- mcp: mcpIsArray
640
- ? // Array form: pass through as-is so connectMcpClientsFromConfig can
641
- // iterate and connect each entry. Typed as SmartServerMcpConfig[].
642
- rawMcp
643
- : mcpType
644
- ? {
645
- type: mcpType,
646
- url: mcpUrl || undefined,
647
- command: mcpCommand || undefined,
648
- args: args['mcp-args'] || get(yaml, 'mcp', 'args')
649
- ? String(args['mcp-args'] || get(yaml, 'mcp', 'args')).split(' ')
650
- : undefined,
651
- headers: get(yaml, 'mcp', 'headers') ||
652
- undefined,
653
- }
654
- : undefined,
655
- agent: {
656
- externalToolsValidationMode: (get(yaml, 'agent', 'externalToolsValidationMode') ?? 'permissive'),
657
- maxIterations: Number(get(yaml, 'agent', 'maxIterations') ?? 10),
658
- maxToolCalls: Number(get(yaml, 'agent', 'maxToolCalls') ?? 30),
659
- toolUnavailableTtlMs: Number(get(yaml, 'agent', 'toolUnavailableTtlMs') ?? 600000),
660
- ragQueryK: Number(get(yaml, 'agent', 'ragQueryK') ?? 10),
661
- ...(get(yaml, 'agent', 'contextBudgetTokens') !== undefined
662
- ? {
663
- contextBudgetTokens: Number(get(yaml, 'agent', 'contextBudgetTokens')),
664
- }
665
- : {}),
666
- ...(get(yaml, 'agent', 'semanticHistoryEnabled') !== undefined
667
- ? {
668
- semanticHistoryEnabled: Boolean(get(yaml, 'agent', 'semanticHistoryEnabled')),
669
- }
670
- : {}),
671
- ...(get(yaml, 'agent', 'historyRecencyWindow') !== undefined
672
- ? {
673
- historyRecencyWindow: Number(get(yaml, 'agent', 'historyRecencyWindow')),
674
- }
675
- : {}),
676
- ...(get(yaml, 'agent', 'historyTurnSummaryPrompt') !== undefined
677
- ? {
678
- historyTurnSummaryPrompt: String(get(yaml, 'agent', 'historyTurnSummaryPrompt')),
679
- }
680
- : {}),
681
- showReasoning: Boolean(args['agent-show-reasoning'] ??
682
- get(yaml, 'agent', 'showReasoning') ??
683
- false),
684
- historyAutoSummarizeLimit: Number(get(yaml, 'agent', 'historyAutoSummarizeLimit') ?? 10),
685
- queryExpansionEnabled: Boolean(get(yaml, 'agent', 'queryExpansionEnabled') ?? false),
686
- toolResultCacheTtlMs: Number(get(yaml, 'agent', 'toolResultCacheTtlMs') ?? 300000),
687
- sessionTokenBudget: Number(get(yaml, 'agent', 'sessionTokenBudget') ?? 0),
688
- ...(get(yaml, 'agent', 'classificationEnabled') !== undefined
689
- ? {
690
- classificationEnabled: Boolean(get(yaml, 'agent', 'classificationEnabled')),
691
- }
692
- : {}),
693
- ...(get(yaml, 'agent', 'toolReselectPerIteration') !== undefined
694
- ? {
695
- toolReselectPerIteration: Boolean(get(yaml, 'agent', 'toolReselectPerIteration')),
696
- }
697
- : {}),
698
- ...(get(yaml, 'agent', 'ragTranslateEnabled') !== undefined
699
- ? {
700
- ragTranslateEnabled: Boolean(get(yaml, 'agent', 'ragTranslateEnabled')),
701
- }
702
- : {}),
703
- ...(get(yaml, 'agent', 'refreshToolsPerIteration') !== undefined
704
- ? {
705
- refreshToolsPerIteration: Boolean(get(yaml, 'agent', 'refreshToolsPerIteration')),
706
- }
707
- : {}),
708
- ...(get(yaml, 'agent', 'streamMode') !== undefined
709
- ? {
710
- streamMode: String(get(yaml, 'agent', 'streamMode')),
711
- }
712
- : {}),
713
- ...(get(yaml, 'agent', 'llmCallStrategy') !== undefined
714
- ? {
715
- llmCallStrategy: String(get(yaml, 'agent', 'llmCallStrategy')),
716
- }
717
- : {}),
718
- ...(get(yaml, 'agent', 'heartbeatIntervalMs') !== undefined
719
- ? {
720
- heartbeatIntervalMs: Number(get(yaml, 'agent', 'heartbeatIntervalMs')),
721
- }
722
- : {}),
723
- ...(get(yaml, 'agent', 'healthTimeoutMs') !== undefined
724
- ? {
725
- healthTimeoutMs: Number(get(yaml, 'agent', 'healthTimeoutMs')),
726
- }
727
- : {}),
728
- ...(get(yaml, 'agent', 'retry') !== undefined
729
- ? {
730
- retry: get(yaml, 'agent', 'retry'),
731
- }
732
- : {}),
733
- ...(get(yaml, 'agent', 'toolSelection') !== undefined
734
- ? {
735
- toolSelection: get(yaml, 'agent', 'toolSelection'),
736
- }
737
- : {}),
738
- },
739
- prompts: promptSystem ||
740
- promptClassifier ||
741
- promptReasoning ||
742
- promptRagTranslate ||
743
- promptHistorySummary
744
- ? {
745
- ...(promptSystem ? { system: promptSystem } : {}),
746
- ...(promptClassifier ? { classifier: promptClassifier } : {}),
747
- ...(typeof promptReasoning === 'string'
748
- ? { reasoning: promptReasoning }
749
- : {}),
750
- ...(typeof promptRagTranslate === 'string'
751
- ? { ragTranslate: promptRagTranslate }
752
- : {}),
753
- ...(typeof promptHistorySummary === 'string'
754
- ? { historySummary: promptHistorySummary }
755
- : {}),
756
- }
757
- : undefined,
96
+ llm: resolveLlmSection(yaml, apiKey),
97
+ rag: resolveRagSection(yaml, args),
98
+ mcp: resolveMcpSection(yaml, args),
99
+ agent: resolveAgentSection(yaml, args),
100
+ prompts: resolvePromptsSection(yaml),
758
101
  mode: get(yaml, 'mode') ?? undefined,
759
102
  logDir: args['log-dir'] ?? get(yaml, 'logDir') ?? null,
760
103
  pluginDir: args['plugin-dir'] ?? get(yaml, 'pluginDir') ?? undefined,
@@ -769,29 +112,7 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
769
112
  const subAgentConfigs = parseSubAgents(yaml, options.configPath, args, env);
770
113
  return subAgentConfigs ? { subAgentConfigs } : {};
771
114
  })(),
772
- ...(() => {
773
- // Pipeline selection: `pipeline: { name, config }`. `name` is required;
774
- // `config` is the plugin's opaque dialect (validated by the plugin's
775
- // parseConfig at build time). A bare string (`pipeline: stepper`) is
776
- // accepted as shorthand for `{ name: <string> }`.
777
- const raw = yaml.pipeline;
778
- if (raw === undefined || raw === null)
779
- return {};
780
- if (typeof raw === 'string')
781
- return { pipeline: { name: raw } };
782
- const obj = raw;
783
- if (typeof obj.name !== 'string') {
784
- throw new Error("pipeline: requires a 'name' (one of: flat, linear, dag, stepper, or a registered plugin)");
785
- }
786
- return {
787
- pipeline: {
788
- name: obj.name,
789
- ...(obj.config && typeof obj.config === 'object'
790
- ? { config: obj.config }
791
- : {}),
792
- },
793
- };
794
- })(),
115
+ ...resolvePipelineSelection(yaml),
795
116
  ...(yaml.skills
796
117
  ? {
797
118
  skills: {
@@ -805,218 +126,10 @@ export function resolveSmartServerConfig(args = {}, yaml = {}, env = process.env
805
126
  ? { skillPlugins: parseSkillPluginsConfig(yaml.skillPlugins) }
806
127
  : {}),
807
128
  };
808
- validateResolvedConfig(resolved, yaml, env);
129
+ validateResolvedConfig(resolved, yaml, env, {
130
+ skipProviderRuntimeChecks: options.skipProviderRuntimeChecks,
131
+ });
809
132
  return resolved;
810
133
  }
811
- const MODES = new Set([
812
- 'cyclic-react',
813
- 'planned-react',
814
- 'deep-stepper',
815
- ]);
816
- /**
817
- * Preset expansion: each `mode` maps to a default `flow` composition.
818
- * An explicit `coordinator.flow` block overrides these per-component.
819
- * `deep-stepper` = llm planner + RECURSIVE executor (demand-driven recursion),
820
- * relying on the Evaluator as terminator (enforced below).
821
- */
822
- const MODE_FLOW_PRESET = {
823
- 'cyclic-react': { planner: 'none', executor: 'cyclic-react' },
824
- 'planned-react': { planner: 'llm', executor: 'cyclic-react' },
825
- 'deep-stepper': { planner: 'llm', executor: 'recursive' },
826
- };
827
- /** Parse declarative `flow.plan` nodes (for the static planner). */
828
- function parseFlowPlan(raw) {
829
- if (!Array.isArray(raw))
830
- return undefined;
831
- const nodes = raw
832
- .filter((n) => !!n && typeof n.goal === 'string')
833
- .map((n, i) => ({
834
- id: typeof n.id === 'string' && n.id ? n.id : `n${i}`,
835
- goal: n.goal,
836
- ...(Array.isArray(n.dependsOn)
837
- ? {
838
- dependsOn: n.dependsOn.filter((d) => typeof d === 'string'),
839
- }
840
- : {}),
841
- ...(typeof n.agent === 'string' ? { agent: n.agent } : {}),
842
- }));
843
- return nodes.length > 0 ? nodes : undefined;
844
- }
845
- /**
846
- * Parse a (possibly nested) `flow` block into a full StepperCompositionSpec,
847
- * inheriting bounds from the root. Mutually recursive with
848
- * parseCompositionNodes (function declarations are hoisted).
849
- */
850
- function parseNestedFlowSpec(flowCfg, bounds) {
851
- const plannerType = flowCfg?.planner?.type ?? 'llm';
852
- if (!['none', 'llm', 'static'].includes(plannerType))
853
- throw new Error(`flow.planner.type must be none|llm|static`);
854
- const granularity = flowCfg?.planner?.granularity ?? 'shallow';
855
- if (!['shallow', 'detailed'].includes(granularity))
856
- throw new Error(`flow.planner.granularity must be shallow|detailed`);
857
- const executor = flowCfg?.executor?.type ?? 'cyclic-react';
858
- if (!['simple', 'cyclic-react', 'recursive'].includes(executor))
859
- throw new Error(`flow.executor.type must be simple|cyclic-react|recursive`);
860
- const plannerSystemPrompt = parseSystemPromptOverride(flowCfg?.planner?.systemPrompt, 'flow.planner.systemPrompt');
861
- const executorSystemPrompt = parseSystemPromptOverride(flowCfg?.executor?.systemPrompt, 'flow.executor.systemPrompt');
862
- const plan = parseFlowPlan(flowCfg?.plan);
863
- const nodes = parseCompositionNodes(flowCfg?.nodes, bounds);
864
- return {
865
- // Declared nodes ARE the plan ⇒ this level is static (keep the spec honest:
866
- // buildFromComposition routes a node-bearing level to a StaticPlanner).
867
- planner: (nodes ? 'static' : plannerType),
868
- granularity: granularity,
869
- ...(plan ? { plan } : {}),
870
- ...(nodes ? { nodes } : {}),
871
- executor: executor,
872
- finalizer: 'llm',
873
- ...(plannerSystemPrompt ? { plannerSystemPrompt } : {}),
874
- ...(executorSystemPrompt ? { executorSystemPrompt } : {}),
875
- ...bounds,
876
- };
877
- }
878
- /** Validate an optional system-prompt override: must be a non-empty string. */
879
- function parseSystemPromptOverride(raw, label) {
880
- if (raw === undefined || raw === null)
881
- return undefined;
882
- if (typeof raw !== 'string' || raw.trim() === '')
883
- throw new Error(`coordinator.${label} must be a non-empty string`);
884
- return raw;
885
- }
886
- /** Parse composition nodes; a node with a nested `flow` recurses into a sub-spec. */
887
- function parseCompositionNodes(raw, bounds) {
888
- if (!Array.isArray(raw))
889
- return undefined;
890
- const nodes = raw
891
- .filter((n) => !!n && typeof n.goal === 'string')
892
- .map((n, i) => ({
893
- id: typeof n.id === 'string' && n.id ? n.id : `n${i}`,
894
- goal: n.goal,
895
- ...(Array.isArray(n.dependsOn)
896
- ? {
897
- dependsOn: n.dependsOn.filter((d) => typeof d === 'string'),
898
- }
899
- : {}),
900
- ...(n.flow ? { flow: parseNestedFlowSpec(n.flow, bounds) } : {}),
901
- }));
902
- return nodes.length > 0 ? nodes : undefined;
903
- }
904
- /**
905
- * Parse stepper coordinator configuration from a raw config object.
906
- *
907
- * Supports:
908
- * - `mode` (string) — default 'planned-react'; one of cyclic-react | planned-react
909
- * - `stepper.maxParallelSteps` (number) — default 4
910
- * - `stepper.maxDepth` (number) — default 4
911
- * - `stepper.tokenBudget` (number) — default 1,000,000
912
- * - `stepper.reviewer.atDepths` (number[] | 'all') — default [0,1]; 'all' means accept any depth
913
- */
914
- export function parseStepperCoordinatorConfig(coord) {
915
- const mode = coord.mode ?? 'planned-react';
916
- if (!MODES.has(mode))
917
- throw new Error(`unknown coordinator.mode '${String(coord.mode)}'`);
918
- // Tool permissioning is the MCP SERVER's responsibility — whatever it exposes
919
- // via tools/list is allowed. The agent does not classify tools (read-only vs
920
- // mutating); there is no agent-side gate. The consumer wires the agent to a
921
- // server that exposes only the permitted tools (e.g. a read-only MCP proxy).
922
- const stepper = coord.stepper ?? {};
923
- const reviewerCfg = stepper.reviewer ?? {};
924
- const atDepths = reviewerCfg.atDepths ?? [0, 1];
925
- const reviewerAtDepths = atDepths === 'all'
926
- ? { has: () => true }
927
- : (() => {
928
- const s = new Set(atDepths);
929
- return { has: (d) => s.has(d) };
930
- })();
931
- const knowledgeSeed = Array.isArray(coord.knowledgeSeed)
932
- ? coord.knowledgeSeed
933
- .filter((e) => e && typeof e.content === 'string' && e.content.trim() !== '')
934
- .map((e) => ({
935
- content: e.content,
936
- artifactType: typeof e.artifactType === 'string' && e.artifactType
937
- ? e.artifactType
938
- : 'guidance',
939
- }))
940
- : [];
941
- // Resolve the program flow: explicit `coordinator.flow` overrides the
942
- // mode-derived preset per component. `mode` thus becomes a preset alias.
943
- const preset = MODE_FLOW_PRESET[mode];
944
- const flowCfg = coord.flow;
945
- const plannerType = flowCfg?.planner?.type ?? preset.planner;
946
- if (!['none', 'llm', 'static'].includes(plannerType))
947
- throw new Error(`coordinator.flow.planner.type must be none|llm|static`);
948
- const granularity = flowCfg?.planner?.granularity ?? 'shallow';
949
- if (!['shallow', 'detailed'].includes(granularity))
950
- throw new Error(`coordinator.flow.planner.granularity must be shallow|detailed`);
951
- const executorType = flowCfg?.executor?.type ?? preset.executor;
952
- if (!['simple', 'cyclic-react', 'recursive'].includes(executorType))
953
- throw new Error(`coordinator.flow.executor.type must be simple|cyclic-react|recursive`);
954
- const finalizerType = flowCfg?.finalizer?.type ?? 'llm';
955
- if (finalizerType !== 'llm')
956
- throw new Error(`coordinator.flow.finalizer.type 'passthrough' is not yet implemented (use 'llm')`);
957
- const plannerSystemPrompt = parseSystemPromptOverride(flowCfg?.planner?.systemPrompt, 'flow.planner.systemPrompt');
958
- const executorSystemPrompt = parseSystemPromptOverride(flowCfg?.executor?.systemPrompt, 'flow.executor.systemPrompt');
959
- // 18.1 Evaluator: ON by default at all depths (per design). Disable via
960
- // `flow.evaluator.enabled: false`; narrow via `flow.evaluator.atDepths`.
961
- const evaluatorEnabled = flowCfg?.evaluator?.enabled !== false;
962
- // RUNAWAY GUARD: demand-driven recursion (executor:recursive / deep-stepper)
963
- // terminates via the Evaluator (executable → leaf, needs-work → recurse).
964
- // Without it recursion has no termination judge — that is exactly the 18.0
965
- // runaway (141 spawns). So recursion REQUIRES the Evaluator enabled.
966
- if (executorType === 'recursive' && !evaluatorEnabled)
967
- throw new Error('coordinator.flow.executor.type "recursive" (deep-stepper) requires the Evaluator ' +
968
- '(it is the recursion terminator) — do not set coordinator.flow.evaluator.enabled: false');
969
- const evalAtDepths = flowCfg?.evaluator?.atDepths ?? 'all';
970
- const evaluatorAtDepths = evalAtDepths === 'all'
971
- ? { has: () => true }
972
- : (() => {
973
- const s = new Set(evalAtDepths);
974
- return { has: (d) => s.has(d) };
975
- })();
976
- const evaluatorSystemPrompt = parseSystemPromptOverride(flowCfg?.evaluator?.systemPrompt, 'flow.evaluator.systemPrompt');
977
- const plan = parseFlowPlan(flowCfg?.plan);
978
- const maxParallelSteps = Number(stepper.maxParallelSteps ?? 4);
979
- const maxDepth = Number(stepper.maxDepth ?? 4);
980
- const tokenBudget = Number(stepper.tokenBudget ?? 1_000_000);
981
- const formalizeTask = coord.formalizeTask === true;
982
- // Nested composition nodes inherit the root bounds (a sub-cycle uses the same
983
- // parallelism / depth / budget / safety unless the runtime threads otherwise).
984
- const bounds = {
985
- reviewerAtDepths,
986
- evaluatorEnabled,
987
- evaluatorAtDepths,
988
- ...(evaluatorSystemPrompt ? { evaluatorSystemPrompt } : {}),
989
- maxParallelSteps,
990
- maxDepth,
991
- tokenBudget,
992
- formalizeTask,
993
- };
994
- const nodes = parseCompositionNodes(flowCfg?.nodes, bounds);
995
- // Static planner needs an explicit plan OR declared nodes (nodes ARE the plan).
996
- if (plannerType === 'static' && !plan && !nodes)
997
- throw new Error(`coordinator.flow.planner.type 'static' requires coordinator.flow.plan or coordinator.flow.nodes`);
998
- return {
999
- mode,
1000
- reviewerAtDepths,
1001
- maxParallelSteps,
1002
- maxDepth,
1003
- tokenBudget,
1004
- knowledgeSeed,
1005
- formalizeTask,
1006
- flow: {
1007
- // Declared root nodes ARE the plan ⇒ static at the root (honest spec).
1008
- planner: (nodes ? 'static' : plannerType),
1009
- granularity: granularity,
1010
- executor: executorType,
1011
- finalizer: 'llm',
1012
- ...(plannerSystemPrompt ? { plannerSystemPrompt } : {}),
1013
- ...(executorSystemPrompt ? { executorSystemPrompt } : {}),
1014
- evaluatorEnabled,
1015
- evaluatorAtDepths,
1016
- ...(evaluatorSystemPrompt ? { evaluatorSystemPrompt } : {}),
1017
- ...(plan ? { plan } : {}),
1018
- ...(nodes ? { nodes } : {}),
1019
- },
1020
- };
1021
- }
134
+ export { parseStepperCoordinatorConfig } from './stepper-config.js';
1022
135
  //# sourceMappingURL=config.js.map