@mcp-abap-adt/llm-agent-server-libs 30.0.0 → 30.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.
Files changed (69) hide show
  1. package/dist/generated/version.d.ts +1 -1
  2. package/dist/generated/version.js +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/smart-agent/config-reload-watcher.d.ts +7 -0
  7. package/dist/smart-agent/config-reload-watcher.d.ts.map +1 -1
  8. package/dist/smart-agent/config-reload-watcher.js +23 -6
  9. package/dist/smart-agent/config-reload-watcher.js.map +1 -1
  10. package/dist/smart-agent/config-validator.d.ts.map +1 -1
  11. package/dist/smart-agent/config-validator.js +170 -5
  12. package/dist/smart-agent/config-validator.js.map +1 -1
  13. package/dist/smart-agent/config.d.ts.map +1 -1
  14. package/dist/smart-agent/config.js +9 -1
  15. package/dist/smart-agent/config.js.map +1 -1
  16. package/dist/smart-agent/decision-config.d.ts +32 -0
  17. package/dist/smart-agent/decision-config.d.ts.map +1 -0
  18. package/dist/smart-agent/decision-config.js +18 -0
  19. package/dist/smart-agent/decision-config.js.map +1 -0
  20. package/dist/smart-agent/http/adapter-route-handler.d.ts +1 -1
  21. package/dist/smart-agent/http/adapter-route-handler.d.ts.map +1 -1
  22. package/dist/smart-agent/http/adapter-route-handler.js +34 -3
  23. package/dist/smart-agent/http/adapter-route-handler.js.map +1 -1
  24. package/dist/smart-agent/http/chat-route-handler.d.ts.map +1 -1
  25. package/dist/smart-agent/http/chat-route-handler.js +35 -4
  26. package/dist/smart-agent/http/chat-route-handler.js.map +1 -1
  27. package/dist/smart-agent/http/config-route-handler.d.ts +6 -3
  28. package/dist/smart-agent/http/config-route-handler.d.ts.map +1 -1
  29. package/dist/smart-agent/http/config-route-handler.js +7 -5
  30. package/dist/smart-agent/http/config-route-handler.js.map +1 -1
  31. package/dist/smart-agent/http/request-abort.d.ts +11 -0
  32. package/dist/smart-agent/http/request-abort.d.ts.map +1 -0
  33. package/dist/smart-agent/http/request-abort.js +19 -0
  34. package/dist/smart-agent/http/request-abort.js.map +1 -0
  35. package/dist/smart-agent/llm/llm-circuit-breakers.d.ts +21 -0
  36. package/dist/smart-agent/llm/llm-circuit-breakers.d.ts.map +1 -0
  37. package/dist/smart-agent/llm/llm-circuit-breakers.js +43 -0
  38. package/dist/smart-agent/llm/llm-circuit-breakers.js.map +1 -0
  39. package/dist/smart-agent/llm/role-llm-resolver.d.ts +3 -0
  40. package/dist/smart-agent/llm/role-llm-resolver.d.ts.map +1 -1
  41. package/dist/smart-agent/llm/role-llm-resolver.js +4 -1
  42. package/dist/smart-agent/llm/role-llm-resolver.js.map +1 -1
  43. package/dist/smart-agent/rag-config.d.ts +3 -0
  44. package/dist/smart-agent/rag-config.d.ts.map +1 -1
  45. package/dist/smart-agent/rag-config.js.map +1 -1
  46. package/dist/smart-agent/resolve-agent-embedder.d.ts +6 -1
  47. package/dist/smart-agent/resolve-agent-embedder.d.ts.map +1 -1
  48. package/dist/smart-agent/resolve-agent-embedder.js +8 -3
  49. package/dist/smart-agent/resolve-agent-embedder.js.map +1 -1
  50. package/dist/smart-agent/resolve-config-sections.d.ts +7 -0
  51. package/dist/smart-agent/resolve-config-sections.d.ts.map +1 -1
  52. package/dist/smart-agent/resolve-config-sections.js +58 -0
  53. package/dist/smart-agent/resolve-config-sections.js.map +1 -1
  54. package/dist/smart-agent/resolve-reranker.d.ts +10 -0
  55. package/dist/smart-agent/resolve-reranker.d.ts.map +1 -0
  56. package/dist/smart-agent/resolve-reranker.js +8 -0
  57. package/dist/smart-agent/resolve-reranker.js.map +1 -0
  58. package/dist/smart-agent/resolve-retrieval.d.ts +26 -0
  59. package/dist/smart-agent/resolve-retrieval.d.ts.map +1 -0
  60. package/dist/smart-agent/resolve-retrieval.js +96 -0
  61. package/dist/smart-agent/resolve-retrieval.js.map +1 -0
  62. package/dist/smart-agent/smart-server.d.ts +70 -6
  63. package/dist/smart-agent/smart-server.d.ts.map +1 -1
  64. package/dist/smart-agent/smart-server.js +221 -77
  65. package/dist/smart-agent/smart-server.js.map +1 -1
  66. package/dist/smart-agent/tools-rag-handle.d.ts.map +1 -1
  67. package/dist/smart-agent/tools-rag-handle.js +4 -2
  68. package/dist/smart-agent/tools-rag-handle.js.map +1 -1
  69. package/package.json +7 -7
