@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.
- package/dist/adapters/llm-adapter.d.ts.map +1 -1
- package/dist/adapters/llm-adapter.js +16 -7
- package/dist/adapters/llm-adapter.js.map +1 -1
- package/dist/agent/rag-helpers.d.ts +20 -0
- package/dist/agent/rag-helpers.d.ts.map +1 -0
- package/dist/agent/rag-helpers.js +61 -0
- package/dist/agent/rag-helpers.js.map +1 -0
- package/dist/agent/rag-orchestrator-types.d.ts +53 -0
- package/dist/agent/rag-orchestrator-types.d.ts.map +1 -0
- package/dist/agent/rag-orchestrator-types.js +2 -0
- package/dist/agent/rag-orchestrator-types.js.map +1 -0
- package/dist/agent/rag-orchestrator.d.ts +14 -0
- package/dist/agent/rag-orchestrator.d.ts.map +1 -0
- package/dist/agent/rag-orchestrator.js +352 -0
- package/dist/agent/rag-orchestrator.js.map +1 -0
- package/dist/agent.d.ts +11 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +111 -818
- package/dist/agent.js.map +1 -1
- package/dist/builder-types.d.ts +64 -0
- package/dist/builder-types.d.ts.map +1 -0
- package/dist/builder-types.js +18 -0
- package/dist/builder-types.js.map +1 -0
- package/dist/builder.d.ts +23 -51
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +69 -224
- package/dist/builder.js.map +1 -1
- package/dist/coordinator/planning/one-shot.d.ts.map +1 -1
- package/dist/coordinator/planning/one-shot.js +13 -2
- package/dist/coordinator/planning/one-shot.js.map +1 -1
- package/dist/health/agent-health.d.ts +13 -0
- package/dist/health/agent-health.d.ts.map +1 -0
- package/dist/health/agent-health.js +82 -0
- package/dist/health/agent-health.js.map +1 -0
- package/dist/health/health-checker.d.ts.map +1 -1
- package/dist/health/health-checker.js +4 -4
- package/dist/health/health-checker.js.map +1 -1
- package/dist/interfaces/pipeline.d.ts +3 -1
- package/dist/interfaces/pipeline.d.ts.map +1 -1
- package/dist/mcp/tool-registry.d.ts +22 -0
- package/dist/mcp/tool-registry.d.ts.map +1 -0
- package/dist/mcp/tool-registry.js +57 -0
- package/dist/mcp/tool-registry.js.map +1 -0
- package/dist/mcp/vectorize-mcp-tools.d.ts +11 -0
- package/dist/mcp/vectorize-mcp-tools.d.ts.map +1 -0
- package/dist/mcp/vectorize-mcp-tools.js +191 -0
- package/dist/mcp/vectorize-mcp-tools.js.map +1 -0
- package/dist/pipeline/context.d.ts +3 -1
- package/dist/pipeline/context.d.ts.map +1 -1
- package/dist/pipeline/default-pipeline.d.ts.map +1 -1
- package/dist/pipeline/default-pipeline.js +1 -0
- package/dist/pipeline/default-pipeline.js.map +1 -1
- package/dist/pipeline/handlers/escalate-if-unavailable.d.ts +33 -0
- package/dist/pipeline/handlers/escalate-if-unavailable.d.ts.map +1 -0
- package/dist/pipeline/handlers/escalate-if-unavailable.js +22 -0
- package/dist/pipeline/handlers/escalate-if-unavailable.js.map +1 -0
- package/dist/pipeline/handlers/pass-through.d.ts +3 -0
- package/dist/pipeline/handlers/pass-through.d.ts.map +1 -0
- package/dist/pipeline/handlers/pass-through.js +77 -0
- package/dist/pipeline/handlers/pass-through.js.map +1 -0
- package/dist/pipeline/handlers/tool-loop-core.d.ts +92 -0
- package/dist/pipeline/handlers/tool-loop-core.d.ts.map +1 -0
- package/dist/pipeline/handlers/tool-loop-core.js +269 -0
- package/dist/pipeline/handlers/tool-loop-core.js.map +1 -0
- package/dist/pipeline/handlers/tool-loop.d.ts.map +1 -1
- package/dist/pipeline/handlers/tool-loop.js +67 -216
- package/dist/pipeline/handlers/tool-loop.js.map +1 -1
- package/dist/pipeline/pipeline-to-stream.d.ts +5 -0
- package/dist/pipeline/pipeline-to-stream.d.ts.map +1 -0
- package/dist/pipeline/pipeline-to-stream.js +49 -0
- package/dist/pipeline/pipeline-to-stream.js.map +1 -0
- package/dist/skills/plugin-host/github-transport.d.ts +35 -0
- package/dist/skills/plugin-host/github-transport.d.ts.map +1 -0
- package/dist/skills/plugin-host/github-transport.js +124 -0
- package/dist/skills/plugin-host/github-transport.js.map +1 -0
- package/dist/skills/plugin-host/index.d.ts +1 -0
- package/dist/skills/plugin-host/index.d.ts.map +1 -1
- package/dist/skills/plugin-host/index.js +1 -0
- package/dist/skills/plugin-host/index.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
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
|
|
440
|
-
|
|
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.
|
|
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
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
//
|
|
783
|
-
|
|
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,
|
|
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
|
-
|
|
872
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1193
|
-
if (
|
|
1194
|
-
|
|
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
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
:
|
|
1428
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|