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