@@ -6,8 +6,8 @@ import http from 'node:http';
6
6
  import { createRequire } from 'node:module';
7
7
  import { resolve as pathResolve } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
9
- import { asymmetricEmbedder, buildNamespacedTools, defaultToolNamespace, isReadinessReporter, SimpleRagProviderRegistry, symmetricEmbedder, } from '@mcp-abap-adt/llm-agent';
10
- import { ClaudeSkillManager, CodexSkillManager, FileSystemPluginLoader, FileSystemSkillManager, getDefaultPluginDirs, HealthChecker, InMemoryKnowledgeBackend, KnowledgeRag, mergePluginExports, SessionRequestLogger, SmartAgentBuilder, SmartAgentSubAgent, WindowContextStrategy, } from '@mcp-abap-adt/llm-agent-libs';
9
+ import { asymmetricEmbedder, buildNamespacedTools, CircuitBreaker, defaultToolNamespace, isReadinessReporter, SimpleRagProviderRegistry, symmetricEmbedder, withCircuitBreaker, } from '@mcp-abap-adt/llm-agent';
10
+ import { applyRetrievalStrategy, ClaudeSkillManager, CodexSkillManager, FileSystemPluginLoader, FileSystemSkillManager, getDefaultPluginDirs, HealthChecker, InMemoryKnowledgeBackend, KnowledgeRag, mergePluginExports, SessionRequestLogger, SmartAgentBuilder, SmartAgentSubAgent, WindowContextStrategy, } from '@mcp-abap-adt/llm-agent-libs';
11
11
  import { DefaultMcpFailureClassifier, MCPClientWrapper, McpClientAdapter, } from '@mcp-abap-adt/llm-agent-mcp';
12
12
  import { prefetchEmbedderFactories } from '@mcp-abap-adt/llm-agent-rag';
13
13
  import { PACKAGE_VERSION } from '../generated/version.js';
@@ -21,9 +21,12 @@ import { CORS_HEADERS, jsonError, writeNotReady, } from './http/response-helpers
21
21
  import { HttpRouteTable } from './http/route-table.js';
22
22
  import { handleSessionDelete, handleSessionResume, handleSessionsList, } from './http/sessions-route-handler.js';
23
23
  import { handleUsageRoute } from './http/usage-route-handler.js';
24
+ import { LlmCircuitBreakers } from './llm/llm-circuit-breakers.js';
24
25
  import { RoleLlmResolver, } from './llm/role-llm-resolver.js';
25
26
  import { assertRagConfigShape, embedderSectionFor, toMakeRagInput, } from './rag-config.js';
26
27
  import { resolveRetrievalEmbedder } from './resolve-agent-embedder.js';
28
+ import { resolveReranker } from './resolve-reranker.js';
29
+ import { resolveRetrievalStrategies, unknownRetrievalKeyWarnings, } from './resolve-retrieval.js';
27
30
  import { makeToolsRagHandle } from './tools-rag-handle.js';
28
31
  import { assertWorkerLlmConfig, parseWorkerLlm } from './worker-llm.js';
29
32
  export { writeNotReady } from './http/response-helpers.js';
