@mcp-abap-adt/llm-agent-libs 20.0.0 → 20.5.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 (104) hide show
  1. package/dist/adapters/llm-adapter.d.ts.map +1 -1
  2. package/dist/adapters/llm-adapter.js +16 -7
  3. package/dist/adapters/llm-adapter.js.map +1 -1
  4. package/dist/agent/rag-helpers.d.ts +20 -0
  5. package/dist/agent/rag-helpers.d.ts.map +1 -0
  6. package/dist/agent/rag-helpers.js +61 -0
  7. package/dist/agent/rag-helpers.js.map +1 -0
  8. package/dist/agent/rag-orchestrator-types.d.ts +53 -0
  9. package/dist/agent/rag-orchestrator-types.d.ts.map +1 -0
  10. package/dist/agent/rag-orchestrator-types.js +2 -0
  11. package/dist/agent/rag-orchestrator-types.js.map +1 -0
  12. package/dist/agent/rag-orchestrator.d.ts +14 -0
  13. package/dist/agent/rag-orchestrator.d.ts.map +1 -0
  14. package/dist/agent/rag-orchestrator.js +352 -0
  15. package/dist/agent/rag-orchestrator.js.map +1 -0
  16. package/dist/agent.d.ts +13 -9
  17. package/dist/agent.d.ts.map +1 -1
  18. package/dist/agent.js +168 -831
  19. package/dist/agent.js.map +1 -1
  20. package/dist/builder-types.d.ts +64 -0
  21. package/dist/builder-types.d.ts.map +1 -0
  22. package/dist/builder-types.js +18 -0
  23. package/dist/builder-types.js.map +1 -0
  24. package/dist/builder.d.ts +27 -51
  25. package/dist/builder.d.ts.map +1 -1
  26. package/dist/builder.js +86 -224
  27. package/dist/builder.js.map +1 -1
  28. package/dist/coordinator/planning/one-shot.d.ts.map +1 -1
  29. package/dist/coordinator/planning/one-shot.js +13 -2
  30. package/dist/coordinator/planning/one-shot.js.map +1 -1
  31. package/dist/health/agent-health.d.ts +13 -0
  32. package/dist/health/agent-health.d.ts.map +1 -0
  33. package/dist/health/agent-health.js +82 -0
  34. package/dist/health/agent-health.js.map +1 -0
  35. package/dist/health/health-checker.d.ts.map +1 -1
  36. package/dist/health/health-checker.js +4 -4
  37. package/dist/health/health-checker.js.map +1 -1
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/interfaces/pipeline.d.ts +5 -1
  43. package/dist/interfaces/pipeline.d.ts.map +1 -1
  44. package/dist/mcp/tool-registry.d.ts +22 -0
  45. package/dist/mcp/tool-registry.d.ts.map +1 -0
  46. package/dist/mcp/tool-registry.js +57 -0
  47. package/dist/mcp/tool-registry.js.map +1 -0
  48. package/dist/mcp/vectorize-mcp-tools.d.ts +11 -0
  49. package/dist/mcp/vectorize-mcp-tools.d.ts.map +1 -0
  50. package/dist/mcp/vectorize-mcp-tools.js +191 -0
  51. package/dist/mcp/vectorize-mcp-tools.js.map +1 -0
  52. package/dist/pipeline/context/tool-loop-context/index.d.ts +7 -0
  53. package/dist/pipeline/context/tool-loop-context/index.d.ts.map +1 -0
  54. package/dist/pipeline/context/tool-loop-context/index.js +5 -0
  55. package/dist/pipeline/context/tool-loop-context/index.js.map +1 -0
  56. package/dist/pipeline/context/tool-loop-context/legacy-accumulate-context-strategy.d.ts +10 -0
  57. package/dist/pipeline/context/tool-loop-context/legacy-accumulate-context-strategy.d.ts.map +1 -0
  58. package/dist/pipeline/context/tool-loop-context/legacy-accumulate-context-strategy.js +25 -0
  59. package/dist/pipeline/context/tool-loop-context/legacy-accumulate-context-strategy.js.map +1 -0
  60. package/dist/pipeline/context/tool-loop-context/legacy-transcript-context-strategy.d.ts +15 -0
  61. package/dist/pipeline/context/tool-loop-context/legacy-transcript-context-strategy.d.ts.map +1 -0
  62. package/dist/pipeline/context/tool-loop-context/legacy-transcript-context-strategy.js +34 -0
  63. package/dist/pipeline/context/tool-loop-context/legacy-transcript-context-strategy.js.map +1 -0
  64. package/dist/pipeline/context/tool-loop-context/rag-recall-context-strategy.d.ts +22 -0
  65. package/dist/pipeline/context/tool-loop-context/rag-recall-context-strategy.d.ts.map +1 -0
  66. package/dist/pipeline/context/tool-loop-context/rag-recall-context-strategy.js +51 -0
  67. package/dist/pipeline/context/tool-loop-context/rag-recall-context-strategy.js.map +1 -0
  68. package/dist/pipeline/context/tool-loop-context/window-context-strategy.d.ts +15 -0
  69. package/dist/pipeline/context/tool-loop-context/window-context-strategy.d.ts.map +1 -0
  70. package/dist/pipeline/context/tool-loop-context/window-context-strategy.js +38 -0
  71. package/dist/pipeline/context/tool-loop-context/window-context-strategy.js.map +1 -0
  72. package/dist/pipeline/context.d.ts +5 -1
  73. package/dist/pipeline/context.d.ts.map +1 -1
  74. package/dist/pipeline/default-pipeline.d.ts.map +1 -1
  75. package/dist/pipeline/default-pipeline.js +2 -0
  76. package/dist/pipeline/default-pipeline.js.map +1 -1
  77. package/dist/pipeline/handlers/escalate-if-unavailable.d.ts +33 -0
  78. package/dist/pipeline/handlers/escalate-if-unavailable.d.ts.map +1 -0
  79. package/dist/pipeline/handlers/escalate-if-unavailable.js +22 -0
  80. package/dist/pipeline/handlers/escalate-if-unavailable.js.map +1 -0
  81. package/dist/pipeline/handlers/pass-through.d.ts +3 -0
  82. package/dist/pipeline/handlers/pass-through.d.ts.map +1 -0
  83. package/dist/pipeline/handlers/pass-through.js +77 -0
  84. package/dist/pipeline/handlers/pass-through.js.map +1 -0
  85. package/dist/pipeline/handlers/tool-loop-core.d.ts +103 -0
  86. package/dist/pipeline/handlers/tool-loop-core.d.ts.map +1 -0
  87. package/dist/pipeline/handlers/tool-loop-core.js +265 -0
  88. package/dist/pipeline/handlers/tool-loop-core.js.map +1 -0
  89. package/dist/pipeline/handlers/tool-loop.d.ts.map +1 -1
  90. package/dist/pipeline/handlers/tool-loop.js +155 -249
  91. package/dist/pipeline/handlers/tool-loop.js.map +1 -1
  92. package/dist/pipeline/pipeline-to-stream.d.ts +5 -0
  93. package/dist/pipeline/pipeline-to-stream.d.ts.map +1 -0
  94. package/dist/pipeline/pipeline-to-stream.js +49 -0
  95. package/dist/pipeline/pipeline-to-stream.js.map +1 -0
  96. package/dist/skills/plugin-host/github-transport.d.ts +35 -0
  97. package/dist/skills/plugin-host/github-transport.d.ts.map +1 -0
  98. package/dist/skills/plugin-host/github-transport.js +124 -0
  99. package/dist/skills/plugin-host/github-transport.js.map +1 -0
  100. package/dist/skills/plugin-host/index.d.ts +1 -0
  101. package/dist/skills/plugin-host/index.d.ts.map +1 -1
  102. package/dist/skills/plugin-host/index.js +1 -0
  103. package/dist/skills/plugin-host/index.js.map +1 -1
  104. package/package.json +14 -14
