@mcp-abap-adt/llm-agent-server 16.2.0 → 18.0.0

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