@@ -236,6 +239,39 @@ export class SmartServer {
236
239
  _mainLlm;
237
240
  _classifierLlm;
238
241
  _helperLlm;
242
+ /**
243
+ * `circuitBreaker:` — one LLM breaker per `llm:` key, wrapped where each LLM
244
+ * is created (held main/classifier/helper and every resolver entry), so every
245
+ * session and role — controller and stepper included — shares it (§14.2).
246
+ */
247
+ _llmBreakers;
248
+ /** `circuitBreaker:` — the one embedder breaker, fed by the retrieval embedder. */
249
+ _embedderBreaker;
250
+ /** The one reranker of this server, resolved once in `_buildInfra()` (§7.4). */
251
+ _reranker;
252
+ /**
253
+ * `rag.retrieval` resolved once in `_buildInfra` — one strategy per listed
254
+ * store key, server-wide (§13.4). Applied at creation of the server-built
255
+ * stores (`tools` / `history`, workers' too) and, through
256
+ * `withRetrievalStrategy`, in every builder's `ragStores` projection.
257
+ */
258
+ _retrievalStrategies = new Map();
259
+ /** `agent.toolSelection`, resolved once; applied to every builder (§13.4). */
260
+ _toolSelectionStrategy;
261
+ /** Plugin output validator, resolved once; applied to every builder (§14.1). */
262
+ _outputValidator;
263
+ /** Plugin query expander, resolved once; applied to every builder (§14.1). */
264
+ _queryExpander;
265
+ /** DI > plugin > YAML `skills:`, resolved once; vectorized at startup only. */
266
+ _skillManager;
267
+ /**
268
+ * `agent.llmCallStrategy`, resolved once as a FACTORY: a strategy may be
269
+ * stateful (`fallback` disables streaming stickily), so each builder gets its
270
+ * own instance (§14.1).
271
+ */
272
+ _llmCallStrategyFactory;
273
+ /** DI > plugin > default `ClineClientAdapter`, resolved once (§14.1). */
274
+ _clientAdapters = [];
239
275
  _fileLogger;
240
276
  _mergedEmbedderFactories;
241
277
  /**
@@ -276,6 +312,12 @@ export class SmartServer {
276
312
  * (the stepper catalog handle), which falls back to catalog order regardless.
277
313
  */
278
314
  _toolsRag;
315
+ /**
316
+ * The strategy-wrapped history store, shared by the startup agent and every
317
+ * session agent (one store; reads are scoped per session by metadata).
318
+ * Unset when `rag:` is not configured.
319
+ */
320
+ _historyRag;
279
321
  /**
280
322
  * The providers every build of this server creates through, and the catalogs
281
323
  * a session registry is hydrated from. One object, handed to every build:
@@ -453,6 +495,9 @@ export class SmartServer {
453
495
  ...(deps.connectMcpWithDescriptors
454
496
  ? { connectMcpWithDescriptors: deps.connectMcpWithDescriptors }
455
497
  : {}),
498
+ ...(deps.makeDecisionModel
499
+ ? { makeDecisionModel: deps.makeDecisionModel }
500
+ : {}),
456
501
  };
457
502
  }
458
503
  async start() {
@@ -526,17 +571,25 @@ export class SmartServer {
526
571
  temperature: optionalNumber(helperCfg.temperature),
527
572
  })
528
573
  : undefined;
529
- this._mainLlm = mainLlm;
530
- this._classifierLlm = classifierLlm;
531
- this._helperLlm = helperLlm;
574
+ // `circuitBreaker:` → the shared breakers (§14.2): created once, before any
575
+ // LLM is held, and wrapped where each LLM is created — never in a builder.
576
+ if (this.cfg.circuitBreaker) {
577
+ this._llmBreakers = new LlmCircuitBreakers(this.cfg.circuitBreaker);
578
+ this._embedderBreaker = new CircuitBreaker(this.cfg.circuitBreaker);
579
+ }
580
+ this._mainLlm = this.guardLlm(mainLlm, 'main');
581
+ this._classifierLlm = this.guardLlm(classifierLlm, 'classifier');
582
+ this._helperLlm = helperLlm && this.guardLlm(helperLlm, 'helper');
532
583
  this._llmMap = llmMap;
533
584
  this._mainTemp = mainTemp;
585
+ const breakers = this._llmBreakers;
534
586
  this._roleLlm = new RoleLlmResolver({
535
587
  getMain: () => this._mainLlm,
536
588
  getHelper: () => this._helperLlm,
537
589
  getClassifier: () => this._classifierLlm,
538
590
  getLlmMap: () => this._llmMap,
539
591
  build: (entry) => this._deps.makeLlm(entry),
592
+ ...(breakers ? { wrap: (llm, key) => breakers.wrap(llm, key) } : {}),
540
593
  });
541
594
  // The programmatic subAgentConfigs path never passed the YAML check, so a
542
595
  // worker's named keys are checked here too (§4.6.7): startup fails loudly.
@@ -588,6 +641,54 @@ export class SmartServer {
588
641
  const registered = mergePluginExports(plugins, mod, spec);
589
642
  log({ event: 'plugin_specifier_loaded', spec, registered });
590
643
  }
644
+ // ---- Reranker (§7.4) -------------------------------------------------
645
+ // Resolved ONCE here — the infra build shared by start() and the embeddable
646
+ // buildAgent() — and applied by buildBaseBuilder outside the
647
+ // applyServerExtras gate, so per-session agents get it too.
648
+ this._reranker = await resolveReranker({
649
+ pluginReranker: plugins.reranker,
650
+ });
651
+ // ---- Validator, expander, skills, LLM-call strategy, adapters (§14.1) --
652
+ // Resolved ONCE here and applied by buildBaseBuilder to every builder
653
+ // (per-session agents serve the requests).
654
+ this._outputValidator = plugins.outputValidator;
655
+ this._queryExpander = plugins.queryExpander;
656
+ this._skillManager =
657
+ this.cfg.skillManager ??
658
+ plugins.skillManager ??
659
+ resolveSkillManager(this.cfg.skills);
660
+ const strategyName = this.cfg.agent?.llmCallStrategy;
661
+ if (strategyName) {
662
+ const { StreamingLlmCallStrategy, NonStreamingLlmCallStrategy, FallbackLlmCallStrategy, } = await import('@mcp-abap-adt/llm-agent');
663
+ const strategies = {
664
+ streaming: () => new StreamingLlmCallStrategy(),
665
+ 'non-streaming': () => new NonStreamingLlmCallStrategy(),
666
+ fallback: () => new FallbackLlmCallStrategy(this._fileLogger),
667
+ };
668
+ this._llmCallStrategyFactory = strategies[strategyName];
669
+ }
670
+ const { ClineClientAdapter } = await import('@mcp-abap-adt/llm-agent');
671
+ this._clientAdapters = [
672
+ ...(this.cfg.clientAdapters ?? []),
673
+ ...plugins.clientAdapters,
674
+ new ClineClientAdapter(),
675
+ ];
676
+ // ---- Per-store retrieval strategies (§13.4) ---------------------------
677
+ // Resolved ONCE, server-wide. An explicit per-store strategy (embedding
678
+ // included) wins over the plugin reranker above; an unlisted store keeps
679
+ // today's behaviour. Decision-backed rerankers share one decision model.
680
+ this._retrievalStrategies = await resolveRetrievalStrategies({
681
+ retrieval: this.cfg.rag?.retrieval,
682
+ decisionCfg: this.cfg.decision,
683
+ makeDecisionModel: this._deps.makeDecisionModel,
684
+ resolveLlm: (key) => this.roleLlm().resolveNamed(key),
685
+ });
686
+ const toolSelectionCfg = this.cfg.agent?.toolSelection;
687
+ this._toolSelectionStrategy = toolSelectionCfg?.strategy
688
+ ? resolveToolSelectionStrategy(toolSelectionCfg.strategy, {
689
+ minScore: toolSelectionCfg.minScore,
690
+ })
691
+ : undefined;
591
692
  // ---- Pipeline-plugin registry: factories (§4.6.7) -------------------
592
693
  // Built-ins are server code — parse, validate, construct with typed settings.
593
694
  // A dynamic instance export is registered as a factory that ignores its
@@ -656,7 +757,10 @@ export class SmartServer {
656
757
  // ONE retrieval embedder: stores write with its `embedDocument`, every
657
758
  // search path embeds with its `embedQuery` (an asymmetric model is two
658
759
  // halves behind it — see resolveRetrievalEmbedder).
659
- const resolvedEmbedder = await resolveRetrievalEmbedder(this.cfg.rag, this._deps.embedder ?? this.cfg.embedder, this._deps.resolveEmbedder, mergedEmbedderFactories, this._fileLogger);
760
+ const resolvedEmbedder = await resolveRetrievalEmbedder(this.cfg.rag, this._deps.embedder ?? this.cfg.embedder, this._deps.resolveEmbedder, mergedEmbedderFactories, this._fileLogger,
761
+ // The embedder breaker sees the real embedding calls (§14.2): each half
762
+ // is wrapped below the document/query role. Worker embedders keep none.
763
+ this.embedderBreakerWrap());
660
764
  // Hold the resolved embedder so buildServerCtx can thread it onto every
661
765
  // pipeline context (the controller pipeline needs it for target-state).
662
766
  this._resolvedEmbedder = resolvedEmbedder;
@@ -745,12 +849,13 @@ export class SmartServer {
745
849
  // through its own. Two calls, two stores — the history store never shared
746
850
  // the tools store's instance.
747
851
  const input = toMakeRagInput(this.cfg.rag.store, resolvedEmbedder, 'rag');
748
- toolsRag = await this._deps.makeRag(input);
749
- historyRag = await this._deps.makeRag(input);
852
+ toolsRag = this.withStrategy('tools', await this._deps.makeRag(input));
853
+ historyRag = this.withStrategy('history', await this._deps.makeRag(input));
750
854
  }
751
855
  // Capture the tools store for the flat/smart pipeline's ToolSelectHandler
752
856
  // (and white-box vectorization assertions). See field doc.
753
857
  this._toolsRag = toolsRag;
858
+ this._historyRag = historyRag;
754
859
  // NOTE: the legacy per-pipeline named-RAG multistore (`pipeline.rag.{name}`)
755
860
  // is GONE with the `pipeline: {name,config}` migration. The top-level `rag:`
756
861
  // block above is the single source of truth for the tools/history stores.
@@ -869,9 +974,10 @@ export class SmartServer {
869
974
  // Assemble everything EXCEPT the coordinator via the shared base-builder
870
975
  // factory; the coordinator gate below wires the chosen variant.
871
976
  const builder = await this.buildBaseBuilder({
872
- mainLlm,
873
- classifierLlm,
874
- helperLlm,
977
+ // The held (breaker-guarded) instances: the builder never wraps an LLM.
978
+ mainLlm: this._mainLlm,
979
+ classifierLlm: this._classifierLlm,
980
+ helperLlm: this._helperLlm,
875
981
  fileLogger,
876
982
  toolsRag,
877
983
  historyRag,
@@ -898,6 +1004,9 @@ export class SmartServer {
898
1004
  const { agent: smartAgent, chat, streamChat, close: closeAgent, circuitBreakers, ragStores, modelProvider, } = agentHandle;
899
1005
  const { ragRegistry: globalRagRegistry, mcpClients: globalMcpClients } = agentHandle;
900
1006
  this._globalRagRegistry = globalRagRegistry;
1007
+ for (const w of unknownRetrievalKeyWarnings(this.cfg.rag?.retrieval, globalRagRegistry.list().map((c) => c.name))) {
1008
+ this.warn(w);
1009
+ }
901
1010
  // Two limits of this server, stated rather than fixed (§6.4): it registers
902
1011
  // no RAG providers, so no session registry has a catalog to hydrate from —
903
1012
  // each holds only the deployment's globals; and its sessions carry a
@@ -1041,7 +1150,8 @@ export class SmartServer {
1041
1150
  agent: smartAgent,
1042
1151
  startTime,
1043
1152
  version: this.cfg.version ?? PACKAGE_VERSION,
1044
- circuitBreakers,
1153
+ // Re-read per check: a PUT /v1/config swap replaces a key's breaker.
1154
+ circuitBreakers: this.breakerList() ?? circuitBreakers,
1045
1155
  });
1046
1156
  // Startup health check removed — use `npm run models:check` for diagnostics.
1047
1157
  // Running health check at startup wastes rate-limit budget when combined
@@ -1261,11 +1371,13 @@ export class SmartServer {
1261
1371
  cache: this._workers.cache,
1262
1372
  // Worker-OWN tools RAG (from subCfg.rag, if declared). Built once;
1263
1373
  // re-wired per-session by reference — never re-vectorized.
1374
+ // rag.retrieval is server-wide: the worker's stores take the MAIN
1375
+ // config's strategy for their key (§13.4).
1264
1376
  makeToolsRag: subCfg.rag
1265
- ? async () => this._deps.makeRag(await this._workerRagInput(name, subCfg.rag, subCfg.embedder, embedderFactories))
1377
+ ? async () => this.withStrategy('tools', await this._deps.makeRag(await this._workerRagInput(name, subCfg.rag, subCfg.embedder, embedderFactories)))
1266
1378
  : undefined,
1267
1379
  makeHistoryRag: subCfg.rag
1268
- ? async () => this._deps.makeRag(await this._workerRagInput(name, subCfg.rag, subCfg.embedder, embedderFactories))
1380
+ ? async () => this.withStrategy('history', await this._deps.makeRag(await this._workerRagInput(name, subCfg.rag, subCfg.embedder, embedderFactories)))
1269
1381
  : undefined,
1270
1382
  // Worker-OWN MCP clients. DI list (subCfg.mcpClients) wins; otherwise
1271
1383
  // SmartAgentBuilder's own MCP-connect path handles `subCfg.mcp` — we
@@ -1327,6 +1439,12 @@ export class SmartServer {
1327
1439
  else if (injected?.mcpClients && injected.mcpClients.length > 0) {
1328
1440
  subBuilder = subBuilder.withMcpClients(injected.mcpClients);
1329
1441
  }
1442
+ // rag.retrieval is server-wide: a worker's projection (named collections
1443
+ // of the shared registry included) gets the same per-store strategies.
1444
+ // Its own tools/history are already wrapped; the brand keeps them as is.
1445
+ for (const [key, strategy] of this._retrievalStrategies) {
1446
+ subBuilder = subBuilder.withRetrievalStrategy(key, strategy);
1447
+ }
1330
1448
  const handle = await subBuilder.build();
1331
1449
  // Backfill the per-worker cache from the BUILT handle (review HIGH #7).
1332
1450
  // Only runs on the primary build path (no `injected`) so per-session
@@ -1341,6 +1459,16 @@ export class SmartServer {
1341
1459
  }
1342
1460
  // -- Pipeline-context dep sources (promoted from the inline coordinator-gate
1343
1461
  // closures; consumed by buildServerCtx, which later tasks call) ----------
1462
+ /**
1463
+ * A server-built store (`tools` / `history`, main or worker) wrapped in the
1464
+ * strategy `rag.retrieval` configures for its key; unlisted → unchanged.
1465
+ * Idempotent (`applyRetrievalStrategy`), so the builder's projection of the
1466
+ * same store never wraps it twice — one rerank per query.
1467
+ */
1468
+ withStrategy(key, store) {
1469
+ const strategy = this._retrievalStrategies.get(key);
1470
+ return strategy ? applyRetrievalStrategy(store, strategy) : store;
1471
+ }
1344
1472
  /** A worker's own store input: its embedder through the seam, then paired. */
1345
1473
  async _workerRagInput(name, rag, diEmbedder, extraFactories) {
1346
1474
  const embedder = await resolveRetrievalEmbedder(rag, diEmbedder, this._deps.resolveEmbedder, extraFactories, this._fileLogger);
@@ -1361,6 +1489,23 @@ export class SmartServer {
1361
1489
  ? this.roleLlm().resolveNamed(key)
1362
1490
  : this.roleLlm().resolve(role);
1363
1491
  }
1492
+ /** `llm` behind its key's circuit breaker when `circuitBreaker:` is set. */
1493
+ guardLlm(llm, key) {
1494
+ return this._llmBreakers ? this._llmBreakers.wrap(llm, key) : llm;
1495
+ }
1496
+ /** The `wrap` argument of the main `resolveRetrievalEmbedder` call, if any. */
1497
+ embedderBreakerWrap() {
1498
+ const breaker = this._embedderBreaker;
1499
+ return breaker ? (e) => withCircuitBreaker(e, breaker) : undefined;
1500
+ }
1501
+ /** `/health`'s breakers: every key's current LLM breaker, then the embedder's. */
1502
+ breakerList() {
1503
+ const llm = this._llmBreakers;
1504
+ const embedder = this._embedderBreaker;
1505
+ if (!llm || !embedder)
1506
+ return undefined;
1507
+ return () => [...llm.list(), embedder];
1508
+ }
1364
1509
  roleLlm() {
1365
1510
  if (!this._roleLlm) {
1366
1511
  throw new Error('role LLM lookup invoked before _buildInfra built the resolver');
@@ -1773,11 +1918,14 @@ export class SmartServer {
1773
1918
  * supplies its own scope's values (startup = global; session = session-scoped);
1774
1919
  * every `.withXxx` is applied conditionally on its `parts` field so both work.
1775
1920
  *
1776
- * `applyServerExtras` gates the startup-only, config/plugin-derived wiring
1777
- * (circuit breaker, reranker/queryExpander/outputValidator, skill manager,
1778
- * LLM-call & tool-selection strategies, client adapters, and the YAML `mcp:`
1779
- * connect path). The per-session re-wire omits these (it inherits a slimmer
1780
- * agent) so they stay gated to preserve behavior.
1921
+ * `applyServerExtras` now guards only the YAML `mcp:` auto-connect fallback,
1922
+ * which stays startup-only (one connection). It also decides whether the skill
1923
+ * manager vectorizes its skills into the tools store (startup build only).
1924
+ * Not gated, because per-session agents serve the requests: the reranker
1925
+ * (§7.4), the per-store retrieval strategies and `agent.toolSelection`
1926
+ * (§13.4), the shared embedder circuit breaker (§14.2), and the output
1927
+ * validator, query expander, skill manager, LLM-call strategy and client
1928
+ * adapters (§14.1).
1781
1929
  */
1782
1930
  async buildBaseBuilder(parts) {
1783
1931
  let builder = new SmartAgentBuilder({
@@ -1820,64 +1968,55 @@ export class SmartServer {
1820
1968
  if (parts.requestLogger) {
1821
1969
  builder = builder.withRequestLogger(parts.requestLogger);
1822
1970
  }
1823
- if (parts.applyServerExtras) {
1824
- const plugins = parts.plugins;
1825
- if (this.cfg.circuitBreaker) {
1826
- builder = builder.withCircuitBreaker(this.cfg.circuitBreaker);
1827
- }
1828
- if (plugins?.reranker) {
1829
- builder = builder.withReranker(plugins.reranker);
1830
- }
1831
- if (plugins?.queryExpander) {
1832
- builder = builder.withQueryExpander(plugins.queryExpander);
1833
- }
1834
- if (plugins?.outputValidator) {
1835
- builder = builder.withOutputValidator(plugins.outputValidator);
1836
- }
1837
- // Skill manager (DI > YAML config > plugin)
1838
- const skillManager = this.cfg.skillManager ??
1839
- plugins?.skillManager ??
1840
- resolveSkillManager(this.cfg.skills);
1841
- if (skillManager) {
1842
- builder = builder.withSkillManager(skillManager);
1843
- }
1844
- // LLM call strategy (from agent config)
1845
- const strategyName = this.cfg.agent?.llmCallStrategy;
1846
- if (strategyName) {
1847
- const { StreamingLlmCallStrategy, NonStreamingLlmCallStrategy, FallbackLlmCallStrategy, } = await import('@mcp-abap-adt/llm-agent');
1848
- const strategies = {
1849
- streaming: () => new StreamingLlmCallStrategy(),
1850
- 'non-streaming': () => new NonStreamingLlmCallStrategy(),
1851
- fallback: () => new FallbackLlmCallStrategy(parts.fileLogger),
1852
- };
1853
- const factory = strategies[strategyName];
1854
- if (factory) {
1855
- builder = builder.withLlmCallStrategy(factory());
1856
- }
1857
- }
1858
- // Tool-selection strategy (from agent.toolSelection config)
1859
- const toolSelectionCfg = this.cfg.agent?.toolSelection;
1860
- if (toolSelectionCfg?.strategy) {
1861
- builder = builder.withToolSelectionStrategy(resolveToolSelectionStrategy(toolSelectionCfg.strategy, {
1862
- minScore: toolSelectionCfg.minScore,
1863
- }));
1864
- }
1971
+ // Not gated: requests are served by per-session agents, built with
1972
+ // applyServerExtras=false; the startup agent is infrastructure only.
1973
+ if (this._reranker) {
1974
+ builder = builder.withReranker(this._reranker);
1975
+ }
1976
+ // Not gated, same reason: the per-store strategies reach every store the
1977
+ // pipeline reads through the builder's `ragStores` projection (named
1978
+ // collections included). Server-built stores are already wrapped; the
1979
+ // projection sees the brand and leaves them as they are.
1980
+ for (const [key, strategy] of this._retrievalStrategies) {
1981
+ builder = builder.withRetrievalStrategy(key, strategy);
1982
+ }
1983
+ // Not gated, same reason: `agent.toolSelection` must reach the per-session
1984
+ // agents that serve requests (§13.4). With a reranked `tools` store,
1985
+ // `minScore` compares reranker probabilities in [0, 1], not cosine.
1986
+ if (this._toolSelectionStrategy) {
1987
+ builder = builder.withToolSelectionStrategy(this._toolSelectionStrategy);
1988
+ }
1989
+ // Not gated, same reason: the ONE embedder breaker guards the stores of
1990
+ // every agent (§14.2). The LLMs given above are already breaker-guarded.
1991
+ if (this._embedderBreaker) {
1992
+ builder = builder.withCircuitBreakers({
1993
+ embedder: this._embedderBreaker,
1994
+ });
1995
+ }
1996
+ // Not gated, same reason: the output validator, query expander, skill
1997
+ // manager, LLM-call strategy and client adapters reach the per-session
1998
+ // agents that serve requests (§14.1). Skills are vectorized into the tools
1999
+ // store by the startup build only.
2000
+ if (this._queryExpander) {
2001
+ builder = builder.withQueryExpander(this._queryExpander);
2002
+ }
2003
+ if (this._outputValidator) {
2004
+ builder = builder.withOutputValidator(this._outputValidator);
2005
+ }
2006
+ if (this._skillManager) {
2007
+ builder = builder.withSkillManager(this._skillManager, {
2008
+ vectorize: parts.applyServerExtras,
2009
+ });
2010
+ }
2011
+ if (this._llmCallStrategyFactory) {
2012
+ builder = builder.withLlmCallStrategy(this._llmCallStrategyFactory());
2013
+ }
2014
+ for (const adapter of this._clientAdapters) {
2015
+ builder = builder.withClientAdapter(adapter);
1865
2016
  }
1866
2017
  if (parts.mcpClients) {
1867
2018
  builder = builder.withMcpClients(parts.mcpClients);
1868
2019
  }
1869
- if (parts.applyServerExtras) {
1870
- // Client adapters (DI > plugin; ClineClientAdapter is the default).
1871
- const { ClineClientAdapter } = await import('@mcp-abap-adt/llm-agent');
1872
- const adapterSources = [
1873
- ...(this.cfg.clientAdapters ?? []),
1874
- ...(parts.plugins?.clientAdapters ?? []),
1875
- new ClineClientAdapter(),
1876
- ];
1877
- for (const adapter of adapterSources) {
1878
- builder = builder.withClientAdapter(adapter);
1879
- }
1880
- }
1881
2020
  if (parts.workerRegistry.size > 0) {
1882
2021
  builder = builder.withSubAgents(parts.workerRegistry);
1883
2022
  }
@@ -1932,6 +2071,7 @@ export class SmartServer {
1932
2071
  const inst = await this.buildPipelineInstance({
1933
2072
  sessionId: parts.sessionId,
1934
2073
  parts,
2074
+ historyRag: this._historyRag,
1935
2075
  });
1936
2076
  // Register the pipeline's disposal hook keyed by sessionId. A prior
1937
2077
  // instance for the same sessionId (e.g. invalidateAll rebuild) is closed
@@ -2125,7 +2265,7 @@ export class SmartServer {
2125
2265
  return;
2126
2266
  }
2127
2267
  await rc.server._withSession(rc.req, rc.res, async (graph, sessionId, traceId) => {
2128
- await handleAdapterRequest(rc.req, rc.res, graph.agent ?? rc.smartAgent, anthropicAdapter, { sessionId, traceId, graph }, this.cfg.agent?.heartbeatIntervalMs);
2268
+ await handleAdapterRequest(rc.req, rc.res, graph.agent ?? rc.smartAgent, anthropicAdapter, { sessionId, traceId, graph }, this.cfg.agent?.heartbeatIntervalMs, rc.log);
2129
2269
  });
2130
2270
  },
2131
2271
  });
@@ -2156,14 +2296,18 @@ export class SmartServer {
2156
2296
  return {
2157
2297
  modelResolver: this.cfg.modelResolver,
2158
2298
  skipModelValidation: this.cfg.skipModelValidation === true,
2299
+ // A swapped-in LLM gets a fresh breaker for its key (§14.2).
2159
2300
  setMainLlm: (llm) => {
2160
- this._mainLlm = llm;
2301
+ this._mainLlm = this.guardLlm(llm, 'main');
2302
+ return this._mainLlm;
2161
2303
  },
2162
2304
  setClassifierLlm: (llm) => {
2163
- this._classifierLlm = llm;
2305
+ this._classifierLlm = this.guardLlm(llm, 'classifier');
2306
+ return this._classifierLlm;
2164
2307
  },
2165
2308
  setHelperLlm: (llm) => {
2166
- this._helperLlm = llm;
2309
+ this._helperLlm = this.guardLlm(llm, 'helper');
2310
+ return this._helperLlm;
2167
2311
  },
2168
2312
  mirrorAgentCfg: (patch) => {
2169
2313
  const merged = {