@mcp-abap-adt/llm-agent-libs 20.0.0 → 20.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) 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 +11 -9
  17. package/dist/agent.d.ts.map +1 -1
  18. package/dist/agent.js +111 -818
  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 +23 -51
  25. package/dist/builder.d.ts.map +1 -1
  26. package/dist/builder.js +69 -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/interfaces/pipeline.d.ts +3 -1
  39. package/dist/interfaces/pipeline.d.ts.map +1 -1
  40. package/dist/mcp/tool-registry.d.ts +22 -0
  41. package/dist/mcp/tool-registry.d.ts.map +1 -0
  42. package/dist/mcp/tool-registry.js +57 -0
  43. package/dist/mcp/tool-registry.js.map +1 -0
  44. package/dist/mcp/vectorize-mcp-tools.d.ts +11 -0
  45. package/dist/mcp/vectorize-mcp-tools.d.ts.map +1 -0
  46. package/dist/mcp/vectorize-mcp-tools.js +191 -0
  47. package/dist/mcp/vectorize-mcp-tools.js.map +1 -0
  48. package/dist/pipeline/context.d.ts +3 -1
  49. package/dist/pipeline/context.d.ts.map +1 -1
  50. package/dist/pipeline/default-pipeline.d.ts.map +1 -1
  51. package/dist/pipeline/default-pipeline.js +1 -0
  52. package/dist/pipeline/default-pipeline.js.map +1 -1
  53. package/dist/pipeline/handlers/escalate-if-unavailable.d.ts +33 -0
  54. package/dist/pipeline/handlers/escalate-if-unavailable.d.ts.map +1 -0
  55. package/dist/pipeline/handlers/escalate-if-unavailable.js +22 -0
  56. package/dist/pipeline/handlers/escalate-if-unavailable.js.map +1 -0
  57. package/dist/pipeline/handlers/pass-through.d.ts +3 -0
  58. package/dist/pipeline/handlers/pass-through.d.ts.map +1 -0
  59. package/dist/pipeline/handlers/pass-through.js +77 -0
  60. package/dist/pipeline/handlers/pass-through.js.map +1 -0
  61. package/dist/pipeline/handlers/tool-loop-core.d.ts +92 -0
  62. package/dist/pipeline/handlers/tool-loop-core.d.ts.map +1 -0
  63. package/dist/pipeline/handlers/tool-loop-core.js +269 -0
  64. package/dist/pipeline/handlers/tool-loop-core.js.map +1 -0
  65. package/dist/pipeline/handlers/tool-loop.d.ts.map +1 -1
  66. package/dist/pipeline/handlers/tool-loop.js +67 -216
  67. package/dist/pipeline/handlers/tool-loop.js.map +1 -1
  68. package/dist/pipeline/pipeline-to-stream.d.ts +5 -0
  69. package/dist/pipeline/pipeline-to-stream.d.ts.map +1 -0
  70. package/dist/pipeline/pipeline-to-stream.js +49 -0
  71. package/dist/pipeline/pipeline-to-stream.js.map +1 -0
  72. package/dist/skills/plugin-host/github-transport.d.ts +35 -0
  73. package/dist/skills/plugin-host/github-transport.d.ts.map +1 -0
  74. package/dist/skills/plugin-host/github-transport.js +124 -0
  75. package/dist/skills/plugin-host/github-transport.js.map +1 -0
  76. package/dist/skills/plugin-host/index.d.ts +1 -0
  77. package/dist/skills/plugin-host/index.d.ts.map +1 -1
  78. package/dist/skills/plugin-host/index.js +1 -0
  79. package/dist/skills/plugin-host/index.js.map +1 -1
  80. package/package.json +14 -14
package/dist/agent.js CHANGED
@@ -1,15 +1,21 @@
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 { runPassThrough } from './pipeline/handlers/pass-through.js';
14
+ import { buildBlockedToolMessages, buildHallucinatedToolMessages, classifyToolCalls, executeToolBatchWithHeartbeat, filterAvailableTools, injectPendingResults, injectToolPriority, runOutputValidationReprompt, } from './pipeline/handlers/tool-loop-core.js';
15
+ import { pipelineToStream } from './pipeline/pipeline-to-stream.js';
10
16
  import { fireInternalToolsAsync } from './policy/mixed-tool-call-handler.js';
11
17
  import { PendingToolResultsRegistry } from './policy/pending-tool-results-registry.js';