package/dist/agent.js CHANGED
@@ -1,15 +1,22 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { getStreamToolCallName, NoopQueryExpander, NoopToolCache, normalizeExternalTools, OrchestratorError, QueryEmbedding, StreamingLlmCallStrategy, TextOnlyEmbedding, toToolCallDelta, } from '@mcp-abap-adt/llm-agent';
2
+ import { getStreamToolCallName, isReadinessReporter, NoopQueryExpander, NoopToolCache, normalizeExternalTools, OrchestratorError, QueryEmbedding, StreamingLlmCallStrategy, TextOnlyEmbedding, toToolCallDelta, } from '@mcp-abap-adt/llm-agent';
3
3
  import { wrapEmbedder } from './adapters/usage-logging-embedder.js';
4
+ import { RagOrchestrator } from './agent/rag-orchestrator.js';
4
5
  import { normalizeRequestOptions } from './agent-request-options.js';
5
6
  import { LlmClassifier } from './classifier/llm-classifier.js';
7
+ import { buildAgentHealthSnapshot } from './health/agent-health.js';
6
8
  export { OrchestratorError, } from '@mcp-abap-adt/llm-agent';
7
9
  import { NoopRequestLogger } from './logger/noop-request-logger.js';
8
10
  import { summaryToUsage } from './logger/session-request-logger.js';
11
+ import { McpToolRegistry } from './mcp/tool-registry.js';
9
12
  import { NoopMetrics } from './metrics/noop-metrics.js';
13
+ import { LegacyAccumulateContextStrategy } from './pipeline/context/tool-loop-context/index.js';
14
+ import { runPassThrough } from './pipeline/handlers/pass-through.js';
15
+ import { buildBlockedToolMessages, buildHallucinatedToolMessages, classifyToolCalls, executeToolBatchWithHeartbeat, filterAvailableTools, injectPendingResults, injectToolPriority, runOutputValidationReprompt, } from './pipeline/handlers/tool-loop-core.js';
16
+ import { pipelineToStream } from './pipeline/pipeline-to-stream.js';
10
17
  import { fireInternalToolsAsync } from './policy/mixed-tool-call-handler.js';
11
18
  import { PendingToolResultsRegistry } from './policy/pending-tool-results-registry.js';
12
- import { isToolContextUnavailableError, ToolAvailabilityRegistry, } from './policy/tool-availability-registry.js';
19
+ import { ToolAvailabilityRegistry } from './policy/tool-availability-registry.js';
13
20
  import { NoopReranker } from './reranker/noop-reranker.js';
14
21
  import { NoopSessionManager } from './session/noop-session-manager.js';
15
22
  import { NoopTracer } from './tracer/noop-tracer.js';
