@gt-fe/eap-runtime 0.0.34 → 0.0.36
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/api/handlers/stream.handler.js +11 -6
- package/dist/graph/adapters/langgraph.adapter.js +75 -25
- package/dist/graph/adapters/subgraph-compiler.js +25 -0
- package/dist/graph/contracts/executable-graph.d.ts +9 -0
- package/dist/graph/nodes/llm-node.js +3 -1
- package/dist/graph/nodes/tool-node.d.ts +4 -0
- package/dist/graph/nodes/tool-node.js +132 -4
- package/dist/registry/graph-loader.js +10 -1
- package/dist/runtime/agent-runtime.js +32 -13
- package/dist/tool/index.d.ts +1 -0
- package/dist/tool/index.js +1 -0
- package/dist/tool/mcp-tool-exposure.d.ts +113 -0
- package/dist/tool/mcp-tool-exposure.js +479 -0
- package/dist/tool/system-prompt-builder.d.ts +10 -3
- package/dist/tool/system-prompt-builder.js +8 -4
- package/package.json +2 -2
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, applyStreamReasoningEvent, applyStreamTextDelta, createStreamAssistantBuckets, createStreamReasoningState, resolvedStreamReasoning, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, appendTerminalToolResult, } from '../../session/persist-turn-messages.js';
|
|
23
23
|
import { collectPublicArtifactLinks, collectPublicArtifactLinksFromEvent, injectPublicArtifactLinks, mergeArtifactLinks, } from '../../tool/inject-public-artifact-links.js';
|
|
24
|
+
import { correctEmployeeVisibleReply, employeeStreamClose, takeEmployeeVisibleEvents, } from '../../tool/mcp-tool-exposure.js';
|
|
24
25
|
import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
|
|
25
26
|
import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
|
|
26
27
|
import { recordTaskTokenUsages } from '../../token/record-task-token-usage.js';
|
|
@@ -332,8 +333,10 @@ export class StreamOrchestrator {
|
|
|
332
333
|
collectPublicArtifactLinksFromEvent(runtimeEvent.type, runtimeEvent.payload, artifactLinks);
|
|
333
334
|
applyStreamReasoningEvent(reasoningState, runtimeEvent.type, runtimeEvent.payload);
|
|
334
335
|
applyStreamTextDelta(assistantBuckets, runtimeEvent.type, runtimeEvent.payload);
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
for (const visibleEvent of takeEmployeeVisibleEvents(runtimeEvent.type, runtimeEvent.payload)) {
|
|
337
|
+
if (shouldEmitTokenSse(tokenDetail, visibleEvent.type)) {
|
|
338
|
+
emitter.emit(this.runtimeEventToSSE({ ...runtimeEvent, type: visibleEvent.type, payload: visibleEvent.payload }, correlationId, taskId));
|
|
339
|
+
}
|
|
337
340
|
}
|
|
338
341
|
}
|
|
339
342
|
}
|
|
@@ -398,12 +401,14 @@ export class StreamOrchestrator {
|
|
|
398
401
|
}
|
|
399
402
|
// ─── Step 7: 发送结束事件序列 ───
|
|
400
403
|
const tokenStats = this.tokenStatsService.getTaskStats(taskId);
|
|
401
|
-
const
|
|
404
|
+
const rawVisible = visibleStreamAssistantText(assistantBuckets);
|
|
405
|
+
const visible = correctEmployeeVisibleReply(rawVisible, toolResults);
|
|
402
406
|
const links = mergeArtifactLinks(artifactLinks, collectPublicArtifactLinks(toolResults));
|
|
403
407
|
const { injected, suffix } = injectPublicArtifactLinks(visible, links);
|
|
404
|
-
|
|
408
|
+
const close = employeeStreamClose({ raw: rawVisible, tools: toolResults, suffix });
|
|
409
|
+
for (const content of close.chunks) {
|
|
405
410
|
emitter.emit(this.buildSSEEvent('response.chunk', correlationId, taskId, {
|
|
406
|
-
content
|
|
411
|
+
content,
|
|
407
412
|
format: 'text',
|
|
408
413
|
}));
|
|
409
414
|
}
|
|
@@ -442,7 +447,7 @@ export class StreamOrchestrator {
|
|
|
442
447
|
messages: buildTurnTranscriptFromStreamEvents({
|
|
443
448
|
pendingToolCalls,
|
|
444
449
|
toolResults,
|
|
445
|
-
assistantTexts:
|
|
450
|
+
assistantTexts: close.persistCorrected ? undefined : streamAssistantTexts(assistantBuckets),
|
|
446
451
|
assistantText: injected,
|
|
447
452
|
finishPoints: graph.finishPoints,
|
|
448
453
|
toolCallReasoningContent,
|
|
@@ -23,7 +23,8 @@ import { ToolResultInjector } from '../../tool/tool-result-injector.js';
|
|
|
23
23
|
import { fileArtifactFromToolOutput } from '../../tool/file-artifact.js';
|
|
24
24
|
import { EchoLLMProvider } from '../../providers/echo-llm-provider.js';
|
|
25
25
|
import { ConditionEvaluator } from '../condition-evaluator.js';
|
|
26
|
-
import { SystemPromptBuilder, buildSkillCatalog,
|
|
26
|
+
import { SystemPromptBuilder, buildSkillCatalog, resolveLlmPromptInputs, spokenName } from '../../tool/system-prompt-builder.js';
|
|
27
|
+
import { correctEmployeeVisibleReply, lastAssistantTextSinceLastUser, exposureScopeId, exposedNamesForScope, forcedToolName, holdEmployeeTextForUnnamedMcp, resolveModelToolSurface, shouldDropUnsettledEmployeeText, toolFactsSinceLastUser, } from '../../tool/mcp-tool-exposure.js';
|
|
27
28
|
import { resolveJailFromState, resolveJailRoots } from '../../tool/harness-primitives.js';
|
|
28
29
|
import os from 'node:os';
|
|
29
30
|
import { bindSelectedSkill } from '../nodes/skill-bind-node.js';
|
|
@@ -417,7 +418,7 @@ export class LangGraphAdapter {
|
|
|
417
418
|
});
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
|
-
if (progress.status === 'started') {
|
|
421
|
+
if (progress.status === 'started' && (!progress.quiet || progress.modelOnly)) {
|
|
421
422
|
stream.sink({
|
|
422
423
|
type: 'tool.called',
|
|
423
424
|
taskId,
|
|
@@ -429,19 +430,22 @@ export class LangGraphAdapter {
|
|
|
429
430
|
...(progress.toolset ? { toolset: progress.toolset } : {}),
|
|
430
431
|
...(progress.toolVersion ? { toolVersion: progress.toolVersion } : {}),
|
|
431
432
|
arguments: progress.arguments ?? {},
|
|
433
|
+
...(progress.modelOnly ? { modelOnly: true } : {}),
|
|
432
434
|
...loc,
|
|
433
435
|
},
|
|
434
436
|
});
|
|
435
437
|
}
|
|
436
438
|
// resolvedPath / skillDirs 只给上面 classifySkillFile;inSkillSubgraph 只给 skill.started 去重。不进对话流。
|
|
437
|
-
const { resolvedPath: _resolvedPath, skillDirs: _skillDirs, inSkillSubgraph: _inSkillSubgraph, ...progressView } = progress;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
439
|
+
const { resolvedPath: _resolvedPath, skillDirs: _skillDirs, inSkillSubgraph: _inSkillSubgraph, modelOnly: _modelOnly, ...progressView } = progress;
|
|
440
|
+
if (!progress.quiet) {
|
|
441
|
+
stream.sink({
|
|
442
|
+
type: 'tool.progress',
|
|
443
|
+
taskId,
|
|
444
|
+
correlationId,
|
|
445
|
+
timestamp,
|
|
446
|
+
payload: progressView,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
445
449
|
if (progress.status === 'completed') {
|
|
446
450
|
stream.sink({
|
|
447
451
|
type: 'tool.result',
|
|
@@ -456,6 +460,7 @@ export class LangGraphAdapter {
|
|
|
456
460
|
success: true,
|
|
457
461
|
...(progress.file ? { file: progress.file } : {}),
|
|
458
462
|
...(progress.files?.length ? { files: progress.files } : {}),
|
|
463
|
+
...(progress.modelOnly ? { modelOnly: true } : {}),
|
|
459
464
|
...loc,
|
|
460
465
|
},
|
|
461
466
|
});
|
|
@@ -472,6 +477,7 @@ export class LangGraphAdapter {
|
|
|
472
477
|
...(progress.toolset ? { toolset: progress.toolset } : {}),
|
|
473
478
|
error: progress.error ?? 'Unknown error',
|
|
474
479
|
willRetry: progress.willRetry === true,
|
|
480
|
+
...(progress.modelOnly ? { modelOnly: true } : {}),
|
|
475
481
|
...(progress.content ? { content: progress.content } : {}),
|
|
476
482
|
...(progress.file ? { file: progress.file } : {}),
|
|
477
483
|
...(progress.files?.length ? { files: progress.files } : {}),
|
|
@@ -545,6 +551,9 @@ export class LangGraphAdapter {
|
|
|
545
551
|
checkpointNs: ephemeral(undefined),
|
|
546
552
|
skillIsolateContext: ephemeral(undefined),
|
|
547
553
|
loadedTools: ephemeral(undefined),
|
|
554
|
+
exposedMcpTools: ephemeral(undefined),
|
|
555
|
+
exposedMcpScopeId: ephemeral(undefined),
|
|
556
|
+
sentMcpToolNames: ephemeral(undefined),
|
|
548
557
|
loadedSkills: ephemeral(undefined),
|
|
549
558
|
skillDefinitions: ephemeral(undefined),
|
|
550
559
|
skillInput: ephemeral(undefined),
|
|
@@ -770,10 +779,8 @@ export class LangGraphAdapter {
|
|
|
770
779
|
}
|
|
771
780
|
const duration = Date.now() - startTime;
|
|
772
781
|
const messages = Array.isArray(result['messages']) ? result['messages'] : [];
|
|
773
|
-
const
|
|
774
|
-
const output =
|
|
775
|
-
? String(lastMessage.content)
|
|
776
|
-
: '';
|
|
782
|
+
const rawOutput = lastAssistantTextSinceLastUser(messages);
|
|
783
|
+
const output = correctEmployeeVisibleReply(rawOutput, toolFactsSinceLastUser(messages));
|
|
777
784
|
// ── Token 累计:从 __llmTokenUsage 数组累加器读取每轮 LLM 明细 + 汇总
|
|
778
785
|
// 数组由 channel reducer 自动累加(每次 LLM 节点追加一条)。
|
|
779
786
|
const llmUsageArray = Array.isArray(result['__llmTokenUsage'])
|
|
@@ -798,6 +805,12 @@ export class LangGraphAdapter {
|
|
|
798
805
|
: state.messages,
|
|
799
806
|
userId: state.userId,
|
|
800
807
|
loadedTools: state.loadedTools ?? result.loadedTools,
|
|
808
|
+
exposedMcpTools: Array.isArray(result['exposedMcpTools'])
|
|
809
|
+
? result['exposedMcpTools']
|
|
810
|
+
: state.exposedMcpTools,
|
|
811
|
+
exposedMcpScopeId: typeof result['exposedMcpScopeId'] === 'string'
|
|
812
|
+
? result['exposedMcpScopeId']
|
|
813
|
+
: state.exposedMcpScopeId,
|
|
801
814
|
forceToolServer: state.forceToolServer ?? result.forceToolServer,
|
|
802
815
|
executionContext: state.executionContext ?? result.executionContext,
|
|
803
816
|
},
|
|
@@ -1202,10 +1215,18 @@ export class LangGraphAdapter {
|
|
|
1202
1215
|
agentSystemPrompt: state['agentSystemPrompt'],
|
|
1203
1216
|
nodePrompt: resolvedPrompt,
|
|
1204
1217
|
});
|
|
1218
|
+
const surface = resolveModelToolSurface({
|
|
1219
|
+
loadedTools,
|
|
1220
|
+
disableTools,
|
|
1221
|
+
exposedNames: exposedNamesForScope(state['exposedMcpTools'], state['exposedMcpScopeId'], exposureScopeId({ sessionId: state['sessionId'], taskId: state['taskId'] })),
|
|
1222
|
+
forcedName: forcedToolName(baseConfig.toolChoice),
|
|
1223
|
+
});
|
|
1224
|
+
const sentMcpToolNames = surface.apiTools.map((tool) => tool.function.name);
|
|
1205
1225
|
const assembled = this.systemPromptBuilder.build({
|
|
1206
1226
|
agentPrompt: promptInputs.agentPrompt,
|
|
1207
1227
|
nodePrompt: promptInputs.nodePrompt,
|
|
1208
1228
|
skillCatalog: catalog,
|
|
1229
|
+
mcpToolIndex: surface.mcpToolIndex,
|
|
1209
1230
|
agentName,
|
|
1210
1231
|
agentCode,
|
|
1211
1232
|
userId,
|
|
@@ -1214,7 +1235,10 @@ export class LangGraphAdapter {
|
|
|
1214
1235
|
jailRoots,
|
|
1215
1236
|
skillDirs,
|
|
1216
1237
|
});
|
|
1217
|
-
const apiTools =
|
|
1238
|
+
const apiTools = surface.apiTools;
|
|
1239
|
+
const sentTools = disableTools
|
|
1240
|
+
? undefined
|
|
1241
|
+
: (apiTools.length > 0 ? apiTools : baseConfig.tools);
|
|
1218
1242
|
const finalConfig = {
|
|
1219
1243
|
...baseConfig,
|
|
1220
1244
|
systemPrompt: assembled.text,
|
|
@@ -1226,7 +1250,7 @@ export class LangGraphAdapter {
|
|
|
1226
1250
|
agentCode,
|
|
1227
1251
|
correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
|
|
1228
1252
|
},
|
|
1229
|
-
tools:
|
|
1253
|
+
tools: sentTools,
|
|
1230
1254
|
toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
|
|
1231
1255
|
};
|
|
1232
1256
|
// 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
|
|
@@ -1237,22 +1261,28 @@ export class LangGraphAdapter {
|
|
|
1237
1261
|
stream.streamedTextThisTurn = false;
|
|
1238
1262
|
stream.reasoningChunkIndex = 0;
|
|
1239
1263
|
}
|
|
1264
|
+
const holdEmployeeText = holdEmployeeTextForUnnamedMcp(loadedTools, sentMcpToolNames);
|
|
1265
|
+
const heldDeltas = [];
|
|
1240
1266
|
const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
|
|
1241
1267
|
? (delta) => {
|
|
1242
1268
|
const live = getExecutionStream();
|
|
1243
1269
|
if (!live)
|
|
1244
1270
|
return;
|
|
1271
|
+
if (holdEmployeeText) {
|
|
1272
|
+
heldDeltas.push(delta);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1245
1275
|
emitLlmStreamDelta(live, node.id, finalConfig.model, delta);
|
|
1246
1276
|
}
|
|
1247
1277
|
: undefined, this.interceptors);
|
|
1248
|
-
const toolsForSpan =
|
|
1249
|
-
?
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
? { description: t.manifest.description.trim() }
|
|
1278
|
+
const toolsForSpan = sentTools && sentTools.length > 0
|
|
1279
|
+
? sentTools.map((tool) => ({
|
|
1280
|
+
name: tool.function.name,
|
|
1281
|
+
...(tool.function.description?.trim()
|
|
1282
|
+
? { description: tool.function.description.trim() }
|
|
1254
1283
|
: {}),
|
|
1255
|
-
}))
|
|
1284
|
+
}))
|
|
1285
|
+
: undefined;
|
|
1256
1286
|
// 组装后再 createNode:input 必须是发给模型的 system/tools/messages;completeNode 不能补 input。
|
|
1257
1287
|
return withGraphSpan(state, {
|
|
1258
1288
|
name: node.id,
|
|
@@ -1266,12 +1296,31 @@ export class LangGraphAdapter {
|
|
|
1266
1296
|
},
|
|
1267
1297
|
}, async () => {
|
|
1268
1298
|
const next = await llmNode.execute(state);
|
|
1269
|
-
|
|
1299
|
+
let dropEmployeeText = false;
|
|
1300
|
+
if (enableStream && holdEmployeeText) {
|
|
1301
|
+
const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
|
|
1302
|
+
const calls = added && typeof added === 'object' && Array.isArray(added.toolCalls)
|
|
1303
|
+
? added.toolCalls
|
|
1304
|
+
: [];
|
|
1305
|
+
dropEmployeeText = shouldDropUnsettledEmployeeText(calls, loadedTools, sentMcpToolNames);
|
|
1306
|
+
const live = getExecutionStream();
|
|
1307
|
+
if (dropEmployeeText) {
|
|
1308
|
+
// 挡住 updates 再把这段正文补发成 response.chunk。
|
|
1309
|
+
if (live)
|
|
1310
|
+
live.streamedTextThisTurn = true;
|
|
1311
|
+
}
|
|
1312
|
+
else if (live) {
|
|
1313
|
+
for (const delta of heldDeltas)
|
|
1314
|
+
emitLlmStreamDelta(live, node.id, finalConfig.model, delta);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (enableStream && !dropEmployeeText) {
|
|
1270
1318
|
emitReasoningCompletedIfAny(getExecutionStream(), node.id, next);
|
|
1271
1319
|
}
|
|
1272
1320
|
const hydratedUpdate = loadedTools && loadedTools !== loadedToolsBase ? { loadedTools } : {};
|
|
1321
|
+
const sentUpdate = { sentMcpToolNames };
|
|
1273
1322
|
if (!silent)
|
|
1274
|
-
return { ...next, ...hydratedUpdate };
|
|
1323
|
+
return { ...next, ...hydratedUpdate, ...sentUpdate };
|
|
1275
1324
|
const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
|
|
1276
1325
|
const raw = added && typeof added === 'object' && typeof added.content === 'string'
|
|
1277
1326
|
? added.content
|
|
@@ -1281,6 +1330,7 @@ export class LangGraphAdapter {
|
|
|
1281
1330
|
messages: [],
|
|
1282
1331
|
silentOutput: raw,
|
|
1283
1332
|
...hydratedUpdate,
|
|
1333
|
+
...sentUpdate,
|
|
1284
1334
|
};
|
|
1285
1335
|
}, { outputFrom: llmSpanOutputFromResult });
|
|
1286
1336
|
};
|
|
@@ -15,6 +15,7 @@ import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace
|
|
|
15
15
|
import { skillSpanFields, skillTraceIdentity } from './skill-entry-span.js';
|
|
16
16
|
import { filterLoadedToolsForSkill, spokenName, } from '../../tool/system-prompt-builder.js';
|
|
17
17
|
import { unwrapHitlPaused } from '../hitl/hitl-handler.js';
|
|
18
|
+
import { exposureScopeId, exposedNamesForScope, mergeExposed } from '../../tool/mcp-tool-exposure.js';
|
|
18
19
|
import { pickContextFields } from '../../token/context-window.js';
|
|
19
20
|
import { assertSkillIoIfEnabled, coerceSkillInputFromMessages, } from '../../registry/skill-io-guard.js';
|
|
20
21
|
import { isolatedSkillMessages, isolatedSkillOutput, } from './skill-isolate.js';
|
|
@@ -163,6 +164,15 @@ export class SubGraphCompiler {
|
|
|
163
164
|
skillIsolateContext: isolateContext || undefined,
|
|
164
165
|
executionContext: state['executionContext'],
|
|
165
166
|
loadedTools: scopedTools,
|
|
167
|
+
exposedMcpTools: Array.isArray(state['exposedMcpTools'])
|
|
168
|
+
? state['exposedMcpTools']
|
|
169
|
+
: undefined,
|
|
170
|
+
exposedMcpScopeId: typeof state['exposedMcpScopeId'] === 'string'
|
|
171
|
+
? state['exposedMcpScopeId']
|
|
172
|
+
: undefined,
|
|
173
|
+
sentMcpToolNames: Array.isArray(state['sentMcpToolNames'])
|
|
174
|
+
? state['sentMcpToolNames']
|
|
175
|
+
: undefined,
|
|
166
176
|
loadedSkills: state['loadedSkills'],
|
|
167
177
|
skillDefinitions: defs,
|
|
168
178
|
agentSystemPrompt: typeof state['agentSystemPrompt'] === 'string'
|
|
@@ -234,10 +244,25 @@ export class SubGraphCompiler {
|
|
|
234
244
|
undefined;
|
|
235
245
|
const parentNodeId = options?.parentNodeId
|
|
236
246
|
|| (dsl.name ? `subgraph:${dsl.name}` : 'subgraph');
|
|
247
|
+
const parentScope = exposureScopeId({
|
|
248
|
+
sessionId: state['sessionId'],
|
|
249
|
+
taskId: state['taskId'],
|
|
250
|
+
});
|
|
251
|
+
const childScope = result.finalState?.exposedMcpScopeId;
|
|
252
|
+
const parentNames = exposedNamesForScope(state['exposedMcpTools'], state['exposedMcpScopeId'], parentScope);
|
|
253
|
+
const childNames = childScope === parentScope && Array.isArray(result.finalState?.exposedMcpTools)
|
|
254
|
+
? result.finalState.exposedMcpTools
|
|
255
|
+
: [];
|
|
237
256
|
return {
|
|
238
257
|
messages: outputMessages,
|
|
239
258
|
currentNode: parentNodeId,
|
|
240
259
|
historyPath: [parentNodeId],
|
|
260
|
+
...(parentScope
|
|
261
|
+
? {
|
|
262
|
+
exposedMcpTools: mergeExposed(parentNames, childNames),
|
|
263
|
+
exposedMcpScopeId: parentScope,
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
241
266
|
...(nestedLastSpanId ? { lastSpanId: nestedLastSpanId } : {}),
|
|
242
267
|
...(Array.isArray(perNode) && perNode.length > 0
|
|
243
268
|
? {
|
|
@@ -197,6 +197,15 @@ export interface GraphState {
|
|
|
197
197
|
ragSources?: string[];
|
|
198
198
|
/** GraphLoader 注入:toolName → 运行期 manifest 引用 */
|
|
199
199
|
loadedTools?: import('../../tool/loaded-tool-definition.js').LoadedToolMap;
|
|
200
|
+
/**
|
|
201
|
+
* 本会话已点名、下一次模型调用要带完整参数的 MCP 工具名。
|
|
202
|
+
* 与 exposedMcpScopeId 一起用。有 sessionId 时按会话,否则按 taskId。新会话不沿用。
|
|
203
|
+
*/
|
|
204
|
+
exposedMcpTools?: string[];
|
|
205
|
+
/** 与 exposedMcpTools 对应的 sessionId,没有 sessionId 时是 taskId。 */
|
|
206
|
+
exposedMcpScopeId?: string;
|
|
207
|
+
/** 上一轮实际发给模型的工具名。工具节点用它判断没点名的 MCP 调用。 */
|
|
208
|
+
sentMcpToolNames?: string[];
|
|
200
209
|
/** GraphLoader 注入:已装配 Skill 列表 */
|
|
201
210
|
loadedSkills?: import('../../tool/system-prompt-builder.js').LoadedSkillInfo[];
|
|
202
211
|
/** GraphLoader 注入:skillId → 完整定义(子图解析 / prompt) */
|
|
@@ -14,6 +14,7 @@ import { buildContextUsage, turnSectionChars } from '../../token/context-window.
|
|
|
14
14
|
import { isAbortError } from '../../tool/timeout.js';
|
|
15
15
|
import { emitRuntimeLog } from '../../common/logger.js';
|
|
16
16
|
import { decideBeforeModel, } from '../../execution/interceptors.js';
|
|
17
|
+
import { projectHistoryForModelTools } from '../../tool/mcp-tool-exposure.js';
|
|
17
18
|
// ─────────────────────────────────────────────
|
|
18
19
|
// Default retry config
|
|
19
20
|
// ─────────────────────────────────────────────
|
|
@@ -84,7 +85,8 @@ export class LLMNode {
|
|
|
84
85
|
if (gate.action === 'deny') {
|
|
85
86
|
throw new Error(gate.reason);
|
|
86
87
|
}
|
|
87
|
-
const
|
|
88
|
+
const toolNames = new Set((this.config.tools ?? []).map((tool) => tool.function.name));
|
|
89
|
+
const messagesForModel = projectHistoryForModelTools(gate.messages ?? messagesWithSystem, toolNames);
|
|
88
90
|
const startedAt = Date.now();
|
|
89
91
|
// 带重试的 LLM 调用
|
|
90
92
|
const response = await this.invokeWithRetry(messagesForModel);
|
|
@@ -58,6 +58,10 @@ export interface ToolNodeProgress {
|
|
|
58
58
|
toolVersion?: string;
|
|
59
59
|
/** 本失败后是否还会再试。缺省 false */
|
|
60
60
|
willRetry?: boolean;
|
|
61
|
+
/** 不发 tool.progress。tool_search 仍发 tool.result,供最终回复判断。 */
|
|
62
|
+
quiet?: boolean;
|
|
63
|
+
/** 只给模型。不进员工 SSE。未点名的调用用这个。 */
|
|
64
|
+
modelOnly?: boolean;
|
|
61
65
|
}
|
|
62
66
|
export declare class ToolNode {
|
|
63
67
|
private readonly orchestrator;
|
|
@@ -17,6 +17,7 @@ import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
|
|
|
17
17
|
import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
|
|
18
18
|
import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
|
|
19
19
|
import { mcpToolsetCode, toolTraceIdentity } from '../../tool/mcp-list.js';
|
|
20
|
+
import { exposureScopeId, exposedNamesForScope, isUnloadedDeferredCall, mcpNamesToExpose, mergeExposed, partitionToolSearchCalls, runToolSearch, unloadedCallToolContent, } from '../../tool/mcp-tool-exposure.js';
|
|
20
21
|
import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
|
|
21
22
|
import { emitRuntimeLog } from '../../common/logger.js';
|
|
22
23
|
import { toolResultHasArtifacts } from '../../tool/inject-public-artifact-links.js';
|
|
@@ -36,6 +37,28 @@ function skillDirsFromState(state) {
|
|
|
36
37
|
.filter((skill) => Boolean(skill.bundlePath))
|
|
37
38
|
.map((skill) => ({ skillId: skill.skillId, bundlePath: skill.bundlePath }));
|
|
38
39
|
}
|
|
40
|
+
function exposureUpdate(state, searchCalls, businessNames, loadedTools, alreadyInTools) {
|
|
41
|
+
const scopeId = exposureScopeId({ sessionId: state['sessionId'], taskId: state['taskId'] });
|
|
42
|
+
const existing = exposedNamesForScope(state['exposedMcpTools'], state['exposedMcpScopeId'], scopeId);
|
|
43
|
+
const messagesById = new Map();
|
|
44
|
+
const searchExpose = [];
|
|
45
|
+
for (const call of searchCalls) {
|
|
46
|
+
const outcome = runToolSearch(call.arguments, loadedTools, alreadyInTools);
|
|
47
|
+
searchExpose.push(...outcome.expose);
|
|
48
|
+
messagesById.set(call.id, {
|
|
49
|
+
role: 'tool',
|
|
50
|
+
content: outcome.content,
|
|
51
|
+
toolCallId: call.id,
|
|
52
|
+
toolName: call.name,
|
|
53
|
+
success: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
messagesById,
|
|
58
|
+
exposedMcpTools: mergeExposed(existing, mcpNamesToExpose(loadedTools, searchExpose, businessNames)),
|
|
59
|
+
exposedMcpScopeId: scopeId,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
39
62
|
export class ToolNode {
|
|
40
63
|
orchestrator;
|
|
41
64
|
injector;
|
|
@@ -83,6 +106,15 @@ export class ToolNode {
|
|
|
83
106
|
return state;
|
|
84
107
|
}
|
|
85
108
|
const loadedTools = state['loadedTools'];
|
|
109
|
+
const partitioned = partitionToolSearchCalls(lastAssistantMessage.toolCalls, loadedTools);
|
|
110
|
+
const sentMcpToolNames = Array.isArray(state['sentMcpToolNames'])
|
|
111
|
+
? state['sentMcpToolNames']
|
|
112
|
+
: undefined;
|
|
113
|
+
const blockedCalls = partitioned.deferActive
|
|
114
|
+
? partitioned.business.filter((call) => isUnloadedDeferredCall(loadedTools?.[call.name], sentMcpToolNames))
|
|
115
|
+
: [];
|
|
116
|
+
const blockedIds = new Set(blockedCalls.map((call) => call.id));
|
|
117
|
+
const executableCalls = partitioned.business.filter((call) => !blockedIds.has(call.id));
|
|
86
118
|
const skillDirs = skillDirsFromState(state);
|
|
87
119
|
const jail = resolveJailFromState({
|
|
88
120
|
agentDir: state['agentDir'],
|
|
@@ -92,13 +124,13 @@ export class ToolNode {
|
|
|
92
124
|
skillDefinitions: state['skillDefinitions'],
|
|
93
125
|
});
|
|
94
126
|
const jailExtraRoots = jail.extraRoots ?? [];
|
|
95
|
-
const batchCalls =
|
|
127
|
+
const batchCalls = executableCalls.map((tc) => ({
|
|
96
128
|
name: tc.name,
|
|
97
129
|
arguments: tc.arguments,
|
|
98
130
|
}));
|
|
99
131
|
const boundSkill = typeof state['activatedSkill'] === 'string' ? state['activatedSkill'] : undefined;
|
|
100
132
|
// 4. 转换为 ToolCallRequest[](附带 manifest 路由字段)
|
|
101
|
-
const requests =
|
|
133
|
+
const requests = executableCalls.map((tc) => {
|
|
102
134
|
const loaded = loadedTools?.[tc.name];
|
|
103
135
|
const toolsetCode = mcpToolsetCode(loaded);
|
|
104
136
|
const correlationId = String(state['correlationId'] ?? tc.id);
|
|
@@ -170,6 +202,84 @@ export class ToolNode {
|
|
|
170
202
|
? state['hitlToolCallId']
|
|
171
203
|
: undefined;
|
|
172
204
|
const confirmationCount = requests.filter((req) => confirmationTools.has(req.toolName)).length;
|
|
205
|
+
// 点名先记进本轮返回值。HITL 抛出时这次返回作废,恢复后本节点会再跑一遍并带上这些名字。
|
|
206
|
+
const exposure = partitioned.deferActive
|
|
207
|
+
? exposureUpdate(state, partitioned.search, partitioned.business.map((call) => call.name), loadedTools, sentMcpToolNames)
|
|
208
|
+
: undefined;
|
|
209
|
+
const blockedMessages = new Map();
|
|
210
|
+
const sideSpanIds = [];
|
|
211
|
+
const recordSideSpan = async (name, callId, input, output, notExecuted) => {
|
|
212
|
+
if (!traceCollector || !traceId)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
const parentNodeId = typeof state['skillSpanId'] === 'string' && state['skillSpanId']
|
|
216
|
+
? state['skillSpanId']
|
|
217
|
+
: undefined;
|
|
218
|
+
const spanId = await traceCollector.createNode({
|
|
219
|
+
traceId,
|
|
220
|
+
name,
|
|
221
|
+
type: 'tool',
|
|
222
|
+
parentNodeId,
|
|
223
|
+
agentCode: agentCode || undefined,
|
|
224
|
+
input,
|
|
225
|
+
metadata: {
|
|
226
|
+
toolCallId: callId,
|
|
227
|
+
...(notExecuted ? { notExecuted: true } : {}),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
sideSpanIds.push(spanId);
|
|
231
|
+
await traceCollector.completeNode(spanId, {
|
|
232
|
+
status: notExecuted ? 'failed' : 'completed',
|
|
233
|
+
output,
|
|
234
|
+
...(notExecuted ? { error: output } : {}),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
warnTraceSpanFailure('createNode', { type: 'tool', name, traceId, toolCallId: callId }, err);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
for (const call of partitioned.search) {
|
|
242
|
+
const content = exposure?.messagesById.get(call.id)?.content ?? '';
|
|
243
|
+
emitProgress({
|
|
244
|
+
toolCallId: call.id,
|
|
245
|
+
toolName: call.name,
|
|
246
|
+
status: 'completed',
|
|
247
|
+
arguments: call.arguments,
|
|
248
|
+
content,
|
|
249
|
+
quiet: true,
|
|
250
|
+
modelOnly: true,
|
|
251
|
+
});
|
|
252
|
+
await recordSideSpan(call.name, call.id, call.arguments, content, false);
|
|
253
|
+
}
|
|
254
|
+
for (const call of blockedCalls) {
|
|
255
|
+
const content = unloadedCallToolContent(call.name);
|
|
256
|
+
blockedMessages.set(call.id, {
|
|
257
|
+
role: 'tool',
|
|
258
|
+
content,
|
|
259
|
+
toolCallId: call.id,
|
|
260
|
+
toolName: call.name,
|
|
261
|
+
success: false,
|
|
262
|
+
});
|
|
263
|
+
emitProgress({
|
|
264
|
+
toolCallId: call.id,
|
|
265
|
+
toolName: call.name,
|
|
266
|
+
status: 'started',
|
|
267
|
+
arguments: call.arguments,
|
|
268
|
+
quiet: true,
|
|
269
|
+
modelOnly: true,
|
|
270
|
+
});
|
|
271
|
+
emitProgress({
|
|
272
|
+
toolCallId: call.id,
|
|
273
|
+
toolName: call.name,
|
|
274
|
+
status: 'error',
|
|
275
|
+
arguments: call.arguments,
|
|
276
|
+
error: content,
|
|
277
|
+
content,
|
|
278
|
+
quiet: true,
|
|
279
|
+
modelOnly: true,
|
|
280
|
+
});
|
|
281
|
+
await recordSideSpan(call.name, call.id, call.arguments, content, true);
|
|
282
|
+
}
|
|
173
283
|
// 5. 并行执行所有 Tool 调用(附 Trace span)
|
|
174
284
|
const spanIds = await Promise.all(requests.map(async (req) => {
|
|
175
285
|
const reusePausedSpan = Boolean(resumeToolSpanId) &&
|
|
@@ -443,11 +553,29 @@ export class ToolNode {
|
|
|
443
553
|
...('file' in result && result.file ? { file: result.file } : {}),
|
|
444
554
|
...('files' in result && result.files?.length ? { files: result.files } : {}),
|
|
445
555
|
}));
|
|
446
|
-
const lastSpanId = [...spanIds].reverse().find((id) => Boolean(id));
|
|
556
|
+
const lastSpanId = [...spanIds, ...sideSpanIds].reverse().find((id) => Boolean(id));
|
|
557
|
+
const orderedMessages = (exposure && partitioned.intercept) || blockedMessages.size > 0
|
|
558
|
+
? lastAssistantMessage.toolCalls.map((call) => {
|
|
559
|
+
const searched = exposure?.messagesById.get(call.id);
|
|
560
|
+
if (searched)
|
|
561
|
+
return searched;
|
|
562
|
+
const blocked = blockedMessages.get(call.id);
|
|
563
|
+
if (blocked)
|
|
564
|
+
return blocked;
|
|
565
|
+
const business = toolMessages.find((message) => message.toolCallId === call.id);
|
|
566
|
+
return business;
|
|
567
|
+
})
|
|
568
|
+
: toolMessages;
|
|
447
569
|
return {
|
|
448
|
-
messages:
|
|
570
|
+
messages: orderedMessages,
|
|
449
571
|
currentNode: nodeId,
|
|
450
572
|
...(lastSpanId ? { lastSpanId } : {}),
|
|
573
|
+
...(exposure
|
|
574
|
+
? {
|
|
575
|
+
exposedMcpTools: exposure.exposedMcpTools,
|
|
576
|
+
exposedMcpScopeId: exposure.exposedMcpScopeId,
|
|
577
|
+
}
|
|
578
|
+
: {}),
|
|
451
579
|
};
|
|
452
580
|
}
|
|
453
581
|
// ─────────────────────────────────────────────
|
|
@@ -270,6 +270,15 @@ function resolveMcpServer(manifest) {
|
|
|
270
270
|
}
|
|
271
271
|
return undefined;
|
|
272
272
|
}
|
|
273
|
+
/** 门户把同一次注册的「用户确认」写在每个 tools[] 上。MCP 不展开这份清单,占位记录读这个值。 */
|
|
274
|
+
function mcpToolsRequireConfirmation(manifest) {
|
|
275
|
+
const tools = manifest.tools;
|
|
276
|
+
if (!Array.isArray(tools))
|
|
277
|
+
return false;
|
|
278
|
+
return tools.some((tool) => Boolean(tool &&
|
|
279
|
+
typeof tool === 'object' &&
|
|
280
|
+
tool.requireConfirmation));
|
|
281
|
+
}
|
|
273
282
|
function mcpDiscoverStub(pre, options) {
|
|
274
283
|
const m = pre.manifest;
|
|
275
284
|
const server = resolveMcpServer(m);
|
|
@@ -306,7 +315,7 @@ function mcpDiscoverStub(pre, options) {
|
|
|
306
315
|
dataScope: normalizeDataScope(m.permissions && typeof m.permissions === 'object' && !Array.isArray(m.permissions)
|
|
307
316
|
? m.permissions.dataScope
|
|
308
317
|
: m.dataScope),
|
|
309
|
-
requireConfirmation:
|
|
318
|
+
requireConfirmation: mcpToolsRequireConfirmation(m),
|
|
310
319
|
execution,
|
|
311
320
|
},
|
|
312
321
|
};
|