12
- import { isToolContextUnavailableError, ToolAvailabilityRegistry, } from './policy/tool-availability-registry.js';
18
+ import { ToolAvailabilityRegistry } from './policy/tool-availability-registry.js';
13
19
  import { NoopReranker } from './reranker/noop-reranker.js';
14
20
  import { NoopSessionManager } from './session/noop-session-manager.js';
15
21
  import { NoopTracer } from './tracer/noop-tracer.js';
@@ -46,7 +52,7 @@ export class SmartAgent {
46
52
  pendingToolResults;
47
53
  requestLogger;
48
54
  defaultLlmCallStrategy;
49
- _activeClients;
55
+ mcpToolRegistry;
50
56
  _mainLlm;
51
57
  _classifierLlm;
52
58
  _helperLlm;
@@ -70,7 +76,7 @@ export class SmartAgent {
70
76
  deps.embedder = wrapEmbedder(deps.embedder);
71
77
  this.defaultLlmCallStrategy =
72
78
  deps.llmCallStrategy ?? new StreamingLlmCallStrategy();
73
- this._activeClients = [...deps.mcpClients];
79
+ this.mcpToolRegistry = new McpToolRegistry(deps.mcpClients, deps.connectionStrategy, deps.ragStores);
74
80
  this._mainLlm = deps.mainLlm;
75
81
  this._helperLlm = deps.helperLlm;
76
82
  this._classifier = deps.classifier;
@@ -80,29 +86,6 @@ export class SmartAgent {
80
86
  get currentMainLlm() {
81
87
  return this._mainLlm;
82
88
  }
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
89
  /** Apply a partial config update at runtime (hot-reload). */
107
90
  applyConfigUpdate(update) {
108
91
  this.config = { ...this.config, ...update };
@@ -235,6 +218,16 @@ export class SmartAgent {
235
218
  classificationEnabled: this.config.classificationEnabled,
236
219
  };
237
220
  }
221
+ /**
222
+ * Readiness (implements `IReadinessReporter`): delegate to the MCP connection
223
+ * strategy when it reports readiness, else `true` (no strategy / non-reporting ⇒
224
+ * readiness unknown → ready). Consumers (e.g. a server's `/health` + request
225
+ * gate) detect this via `isReadinessReporter(agent)` — no growth of `ISmartAgent`.
226
+ */
227
+ isReady() {
228
+ const strategy = this.deps.connectionStrategy;
229
+ return isReadinessReporter(strategy) ? strategy.isReady() : true;
230
+ }
238
231
  async healthCheck(options) {
239
232
  const HEALTH_TIMEOUT_MS = this.config.healthTimeoutMs ?? 5_000;
240
233
  const { signal: timeoutSignal, clear: clearTimeout_ } = createTimeoutSignal(HEALTH_TIMEOUT_MS);
@@ -244,77 +237,13 @@ export class SmartAgent {
244
237
  signal: merged.signal,
245
238
  maxTokens: 1,
246
239
  };
247
- const results = {
248
- llm: false,
249
- rag: false,
250
- mcp: [],
251
- };
252
240
  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
- 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;
241
+ const snapshot = await buildAgentHealthSnapshot(this._mainLlm, this.deps.ragStores, this.mcpToolRegistry.getActiveClients(), healthOptions);
242
+ return { ok: true, value: snapshot };
275
243
  }
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;
312
- }
313
- catch {
314
- // AbortSignal timeout — leave mcp as empty
244
+ finally {
245
+ clearTimeout_();
315
246
  }
316
- clearTimeout_();
317
- return { ok: true, value: results };
318
247
  }
319
248
  async process(textOrMessages, options) {
320
249
  let content = '';
@@ -432,372 +361,67 @@ export class SmartAgent {
432
361
  this.requestLogger.startRequest(traceId);
433
362
  try {
434
363
  if (mode === 'pass') {
435
- const messages = typeof textOrMessages === 'string'
364
+ const passMessages = typeof textOrMessages === 'string'
436
365
  ? [{ role: 'user', content: textOrMessages }]
437
366
  : textOrMessages;
438
367
  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 };
368
+ for await (const chunk of runPassThrough(this._mainLlm, this.requestLogger, passMessages, externalTools, opts)) {
369
+ yield chunk;
492
370
  }
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
371
  rootSpan.setStatus('ok');
517
372
  rootSpan.end();
518
373
  return;
519
374
  }
520
375
  // Pipeline path (when configured via Builder)
521
376
  if (this.deps.pipeline) {
522
- const stream = this._runStructuredPipeline(textOrMessages, externalTools, opts, rootSpan, sessionId);
377
+ const stream = pipelineToStream(this.deps.pipeline, textOrMessages, externalTools, opts);
523
378
  for await (const chunk of stream)
524
379
  yield chunk;
525
380
  rootSpan.setStatus('ok');
526
381
  rootSpan.end();
527
382
  return;
528
383
  }
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,
384
+ // Default hardcoded flow: RAG fan-out + context assembly. The orchestrator
385
+ // is constructed PER REQUEST so it reads the LIVE _mainLlm/_helperLlm/
386
+ // _classifier (hot-swap via reconfigure() keeps working — see issue #164).
387
+ const orchestrator = new RagOrchestrator({
388
+ mainLlm: this._mainLlm,
389
+ helperLlm: this._helperLlm,
390
+ classifier: this._classifier,
391
+ config: this.config,
392
+ tracer: this.tracer,
393
+ metrics: this.metrics,
394
+ reranker: this.reranker,
395
+ queryExpander: this.queryExpander,
396
+ sessionManager: this.sessionManager,
397
+ toolAvailabilityRegistry: this.toolAvailabilityRegistry,
398
+ mcpToolRegistry: this.mcpToolRegistry,
399
+ requestLogger: this.requestLogger,
400
+ ragStores: this.deps.ragStores,
401
+ embedder: this.deps.embedder,
402
+ assembler: this.deps.assembler,
403
+ skillManager: this.deps.skillManager,
404
+ translateQueryStores: this.deps.translateQueryStores,
405
+ });
406
+ const orchResult = await orchestrator.orchestrate(textOrMessages, {
407
+ opts,
408
+ rootSpan,
409
+ sessionId,
410
+ mode,
411
+ externalTools,
767
412
  });
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);
413
+ if (!orchResult.ok) {
414
+ rootSpan.setStatus('error', orchResult.error.message);
773
415
  rootSpan.end();
774
- yield {
775
- ok: false,
776
- error: new OrchestratorError(assembleResult.error.message, 'ASSEMBLER_ERROR'),
777
- };
416
+ yield orchResult;
778
417
  return;
779
418
  }
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
- });
419
+ // skillContent is NOT destructured — the caller doesn't use it; it is
420
+ // already baked into assembledMessages inside orchestrate(). Destructuring
421
+ // it unused would trip noUnusedLocals.
422
+ const { retrieved, finalTools, assembledMessages, mainAction, toolClientMap, } = orchResult.value;
799
423
  // 4. Single Streaming Loop
800
- const stream = this._runStreamingToolLoop(mainAction, retrieved, assembleResult.value, toolClientMap, opts, rootSpan, sessionId, externalTools, finalTools, detectedAdapter);
424
+ const stream = this._runStreamingToolLoop(mainAction, retrieved, assembledMessages, toolClientMap, opts, rootSpan, sessionId, externalTools, finalTools, detectedAdapter);
801
425
  for await (const chunk of stream)
802
426
  yield chunk;
803
427
  rootSpan.setStatus('ok');
@@ -809,54 +433,6 @@ export class SmartAgent {
809
433
  this.metrics.requestLatency.record(Date.now() - requestStart);
810
434
  }
811
435
  }
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
436
  async *_runStreamingToolLoop(_action, _retrieved, initialMessages, toolClientMap, opts, parentSpan, sessionId, externalTools, activeTools, clientAdapter) {
861
437
  const toolLoopSpan = this.tracer.startSpan('smart_agent.tool_loop', {
862
438
  parent: parentSpan,
@@ -868,36 +444,8 @@ export class SmartAgent {
868
444
  const timingLog = [];
869
445
  const loopStart = Date.now();
870
446
  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
- }
900
- }
447
+ messages = injectToolPriority(messages, externalTools);
448
+ messages = await injectPendingResults(messages, this.pendingToolResults, sessionId, opts);
901
449
  for (let iteration = 0;; iteration++) {
902
450
  let iterationBuffer = '';
903
451
  if (opts?.signal?.aborted) {
@@ -933,7 +481,7 @@ export class SmartAgent {
933
481
  parent: toolLoopSpan,
934
482
  attributes: { 'llm.iteration': iteration + 1 },
935
483
  });
936
- const refreshed = await this._listAllTools(opts);
484
+ const refreshed = await this.mcpToolRegistry.resolve(opts);
937
485
  const prevNames = [...toolClientMap.keys()];
938
486
  toolClientMap.clear();
939
487
  for (const [name, client] of refreshed.toolClientMap) {
@@ -1020,7 +568,7 @@ export class SmartAgent {
1020
568
  .filter((id) => id.startsWith('tool:'))
1021
569
  .map((id) => id.slice(5).replace(/:.*$/, '')));
1022
570
  if (newToolNames.size > 0) {
1023
- const refreshed = await this._listAllTools(opts);
571
+ const refreshed = await this.mcpToolRegistry.resolve(opts);
1024
572
  const newMcpTools = refreshed.tools.filter((t) => newToolNames.has(t.name));
1025
573
  currentTools = [
1026
574
  ...newMcpTools,
@@ -1047,14 +595,7 @@ export class SmartAgent {
1047
595
  reselectSpan.end();
1048
596
  }
1049
597
  }
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
- }
598
+ currentTools = filterAvailableTools(this.toolAvailabilityRegistry, sessionId, currentTools, iteration, opts);
1058
599
  opts?.sessionLogger?.logStep(`llm_request_iter_${iteration + 1}`, {
1059
600
  messages,
1060
601
  tools: currentTools,
@@ -1189,17 +730,9 @@ export class SmartAgent {
1189
730
  });
1190
731
  if (finishReason !== 'tool_calls' || toolCalls.length === 0) {
1191
732
  // 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
- ];
733
+ const val = await runOutputValidationReprompt(this.outputValidator, content, messages, currentTools, opts);
734
+ if (val.reprompt) {
735
+ messages = val.messages;
1203
736
  continue;
1204
737
  }
1205
738
  opts?.sessionLogger?.logStep('final_response', { content, usage });
@@ -1242,70 +775,13 @@ export class SmartAgent {
1242
775
  };
1243
776
  return;
1244
777
  }
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));
778
+ const { internalCalls, validExternalCalls, blockedCalls, hallucinations, } = classifyToolCalls(toolCalls, toolClientMap, externalToolNames, this.toolAvailabilityRegistry, sessionId);
1252
779
  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),
1280
- });
780
+ messages = buildBlockedToolMessages(messages, content, blockedCalls, opts);
1281
781
  continue;