@@ -46,7 +53,7 @@ export class SmartAgent {
46
53
  pendingToolResults;
47
54
  requestLogger;
48
55
  defaultLlmCallStrategy;
49
- _activeClients;
56
+ mcpToolRegistry;
50
57
  _mainLlm;
51
58
  _classifierLlm;
52
59
  _helperLlm;
@@ -70,7 +77,7 @@ export class SmartAgent {
70
77
  deps.embedder = wrapEmbedder(deps.embedder);
71
78
  this.defaultLlmCallStrategy =
72
79
  deps.llmCallStrategy ?? new StreamingLlmCallStrategy();
73
- this._activeClients = [...deps.mcpClients];
80
+ this.mcpToolRegistry = new McpToolRegistry(deps.mcpClients, deps.connectionStrategy, deps.ragStores);
74
81
  this._mainLlm = deps.mainLlm;
75
82
  this._helperLlm = deps.helperLlm;
76
83
  this._classifier = deps.classifier;
@@ -80,29 +87,6 @@ export class SmartAgent {
80
87
  get currentMainLlm() {
81
88
  return this._mainLlm;
82
89
  }
83
- async _resolveActiveClients(opts) {
84
- if (!this.deps.connectionStrategy)
85
- return;
86
- const result = await this.deps.connectionStrategy.resolve(this._activeClients, opts);
87
- this._activeClients = result.clients;
88
- if (result.toolsChanged) {
89
- await this._revectorizeTools(result.clients, opts);
90
- }
91
- }
92
- async _revectorizeTools(clients, opts) {
93
- const toolsRag = this.deps.ragStores.tools ?? Object.values(this.deps.ragStores)[0];
94
- if (!toolsRag)
95
- return;
96
- for (const client of clients) {
97
- const result = await client.listTools(opts);
98
- if (!result.ok)
99
- continue;
100
- for (const tool of result.value) {
101
- const text = `Tool: ${tool.name} — ${tool.description}`;
102
- await toolsRag.writer?.()?.upsertRaw(`tool:${tool.name}`, text, {});
103
- }
104
- }
105
- }
106
90
  /** Apply a partial config update at runtime (hot-reload). */
107
91
  applyConfigUpdate(update) {
108
92
  this.config = { ...this.config, ...update };
@@ -235,6 +219,16 @@ export class SmartAgent {
235
219
  classificationEnabled: this.config.classificationEnabled,
236
220
  };
237
221
  }
222
+ /**
223
+ * Readiness (implements `IReadinessReporter`): delegate to the MCP connection
224
+ * strategy when it reports readiness, else `true` (no strategy / non-reporting ⇒
225
+ * readiness unknown → ready). Consumers (e.g. a server's `/health` + request
226
+ * gate) detect this via `isReadinessReporter(agent)` — no growth of `ISmartAgent`.
227
+ */
228
+ isReady() {
229
+ const strategy = this.deps.connectionStrategy;
230
+ return isReadinessReporter(strategy) ? strategy.isReady() : true;
231
+ }
238
232
  async healthCheck(options) {
239
233
  const HEALTH_TIMEOUT_MS = this.config.healthTimeoutMs ?? 5_000;
240
234
  const { signal: timeoutSignal, clear: clearTimeout_ } = createTimeoutSignal(HEALTH_TIMEOUT_MS);
@@ -244,77 +238,13 @@ export class SmartAgent {
244
238
  signal: merged.signal,
245
239
  maxTokens: 1,
246
240
  };
247
- const results = {
248
- llm: false,
249
- rag: false,
250
- mcp: [],
251
- };
252
- try {
253
- if (this._mainLlm.healthCheck) {
254
- const hc = await this._mainLlm.healthCheck(healthOptions);
255
- results.llm = hc.ok && hc.value;
256
- }
257
- else {
258
- // Fallback for ILlm implementations without healthCheck
259
- const llmRes = await this._mainLlm.chat([{ role: 'user', content: 'ping' }], [], healthOptions);
260
- results.llm = llmRes.ok;
261
- }
262
- }
263
- catch {
264
- results.llm = false;
265
- }
266
241
  try {
267
- const firstStore = Object.values(this.deps.ragStores)[0];
268
- const ragRes = firstStore
269
- ? await firstStore.healthCheck(healthOptions)
270
- : { ok: true, value: undefined };
271
- results.rag = ragRes.ok;
272
- }
273
- catch {
274
- results.rag = false;
275
- }
276
- try {
277
- const mcpChecks = await Promise.all(this._activeClients.map(async (client) => {
278
- try {
279
- if (client.healthCheck) {
280
- const hc = await client.healthCheck(healthOptions);
281
- return {
282
- name: 'mcp-client',
283
- ok: hc.ok,
284
- error: hc.ok || !hc.error
285
- ? undefined
286
- : hc.error instanceof Error
287
- ? hc.error.message
288
- : String(hc.error),
289
- };
290
- }
291
- // Fallback for IMcpClient implementations without healthCheck
292
- const tools = await client.listTools(healthOptions);
293
- return {
294
- name: 'mcp-client',
295
- ok: tools.ok,
296
- error: tools.ok || !tools.error
297
- ? undefined
298
- : tools.error instanceof Error
299
- ? tools.error.message
300
- : String(tools.error),
301
- };
302
- }
303
- catch (err) {
304
- return {
305
- name: 'mcp-client',
306
- ok: false,
307
- error: err instanceof Error ? err.message : String(err),
308
- };
309
- }
310
- }));
311
- results.mcp = mcpChecks;
242
+ const snapshot = await buildAgentHealthSnapshot(this._mainLlm, this.deps.ragStores, this.mcpToolRegistry.getActiveClients(), healthOptions);
243
+ return { ok: true, value: snapshot };
312
244
  }
313
- catch {
314
- // AbortSignal timeout — leave mcp as empty
245
+ finally {
246
+ clearTimeout_();
315
247
  }
316
- clearTimeout_();
317
- return { ok: true, value: results };
318
248
  }
319
249
  async process(textOrMessages, options) {
320
250
  let content = '';
@@ -432,372 +362,67 @@ export class SmartAgent {
432
362
  this.requestLogger.startRequest(traceId);
433
363
  try {
434
364
  if (mode === 'pass') {
435
- const messages = typeof textOrMessages === 'string'
365
+ const passMessages = typeof textOrMessages === 'string'
436
366
  ? [{ role: 'user', content: textOrMessages }]
437
367
  : textOrMessages;
438
368
  opts?.sessionLogger?.logStep('client_request', { textOrMessages });
439
- const passStart = Date.now();
440
- const traceId2 = opts?.trace?.traceId;
441
- const stream = this._mainLlm.streamChat(messages, externalTools, opts);
442
- let passContent = '';
443
- const passToolCalls = [];
444
- let accPrompt = 0;
445
- let accCompletion = 0;
446
- let accTotal = 0;
447
- let hasUsage = false;
448
- const logPassUsage = () => {
449
- // Log only if a usage chunk was actually seen (mirrors
450
- // LoggingLlm.streamChat) — avoids creating a zero tool-loop bucket.
451
- if (!hasUsage)
452
- return;
453
- this.requestLogger.logLlmCall({
454
- component: 'tool-loop',
455
- model: this._mainLlm.model ?? 'unknown',
456
- promptTokens: accPrompt,
457
- completionTokens: accCompletion,
458
- totalTokens: accTotal,
459
- durationMs: Date.now() - passStart,
460
- requestId: traceId2,
461
- });
462
- };
463
- for await (const chunk of stream) {
464
- if (!chunk.ok) {
465
- // process() returns on the first error chunk → post-loop code never
466
- // runs. Log accumulated (partial) spend BEFORE yielding the error.
467
- logPassUsage();
468
- yield chunk;
469
- rootSpan.setStatus('ok');
470
- rootSpan.end();
471
- return;
472
- }
473
- if (chunk.value.reset) {
474
- passContent = '';
475
- passToolCalls.length = 0;
476
- continue;
477
- }
478
- if (chunk.value.content)
479
- passContent += chunk.value.content;
480
- if (chunk.value.toolCalls)
481
- passToolCalls.push(...chunk.value.toolCalls);
482
- if (chunk.value.usage) {
483
- accPrompt += chunk.value.usage.promptTokens;
484
- accCompletion += chunk.value.usage.completionTokens;
485
- accTotal += chunk.value.usage.totalTokens;
486
- hasUsage = true;
487
- }
488
- // Strip usage from the forwarded chunk: the single usage-bearing chunk
489
- // is the terminal getSummary chunk below (one usage chunk per request).
490
- const { usage: _omitUsage, ...rest } = chunk.value;
491
- yield { ok: true, value: rest };
369
+ for await (const chunk of runPassThrough(this._mainLlm, this.requestLogger, passMessages, externalTools, opts)) {
370
+ yield chunk;
492
371
  }
493
- opts?.sessionLogger?.logStep('llm_response_pass', {
494
- content: passContent,
495
- toolCalls: passToolCalls.length > 0 ? passToolCalls : undefined,
496
- });
497
- logPassUsage();
498
- const passSummary = traceId2
499
- ? this.requestLogger.getSummary(traceId2)
500
- : undefined;
501
- yield {
502
- ok: true,
503
- value: {
504
- content: '',
505
- finishReason: 'stop',
506
- ...(passSummary
507
- ? {
508
- usage: {
509
- ...summaryToUsage(passSummary),
510
- models: passSummary.byModel,
511
- },
512
- }
513
- : {}),
514
- },
515
- };
516
372
  rootSpan.setStatus('ok');
517
373
  rootSpan.end();
518
374
  return;
519
375
  }
520
376
  // Pipeline path (when configured via Builder)
521
377
  if (this.deps.pipeline) {
522
- const stream = this._runStructuredPipeline(textOrMessages, externalTools, opts, rootSpan, sessionId);
378
+ const stream = pipelineToStream(this.deps.pipeline, textOrMessages, externalTools, opts);
523
379
  for await (const chunk of stream)
524
380
  yield chunk;
525
381
  rootSpan.setStatus('ok');
526
382
  rootSpan.end();
527
383
  return;
528
384
  }
529
- // 1. Unified Preparation (default hardcoded flow)
530
- const initResult = await this._preparePipeline(textOrMessages, opts, rootSpan);
531
- if (!initResult.ok) {
532
- rootSpan.setStatus('error', initResult.error.message);
533
- rootSpan.end();
534
- yield initResult;
535
- return;
536
- }
537
- let { processedHistory } = initResult.value;
538
- const { subprompts, toolClientMap } = initResult.value;
539
- // Token budget check — summarize if over budget
540
- if (this.sessionManager.isOverBudget()) {
541
- const sumResult = await this._summarizeHistory(processedHistory, opts);
542
- if (sumResult.ok)
543
- processedHistory = sumResult.value;
544
- this.sessionManager.reset();
545
- }
546
- // 2. Decide context and tools for the WHOLE request
547
- await this._resolveActiveClients(opts);
548
- const actions = subprompts.filter((sp) => sp.type === 'action');
549
- const hasActions = actions.length > 0;
550
- const hasMcpClients = this._activeClients.length > 0;
551
- const hasRagStores = Object.keys(this.deps.ragStores).length > 0;
552
- const shouldRetrieve = mode === 'hard' || (hasActions && (hasMcpClients || hasRagStores));
553
- let finalTools = [];
554
- let retrieved = {
555
- ragResults: {},
556
- tools: [],
557
- };
558
- let skillContent = '';
559
- if (shouldRetrieve) {
560
- // Collect all action texts for RAG
561
- const combinedActionText = actions.map((a) => a.text).join(' ');
562
- // Translate + expand once (only used for stores in translateQueryStores)
563
- const translateStores = this.deps.translateQueryStores;
564
- let translatedText;
565
- if (translateStores && translateStores.size > 0) {
566
- translatedText = await this._toEnglishForRag(combinedActionText, opts);
567
- if (this.config.queryExpansionEnabled) {
568
- const expandResult = await this.queryExpander.expand(translatedText, opts);
569
- if (expandResult.ok)
570
- translatedText = expandResult.value;
571
- }
572
- }
573
- const k = this.config.ragQueryK ?? 10;
574
- const ragSpan = this.tracer.startSpan('smart_agent.rag_query', {
575
- parent: rootSpan,
576
- attributes: { 'rag.k': k },
577
- });
578
- const storeEntries = Object.entries(this.deps.ragStores);
579
- // Build per-store embedding: translated for translateQuery stores, original for others
580
- const mkEmbed = (text) => this.deps.embedder
581
- ? new QueryEmbedding(text, this.deps.embedder, opts)
582
- : new TextOnlyEmbedding(text);
583
- // Cache embeddings to avoid duplicate embed calls
584
- const originalEmbedding = mkEmbed(combinedActionText);
585
- const translatedEmbedding = translatedText && translatedText !== combinedActionText
586
- ? mkEmbed(translatedText)
587
- : originalEmbedding;
588
- const ragQueryResults = await Promise.all(storeEntries.map(([name, store]) => {
589
- const emb = translateStores?.has(name) && translatedText
590
- ? translatedEmbedding
591
- : originalEmbedding;
592
- return store.query(emb, k, opts).then((r) => ({ name, result: r }));
593
- }));
594
- ragSpan.end();
595
- const ragResultsMap = {};
596
- for (const { name, result: r } of ragQueryResults) {
597
- ragResultsMap[name] = r.ok ? r.value : [];
598
- this.metrics.ragQueryCount.add(1, {
599
- store: name,
600
- hit: String(r.ok && r.value.length > 0),
601
- });
602
- }
603
- // Rerank results
604
- // Rerank all stores in parallel, using matching query text per store
605
- const rerankedEntries = await Promise.all(Object.entries(ragResultsMap).map(async ([name, results]) => {
606
- if (results.length > 0) {
607
- const rerankText = translateStores?.has(name) && translatedText
608
- ? translatedText
609
- : combinedActionText;
610
- const rr = await this.reranker.rerank(rerankText, results, opts);
611
- return { name, results: rr.ok ? rr.value : results };
612
- }
613
- return { name, results };
614
- }));
615
- const rerankedMap = {};
616
- for (const { name, results } of rerankedEntries) {
617
- rerankedMap[name] = results;
618
- }
619
- const { tools: mcpTools } = await this._listAllTools(opts);
620
- // Collect all RAG results for tool discovery
621
- const allRagResults = Object.values(rerankedMap).flat();
622
- // Log RAG results with scores for diagnostics
623
- for (const [storeName, results] of Object.entries(rerankedMap)) {
624
- const logQuery = translateStores?.has(storeName) && translatedText
625
- ? translatedText
626
- : combinedActionText;
627
- opts?.sessionLogger?.logStep(`rag_query_${storeName}`, {
628
- query: logQuery.slice(0, 200),
629
- k,
630
- resultCount: results.length,
631
- results: results.map((r) => ({
632
- id: r.metadata.id,
633
- score: r.score,
634
- text: r.text.slice(0, 120),
635
- })),
636
- });
637
- }
638
- const ragToolNames = new Set(allRagResults
639
- .map((r) => r.metadata.id)
640
- .filter((id) => id?.startsWith('tool:'))
641
- .map((id) => id.slice(5).replace(/:.*$/, '')));
642
- const selectedMcpTools = ragToolNames.size > 0
643
- ? mcpTools.filter((t) => ragToolNames.has(t.name))
644
- : mode === 'hard'
645
- ? mcpTools
646
- : [];
647
- // Log tool selection diagnostics
648
- opts?.sessionLogger?.logStep('tools_selected', {
649
- totalMcp: mcpTools.length,
650
- ragMatchedTools: [...ragToolNames],
651
- selectedCount: selectedMcpTools.length + externalTools.length,
652
- selectedNames: [
653
- ...selectedMcpTools.map((t) => t.name),
654
- ...externalTools.map((t) => t.name),
655
- ],
656
- });
657
- retrieved = {
658
- ragResults: rerankedMap,
659
- tools: selectedMcpTools,
660
- };
661
- // D4: external (client) tools are always offered regardless of mode;
662
- // mode governs only the worker's INTERNAL execution posture.
663
- finalTools = [...selectedMcpTools, ...externalTools];
664
- opts?.sessionLogger?.logStep('external_tools_merge', {
665
- mode,
666
- mcpCount: selectedMcpTools.length,
667
- externalCount: externalTools.length,
668
- externalNames: externalTools.map((t) => t.name),
669
- finalCount: finalTools.length,
670
- });
671
- // Skill injection (when enabled and skillManager configured)
672
- if (this.config.skillInjectionEnabled !== false &&
673
- this.deps.skillManager) {
674
- const ragSkillNames = new Set(allRagResults
675
- .map((r) => r.metadata.id)
676
- .filter((id) => id?.startsWith('skill:'))
677
- .map((id) => id.slice(6)));
678
- // Fallback: dedicated RAG query when no skill:* in existing results
679
- if (ragSkillNames.size === 0) {
680
- const k = this.config.ragQueryK ?? 15;
681
- const storeEntries = Object.entries(this.deps.ragStores);
682
- const fallbackResults = await Promise.all(storeEntries.map(([name, store]) => {
683
- const text = translateStores?.has(name) && translatedText
684
- ? translatedText
685
- : combinedActionText;
686
- const emb = this.deps.embedder
687
- ? new QueryEmbedding(text, this.deps.embedder, opts)
688
- : new TextOnlyEmbedding(text);
689
- return store.query(emb, k, opts);
690
- }));
691
- for (const result of fallbackResults) {
692
- if (result.ok) {
693
- for (const r of result.value) {
694
- const id = r.metadata.id;
695
- if (id?.startsWith('skill:')) {
696
- ragSkillNames.add(id.slice(6));
697
- }
698
- }
699
- }
700
- }
701
- if (ragSkillNames.size > 0) {
702
- opts?.sessionLogger?.logStep('skill_select_rag_fallback', {
703
- query: combinedActionText.slice(0, 200),
704
- k,
705
- matchedSkills: [...ragSkillNames],
706
- });
707
- }
708
- }
709
- const allSkillsResult = await this.deps.skillManager.listSkills(opts);
710
- if (allSkillsResult.ok) {
711
- const allSkills = allSkillsResult.value;
712
- const matched = ragSkillNames.size > 0
713
- ? allSkills.filter((s) => ragSkillNames.has(s.name))
714
- : mode === 'hard'
715
- ? allSkills
716
- : [];
717
- const contentParts = [];
718
- for (const skill of matched) {
719
- const contentResult = await skill.getContent(undefined, opts);
720
- if (contentResult.ok && contentResult.value) {
721
- contentParts.push(`### Skill: ${skill.name}\n${contentResult.value}`);
722
- }
723
- }
724
- skillContent = contentParts.join('\n\n');
725
- opts?.sessionLogger?.logStep('skills_selected', {
726
- totalSkills: allSkills.length,
727
- ragMatchedSkills: [...ragSkillNames],
728
- selectedCount: matched.length,
729
- selectedNames: matched.map((s) => s.name),
730
- });
731
- }
732
- }
733
- }
734
- else {
735
- // If we're here, mode is definitely 'smart' (not 'hard' or 'pass')
736
- finalTools = externalTools;
737
- }
738
- const filteredTools = this.toolAvailabilityRegistry.filterTools(sessionId, finalTools);
739
- finalTools = filteredTools.allowed;
740
- if (filteredTools.blocked.length > 0) {
741
- opts?.sessionLogger?.logStep('active_tools_filtered_by_registry', {
742
- blocked: filteredTools.blocked,
743
- });
744
- }
745
- // 3. Assemble Context once
746
- const mainAction = actions.length > 1
747
- ? {
748
- type: 'action',
749
- text: actions.map((a) => a.text).join('\n'),
750
- context: actions.find((a) => a.context)?.context,
751
- dependency: 'independent',
752
- }
753
- : actions.length === 1
754
- ? actions[0]
755
- : subprompts.find((sp) => sp.type === 'chat') || subprompts[0];
756
- if (actions.length > 1) {
757
- opts?.sessionLogger?.logStep('actions_merged', {
758
- count: actions.length,
759
- actions: actions.map((a) => ({
760
- text: a.text,
761
- dependency: a.dependency,
762
- })),
763
- });
764
- }
765
- const assembleSpan = this.tracer.startSpan('smart_agent.assemble', {
766
- parent: rootSpan,
385
+ // Default hardcoded flow: RAG fan-out + context assembly. The orchestrator
386
+ // is constructed PER REQUEST so it reads the LIVE _mainLlm/_helperLlm/
387
+ // _classifier (hot-swap via reconfigure() keeps working — see issue #164).
388
+ const orchestrator = new RagOrchestrator({
389
+ mainLlm: this._mainLlm,
390
+ helperLlm: this._helperLlm,
391
+ classifier: this._classifier,
392
+ config: this.config,
393
+ tracer: this.tracer,
394
+ metrics: this.metrics,
395
+ reranker: this.reranker,
396
+ queryExpander: this.queryExpander,
397
+ sessionManager: this.sessionManager,
398
+ toolAvailabilityRegistry: this.toolAvailabilityRegistry,
399
+ mcpToolRegistry: this.mcpToolRegistry,
400
+ requestLogger: this.requestLogger,
401
+ ragStores: this.deps.ragStores,
402
+ embedder: this.deps.embedder,
403
+ assembler: this.deps.assembler,
404
+ skillManager: this.deps.skillManager,
405
+ translateQueryStores: this.deps.translateQueryStores,
767
406
  });
768
- const assembleResult = await this.deps.assembler.assemble(mainAction, retrieved, processedHistory, opts);
769
- if (!assembleResult.ok) {
770
- assembleSpan.setStatus('error', assembleResult.error.message);
771
- assembleSpan.end();
772
- rootSpan.setStatus('error', assembleResult.error.message);
407
+ const orchResult = await orchestrator.orchestrate(textOrMessages, {
408
+ opts,
409
+ rootSpan,
410
+ sessionId,
411
+ mode,
412
+ externalTools,
413
+ });
414
+ if (!orchResult.ok) {
415
+ rootSpan.setStatus('error', orchResult.error.message);
773
416
  rootSpan.end();
774
- yield {
775
- ok: false,
776
- error: new OrchestratorError(assembleResult.error.message, 'ASSEMBLER_ERROR'),
777
- };
417
+ yield orchResult;
778
418
  return;
779
419
  }
780
- assembleSpan.setStatus('ok');
781
- assembleSpan.end();
782
- // Inject skill content into system message (post-assembly)
783
- if (skillContent) {
784
- const sysMsg = assembleResult.value.find((m) => m.role === 'system');
785
- if (sysMsg) {
786
- sysMsg.content += `\n\n## Active Skills\n${skillContent}`;
787
- }
788
- else {
789
- assembleResult.value.unshift({
790
- role: 'system',
791
- content: `## Active Skills\n${skillContent}`,
792
- });
793
- }
794
- }
795
- opts?.sessionLogger?.logStep(`final_context_assembled`, {
796
- messages: assembleResult.value,
797
- tools: finalTools.map((t) => t.name),
798
- });
420
+ // skillContent is NOT destructured — the caller doesn't use it; it is
421
+ // already baked into assembledMessages inside orchestrate(). Destructuring
422
+ // it unused would trip noUnusedLocals.
423
+ const { retrieved, finalTools, assembledMessages, mainAction, toolClientMap, } = orchResult.value;
799
424
  // 4. Single Streaming Loop
800
- const stream = this._runStreamingToolLoop(mainAction, retrieved, assembleResult.value, toolClientMap, opts, rootSpan, sessionId, externalTools, finalTools, detectedAdapter);
425
+ const stream = this._runStreamingToolLoop(mainAction, retrieved, assembledMessages, toolClientMap, opts, rootSpan, sessionId, externalTools, finalTools, detectedAdapter);
801
426
  for await (const chunk of stream)
802
427
  yield chunk;
803
428
  rootSpan.setStatus('ok');
@@ -809,54 +434,6 @@ export class SmartAgent {
809
434
  this.metrics.requestLatency.record(Date.now() - requestStart);
810
435
  }
811
436
  }
812
- async _preparePipeline(textOrMessages, opts, parentSpan) {
813
- opts?.sessionLogger?.logStep('client_request', { textOrMessages });
814
- const text = typeof textOrMessages === 'string'
815
- ? textOrMessages
816
- : (textOrMessages.filter((m) => m.role === 'user').slice(-1)[0]
817
- ?.content ?? '');
818
- const history = typeof textOrMessages === 'string' ? [] : textOrMessages;
819
- let processedHistory = history;
820
- const summarizeLimit = this.config.historyAutoSummarizeLimit ?? 10;
821
- if (this._helperLlm && history.length > summarizeLimit) {
822
- const res = await this._summarizeHistory(history, opts);
823
- if (res.ok)
824
- processedHistory = res.value;
825
- }
826
- let subprompts;
827
- if (this.config.classificationEnabled === false) {
828
- // Skip classification — treat entire input as a single action
829
- subprompts = [
830
- { type: 'action', text, dependency: 'independent' },
831
- ];
832
- opts?.sessionLogger?.logStep('classification_skipped', { text });
833
- }
834
- else {
835
- const classifySpan = this.tracer.startSpan('smart_agent.classify', {
836
- parent: parentSpan,
837
- });
838
- const classifyResult = await this._classifier.classify(text, opts);
839
- if (!classifyResult.ok) {
840
- classifySpan.setStatus('error', classifyResult.error.message);
841
- classifySpan.end();
842
- return {
843
- ok: false,
844
- error: new OrchestratorError(classifyResult.error.message, 'CLASSIFIER_ERROR'),
845
- };
846
- }
847
- classifySpan.setStatus('ok');
848
- classifySpan.end();
849
- opts?.sessionLogger?.logStep('classifier_response', {
850
- subprompts: classifyResult.value,
851
- });
852
- subprompts = classifyResult.value;
853
- }
854
- for (const sp of subprompts) {
855
- this.metrics.classifierIntentCount.add(1, { intent: sp.type });
856
- }
857
- const { toolClientMap } = await this._listAllTools(opts);
858
- return { ok: true, value: { subprompts, processedHistory, toolClientMap } };
859
- }
860
437
  async *_runStreamingToolLoop(_action, _retrieved, initialMessages, toolClientMap, opts, parentSpan, sessionId, externalTools, activeTools, clientAdapter) {
861
438
  const toolLoopSpan = this.tracer.startSpan('smart_agent.tool_loop', {
862
439
  parent: parentSpan,
@@ -868,36 +445,25 @@ export class SmartAgent {
868
445
  const timingLog = [];
869
446
  const loopStart = Date.now();
870
447
  let currentTools = activeTools;
871
- // Inject tool priority instruction when external tools are present
872
- if (externalTools.length > 0) {
873
- const systemIdx = messages.findIndex((m) => m.role === 'system');
874
- if (systemIdx >= 0) {
875
- const sys = messages[systemIdx];
876
- messages = [...messages];
877
- messages[systemIdx] = {
878
- ...sys,
879
- content: `${sys.content}\n\nIMPORTANT: You have internal tools and client-provided tools (marked [client-provided] in their description). Always prefer internal tools when they can accomplish the task. Use client-provided tools only when no internal tool can do the job.`,
880
- };
881
- }
882
- }
883
- // Inject pending internal tool results from previous mixed-call request
884
- if (this.pendingToolResults.has(sessionId)) {
885
- const pending = await this.pendingToolResults.consume(sessionId);
886
- if (pending) {
887
- messages = [
888
- ...messages,
889
- pending.assistantMessage,
890
- ...pending.results.map((r) => ({
891
- role: 'tool',
892
- content: r.text,
893
- tool_call_id: r.toolCallId,
894
- })),
895
- ];
896
- opts?.sessionLogger?.logStep('pending_tool_results_injected', {
897
- toolNames: pending.results.map((r) => r.toolName),
898
- });
899
- }
448
+ messages = injectToolPriority(messages, externalTools);
449
+ // staticPrefix is immutable across all iterations — re-emitted every round.
450
+ const staticPrefix = messages;
451
+ const strategy = (this.deps.toolLoopContextStrategyFactory ??
452
+ (() => new LegacyAccumulateContextStrategy()))({ run: undefined });
453
+ // controlTail carries validation-reprompt pairs (assistant+user) that are
454
+ // NOT tool rounds. It is pruned after the next recorded tool round.
455
+ const controlTail = [];
456
+ const withPending = await injectPendingResults(messages, this.pendingToolResults, sessionId, opts);
457
+ if (withPending.length > staticPrefix.length) {
458
+ // Injected pending results form an assistant+tool group → record as round.
459
+ const injected = withPending.slice(staticPrefix.length);
460
+ const pendingRound = {
461
+ assistant: injected[0],
462
+ results: injected.slice(1),
463
+ };
464
+ await strategy.record(pendingRound);
900
465
  }
466
+ messages = (await strategy.form({ prefix: staticPrefix, queryText: _action.text })).concat(controlTail);
901
467
  for (let iteration = 0;; iteration++) {
902
468
  let iterationBuffer = '';
903
469
  if (opts?.signal?.aborted) {
@@ -933,7 +499,7 @@ export class SmartAgent {
933
499
  parent: toolLoopSpan,
934
500
  attributes: { 'llm.iteration': iteration + 1 },
935
501
  });
936
- const refreshed = await this._listAllTools(opts);
502
+ const refreshed = await this.mcpToolRegistry.resolve(opts);
937
503
  const prevNames = [...toolClientMap.keys()];
938
504
  toolClientMap.clear();
939
505
  for (const [name, client] of refreshed.toolClientMap) {
@@ -1020,7 +586,7 @@ export class SmartAgent {
1020
586
  .filter((id) => id.startsWith('tool:'))
1021
587
  .map((id) => id.slice(5).replace(/:.*$/, '')));
1022
588
  if (newToolNames.size > 0) {
1023
- const refreshed = await this._listAllTools(opts);
589
+ const refreshed = await this.mcpToolRegistry.resolve(opts);
1024
590
  const newMcpTools = refreshed.tools.filter((t) => newToolNames.has(t.name));
1025
591
  currentTools = [
1026
592
  ...newMcpTools,
@@ -1047,14 +613,7 @@ export class SmartAgent {
1047
613
  reselectSpan.end();
1048
614
  }
1049
615
  }
1050
- const filteredForIteration = this.toolAvailabilityRegistry.filterTools(sessionId, currentTools);
1051
- currentTools = filteredForIteration.allowed;
1052
- if (filteredForIteration.blocked.length > 0) {
1053
- opts?.sessionLogger?.logStep('active_tools_filtered_in_iteration', {
1054
- iteration: iteration + 1,
1055
- blocked: filteredForIteration.blocked,
1056
- });
1057
- }
616
+ currentTools = filterAvailableTools(this.toolAvailabilityRegistry, sessionId, currentTools, iteration, opts);
1058
617
  opts?.sessionLogger?.logStep(`llm_request_iter_${iteration + 1}`, {
1059
618
  messages,
1060
619
  tools: currentTools,
@@ -1189,17 +748,17 @@ export class SmartAgent {
1189
748
  });
1190
749
  if (finishReason !== 'tool_calls' || toolCalls.length === 0) {
1191
750
  // Output validation
1192
- const valResult = await this.outputValidator.validate(content, { messages, tools: currentTools }, opts);
1193
- if (valResult.ok && !valResult.value.valid) {
1194
- const correction = valResult.value.correctedContent ?? valResult.value.reason;
1195
- messages = [
1196
- ...messages,
1197
- { role: 'assistant', content },
1198
- {
1199
- role: 'user',
1200
- content: `Your previous response was rejected by validation: ${correction}. Please try again.`,
1201
- },
1202
- ];
751
+ const val = await runOutputValidationReprompt(this.outputValidator, content, messages, currentTools, opts);
752
+ if (val.reprompt) {
753
+ // Reprompt pairs (assistant+user correction) go into controlTail, NOT
754
+ // into the strategy — they are ephemeral and survive only until the
755
+ // next recorded tool round, at which point controlTail is pruned.
756
+ const appended = val.messages.slice(messages.length);
757
+ controlTail.push(...appended);
758
+ messages = (await strategy.form({
759
+ prefix: staticPrefix,
760
+ queryText: _action.text,
761
+ })).concat(controlTail);
1203
762
  continue;
1204
763
  }
1205
764
  opts?.sessionLogger?.logStep('final_response', { content, usage });
@@ -1242,70 +801,25 @@ export class SmartAgent {
1242
801
  };
1243
802
  return;
1244
803
  }
1245
- const internalCalls = toolCalls.filter((tc) => toolClientMap.has(tc.name));
1246
- const validExternalCalls = toolCalls.filter((tc) => externalToolNames.has(tc.name));
1247
- const blockedToolNames = this.toolAvailabilityRegistry.getBlockedToolNames(sessionId);
1248
- const blockedCalls = toolCalls.filter((tc) => blockedToolNames.has(tc.name));
1249
- const hallucinations = toolCalls.filter((tc) => !blockedToolNames.has(tc.name) &&
1250
- !toolClientMap.has(tc.name) &&
1251
- !externalToolNames.has(tc.name));
804
+ const { internalCalls, validExternalCalls, blockedCalls, hallucinations, } = classifyToolCalls(toolCalls, toolClientMap, externalToolNames, this.toolAvailabilityRegistry, sessionId);
1252
805
  if (blockedCalls.length > 0) {
1253
- messages = [
1254
- ...messages,
1255
- {
1256
- role: 'assistant',
1257
- content: content || null,
1258
- tool_calls: blockedCalls.map((tc) => ({
1259
- id: tc.id,
1260
- type: 'function',
1261
- function: {
1262
- name: tc.name,
1263
- arguments: JSON.stringify(tc.arguments),
1264
- },
1265
- })),
1266
- },
1267
- ];
1268
- for (const blocked of blockedCalls) {
1269
- messages = [
1270
- ...messages,
1271
- {
1272
- role: 'tool',
1273
- content: `Error: Tool "${blocked.name}" is temporarily unavailable in this session.`,
1274
- tool_call_id: blocked.id,
1275
- },
1276
- ];
1277
- }
1278
- opts?.sessionLogger?.logStep('blocked_tool_calls_intercepted', {
1279
- toolNames: blockedCalls.map((tc) => tc.name),
806
+ const group = buildBlockedToolMessages(content, blockedCalls, opts);
807
+ await strategy.record({
808
+ assistant: group.assistant,
809
+ results: group.results,
1280
810
  });
811
+ controlTail.length = 0;
812
+ messages = (await strategy.form({ prefix: staticPrefix, queryText: _action.text })).concat(controlTail);
1281
813
  continue;
1282
814
  }
1283
815
  if (hallucinations.length > 0) {
1284
- messages = [
1285
- ...messages,
1286
- {
1287
- role: 'assistant',
1288
- content: content || null,
1289
- tool_calls: toolCalls.map((tc) => ({
1290
- id: tc.id,
1291
- type: 'function',
1292
- function: {
1293
- name: tc.name,
1294
- arguments: JSON.stringify(tc.arguments),
1295
- },
1296
- })),
1297
- },
1298
- ];
1299
- for (const h of hallucinations) {
1300
- messages = [
1301
- ...messages,
1302
- {
1303
- role: 'tool',
1304
- content: `Error: Tool "${h.name}" not found.`,
1305
- tool_call_id: h.id,
1306
- },
1307
- ];
1308
- }
816
+ const group = buildHallucinatedToolMessages(content, toolCalls, hallucinations);
817
+ await strategy.record({
818
+ assistant: group.assistant,
819
+ results: group.results,
820
+ });
821
+ controlTail.length = 0;
822
+ messages = (await strategy.form({ prefix: staticPrefix, queryText: _action.text })).concat(controlTail);
1309
823
  continue;
1310
824
  }
1311
825
  if (validExternalCalls.length > 0) {
@@ -1342,22 +856,21 @@ export class SmartAgent {
1342
856
  }
1343
857
  return;
1344
858
  }
1345
- if (content || internalCalls.length > 0)
1346
- messages = [
1347
- ...messages,
1348
- {
1349
- role: 'assistant',
1350
- content: content || null,
1351
- tool_calls: internalCalls.map((tc) => ({
1352
- id: tc.id,
1353
- type: 'function',
1354
- function: {
1355
- name: tc.name,
1356
- arguments: JSON.stringify(tc.arguments),
1357
- },
1358
- })),
859
+ // Capture the assistant message now; record the full round (assistant +
860
+ // tool results) atomically after the batch completes so the strategy
861
+ // always sees a complete ToolRound.
862
+ const batchAssistantMsg = {
863
+ role: 'assistant',
864
+ content: content || null,
865
+ tool_calls: internalCalls.map((tc) => ({
866
+ id: tc.id,
867
+ type: 'function',
868
+ function: {
869
+ name: tc.name,
870
+ arguments: JSON.stringify(tc.arguments),
1359
871
  },
1360
- ];
872
+ })),
873
+ };
1361
874
  // Truncate batch to remaining budget
1362
875
  const remaining = this.config.maxToolCalls !== undefined
1363
876
  ? this.config.maxToolCalls - toolCallCount
@@ -1395,229 +908,53 @@ export class SmartAgent {
1395
908
  },
1396
909
  };
1397
910
  }
1398
- const toolExecPromises = batch.map(async (tc) => {
1399
- const toolStart = Date.now();
1400
- opts?.sessionLogger?.logStep(`mcp_call_${tc.name}`, {
1401
- arguments: tc.arguments,
1402
- });
1403
- const client = toolClientMap.get(tc.name);
1404
- if (!client)
1405
- return { tc, text: '', res: null, duration: 0 };
1406
- const toolSpan = this.tracer.startSpan('smart_agent.tool_call', {
1407
- parent: toolLoopSpan,
1408
- attributes: { 'tool.name': tc.name },
1409
- });
1410
- const cached = this.toolCache.get(tc.name, tc.arguments);
1411
- const res = cached
1412
- ? (() => {
1413
- this.metrics.toolCacheHitCount.add();
1414
- toolSpan.setAttribute('cache', 'hit');
1415
- return { ok: true, value: cached };
1416
- })()
1417
- : await (async () => {
1418
- const r = await client.callTool(tc.name, tc.arguments, opts);
1419
- if (r.ok)
1420
- this.toolCache.set(tc.name, tc.arguments, r.value);
1421
- return r;
1422
- })();
1423
- const text = !res.ok
1424
- ? res.error.message
1425
- : typeof res.value.content === 'string'
1426
- ? res.value.content
1427
- : JSON.stringify(res.value.content);
1428
- toolSpan.setStatus(res.ok ? 'ok' : 'error', res.ok ? undefined : text);
1429
- toolSpan.end();
1430
- return { tc, text, res, duration: Date.now() - toolStart };
1431
- });
1432
- // Race: tool execution vs periodic heartbeat
1433
- const allDone = Promise.all(toolExecPromises);
1434
- const pendingTools = new Set(batch.map((tc) => tc.name));
1435
- const toolStartTime = Date.now();
1436
- let results = [];
1437
- let settled = false;
1438
- // Mark individual tools as done when they resolve
1439
- for (const [i, p] of toolExecPromises.entries()) {
1440
- p.then(() => pendingTools.delete(batch[i].name));
1441
- }
1442
- while (!settled) {
1443
- const winner = await Promise.race([
1444
- allDone.then((r) => ({ tag: 'done', results: r })),
1445
- new Promise((resolve) => setTimeout(() => resolve({ tag: 'tick' }), heartbeatMs)),
1446
- ]);
1447
- if (winner.tag === 'done') {
1448
- results = winner.results;
1449
- settled = true;
1450
- }
1451
- else {
1452
- // Yield heartbeat for each still-pending tool
1453
- for (const tool of pendingTools) {
1454
- yield {
1455
- ok: true,
1456
- value: {
1457
- content: '',
1458
- heartbeat: {
1459
- tool,
1460
- elapsed: Date.now() - toolStartTime,
1461
- },
1462
- },
1463
- };
1464
- }
1465
- }
1466
- }
1467
- // Collect per-tool timing into the shared timing log
1468
- for (const r of results) {
1469
- timingLog.push({
1470
- phase: `tool_${r.tc.name}`,
1471
- duration: r.duration,
1472
- });
1473
- }
1474
- // Process results: update availability, metrics, messages
1475
- const toolMessages = [];
1476
- for (const { tc, text, res } of results) {
1477
- if (!res)
1478
- continue;
1479
- if (!res.ok &&
1480
- isToolContextUnavailableError(text) &&
1481
- !externalToolNames.has(tc.name)) {
1482
- const entry = this.toolAvailabilityRegistry.block(sessionId, tc.name, text);
1483
- currentTools = currentTools.filter((t) => t.name !== tc.name);
1484
- opts?.sessionLogger?.logStep(`tool_blacklisted_${tc.name}`, {
1485
- reason: text,
1486
- blockedUntil: entry.blockedUntil,
911
+ // Execute all tool calls concurrently with heartbeat (shared core).
912
+ const outcome = yield* executeToolBatchWithHeartbeat({
913
+ batch,
914
+ toolClientMap,
915
+ toolCache: this.toolCache,
916
+ tracer: this.tracer,
917
+ metrics: this.metrics,
918
+ parentSpan: toolLoopSpan,
919
+ toolAvailabilityRegistry: this.toolAvailabilityRegistry,
920
+ sessionId,
921
+ externalToolNames,
922
+ currentTools,
923
+ toolCallCount,
924
+ timingLog,
925
+ heartbeatMs,
926
+ options: opts,
927
+ mcpFailureClassifier: this.deps.mcpFailureClassifier,
928
+ onToolExecuted: (r) => {
929
+ const traceId = opts?.trace?.traceId ?? 'agent-tool-loop';
930
+ const isError = !r.res?.ok || (r.res.ok && !!r.res.value.isError);
931
+ this.deps.logger?.log({
932
+ type: 'tool_call',
933
+ traceId,
934
+ toolName: r.tc.name,
935
+ isError,
936
+ durationMs: r.duration,
937
+ });
938
+ opts?.sessionLogger?.logStep('mcp_tool_call', {
939
+ toolName: r.tc.name,
940
+ durationMs: r.duration,
941
+ isError,
1487
942
  });
1488
- }
1489
- opts?.sessionLogger?.logStep(`mcp_result_${tc.name}`, {
1490
- result: text,
1491
- });
1492
- toolCallCount++;
1493
- this.metrics.toolCallCount.add();
1494
- toolMessages.push({
1495
- role: 'tool',
1496
- content: text,
1497
- tool_call_id: tc.id,
1498
- });
1499
- }
1500
- messages = [...messages, ...toolMessages];
1501
- }
1502
- }
1503
- async _listAllTools(opts) {
1504
- await this._resolveActiveClients(opts);
1505
- const tools = [];
1506
- const toolClientMap = new Map();
1507
- const settled = await Promise.allSettled(this._activeClients.map(async (client) => ({
1508
- client,
1509
- result: await client.listTools(opts),
1510
- })));
1511
- for (const e of settled) {
1512
- if (e.status === 'fulfilled' && e.value.result.ok) {
1513
- for (const t of e.value.result.value) {
1514
- if (!toolClientMap.has(t.name)) {
1515
- tools.push(t);
1516
- toolClientMap.set(t.name, e.value.client);
1517
- }
1518
- }
1519
- }
1520
- }
1521
- return { tools, toolClientMap };
1522
- }
1523
- async _toEnglishForRag(text, opts) {
1524
- if (/^[\p{ASCII}]+$/u.test(text) || text.length < 15)
1525
- return text;
1526
- const dp = 'Translate the user request to English for search purposes. Preserve technical terms if present. Reply with only the expanded English terms, no explanation.';
1527
- const llm = this._helperLlm || this._mainLlm;
1528
- const res = await llm.chat([
1529
- {
1530
- role: 'system',
1531
- content: this.config.ragTranslatePrompt || dp,
1532
- },
1533
- { role: 'user', content: text },
1534
- ], [], opts);
1535
- return res.ok && res.value.content.trim() ? res.value.content.trim() : text;
1536
- }
1537
- async _summarizeHistory(h, opts) {
1538
- if (!this._helperLlm)
1539
- return { ok: true, value: h };
1540
- const toS = h.slice(0, -5);
1541
- const rec = h.slice(-5);
1542
- if (toS.length === 0)
1543
- return { ok: true, value: h };
1544
- const dp = 'Summarize the conversation so far in 2-3 sentences. Focus on the user goals and the current status of the task. Keep technical SAP terms as is.';
1545
- const summarizeStart = Date.now();
1546
- const res = await this._helperLlm.chat([
1547
- ...toS,
1548
- {
1549
- role: 'system',
1550
- content: this.config.historySummaryPrompt || dp,
1551
- },
1552
- ], [], opts);
1553
- this.requestLogger.logLlmCall({
1554
- component: 'helper',
1555
- model: this._helperLlm.model ?? 'unknown',
1556
- promptTokens: res.ok ? (res.value.usage?.promptTokens ?? 0) : 0,
1557
- completionTokens: res.ok ? (res.value.usage?.completionTokens ?? 0) : 0,
1558
- totalTokens: res.ok ? (res.value.usage?.totalTokens ?? 0) : 0,
1559
- durationMs: Date.now() - summarizeStart,
1560
- requestId: opts?.trace?.traceId,
1561
- });
1562
- if (!res.ok)
1563
- return { ok: true, value: h };
1564
- return {
1565
- ok: true,
1566
- value: [
1567
- {
1568
- role: 'system',
1569
- content: `Summary of previous conversation: ${res.value.content}`,
1570
943
  },
1571
- ...rec,
1572
- ],
1573
- };
1574
- }
1575
- async *_runStructuredPipeline(textOrMessages, externalTools, opts, _parentSpan, _sessionId) {
1576
- if (!this.deps.pipeline)
1577
- return;
1578
- const history = typeof textOrMessages === 'string' ? [] : textOrMessages;
1579
- const chunkQueue = [];
1580
- let resolveWait = null;
1581
- let done = false;
1582
- const executorPromise = this.deps.pipeline
1583
- .execute(textOrMessages, history, opts, (chunk) => {
1584
- chunkQueue.push(chunk);
1585
- if (resolveWait) {
1586
- resolveWait();
1587
- resolveWait = null;
1588
- }
1589
- }, externalTools)
1590
- .then(() => {
1591
- done = true;
1592
- if (resolveWait) {
1593
- resolveWait();
1594
- resolveWait = null;
1595
- }
1596
- })
1597
- .catch((err) => {
1598
- chunkQueue.push({
1599
- ok: false,
1600
- error: new OrchestratorError(String(err), 'PIPELINE_ERROR'),
1601
944
  });
1602
- done = true;
1603
- if (resolveWait) {
1604
- resolveWait();
1605
- resolveWait = null;
1606
- }
1607
- });
1608
- while (!done || chunkQueue.length > 0) {
1609
- if (chunkQueue.length > 0) {
1610
- const chunk = chunkQueue.shift();
1611
- if (chunk !== undefined)
1612
- yield chunk;
1613
- }
1614
- else if (!done) {
1615
- await new Promise((r) => {
1616
- resolveWait = r;
1617
- });
1618
- }
945
+ if (outcome.escalated)
946
+ return;
947
+ currentTools = outcome.currentTools;
948
+ toolCallCount = outcome.toolCallCount;
949
+ const batchRound = {
950
+ assistant: batchAssistantMsg,
951
+ results: outcome.toolMessages,
952
+ meta: outcome.resultMeta,
953
+ };
954
+ await strategy.record(batchRound);
955
+ controlTail.length = 0;
956
+ messages = (await strategy.form({ prefix: staticPrefix, queryText: _action.text })).concat(controlTail);
1619
957
  }
1620
- await executorPromise;
1621
958
  }
1622
959
  }
1623
960
  //# sourceMappingURL=agent.js.map