1282
782
  }
1283
783
  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
- }
784
+ messages = buildHallucinatedToolMessages(messages, content, toolCalls, hallucinations);
1309
785
  continue;
1310
786
  }
1311
787
  if (validExternalCalls.length > 0) {
@@ -1395,229 +871,46 @@ export class SmartAgent {
1395
871
  },
1396
872
  };
1397
873
  }
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,
874
+ // Execute all tool calls concurrently with heartbeat (shared core).
875
+ const outcome = yield* executeToolBatchWithHeartbeat({
876
+ batch,
877
+ toolClientMap,
878
+ toolCache: this.toolCache,
879
+ tracer: this.tracer,
880
+ metrics: this.metrics,
881
+ parentSpan: toolLoopSpan,
882
+ toolAvailabilityRegistry: this.toolAvailabilityRegistry,
883
+ sessionId,
884
+ externalToolNames,
885
+ currentTools,
886
+ toolCallCount,
887
+ timingLog,
888
+ heartbeatMs,
889
+ options: opts,
890
+ mcpFailureClassifier: this.deps.mcpFailureClassifier,
891
+ onToolExecuted: (r) => {
892
+ const traceId = opts?.trace?.traceId ?? 'agent-tool-loop';
893
+ const isError = !r.res?.ok || (r.res.ok && !!r.res.value.isError);
894
+ this.deps.logger?.log({
895
+ type: 'tool_call',
896
+ traceId,
897
+ toolName: r.tc.name,
898
+ isError,
899
+ durationMs: r.duration,
900
+ });
901
+ opts?.sessionLogger?.logStep('mcp_tool_call', {
902
+ toolName: r.tc.name,
903
+ durationMs: r.duration,
904
+ isError,
1487
905
  });
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
906
  },
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
907
  });
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
- }
908
+ if (outcome.escalated)
909
+ return;
910
+ currentTools = outcome.currentTools;
911
+ toolCallCount = outcome.toolCallCount;
912
+ messages = [...messages, ...outcome.toolMessages];
1619
913
  }
1620
- await executorPromise;
1621
914
  }
1622
915
  }
1623
916
  //# sourceMappingURL=agent.js.map