@gt-fe/eap-runtime 0.0.33 → 0.0.35
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/invoke.handler.d.ts +0 -1
- package/dist/api/handlers/invoke.handler.js +22 -5
- package/dist/api/handlers/stream.handler.d.ts +0 -1
- package/dist/api/handlers/stream.handler.js +30 -23
- package/dist/common/types.d.ts +4 -3
- package/dist/graph/adapters/langgraph.adapter.js +181 -86
- package/dist/graph/adapters/subgraph-compiler.js +25 -0
- package/dist/graph/contracts/executable-graph.d.ts +9 -0
- package/dist/graph/execution-trace-context.d.ts +3 -0
- package/dist/graph/nodes/llm-node.d.ts +7 -2
- package/dist/graph/nodes/llm-node.js +11 -4
- package/dist/graph/nodes/tool-node.d.ts +4 -0
- package/dist/graph/nodes/tool-node.js +132 -4
- package/dist/providers/http-llm-provider.js +27 -10
- package/dist/registry/graph-loader.js +10 -1
- package/dist/runtime/agent-runtime.d.ts +7 -0
- package/dist/runtime/agent-runtime.js +91 -50
- package/dist/runtime/types.d.ts +1 -1
- package/dist/session/persist-turn-messages.d.ts +10 -2
- package/dist/session/persist-turn-messages.js +35 -10
- package/dist/sse/events/index.d.ts +6 -4
- package/dist/sse/events/index.js +3 -1
- package/dist/sse/events/reasoning-events.d.ts +24 -2
- package/dist/sse/events/reasoning-events.js +3 -1
- package/dist/sse/events/skill-events.d.ts +2 -0
- package/dist/task/contracts/task-manager.contract.d.ts +2 -2
- package/dist/task/contracts/task-manager.contract.js +1 -1
- package/dist/task/index.d.ts +2 -0
- package/dist/task/index.js +1 -0
- package/dist/task/task-abort.d.ts +14 -0
- package/dist/task/task-abort.js +33 -0
- package/dist/task/task-timeout.d.ts +11 -0
- package/dist/task/task-timeout.js +40 -4
- package/dist/task/task.service.js +11 -5
- package/dist/tool/index.d.ts +2 -1
- package/dist/tool/index.js +2 -1
- 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 +13 -10
- package/dist/tool/system-prompt-builder.js +18 -32
- package/package.json +1 -1
|
@@ -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) */
|
|
@@ -8,9 +8,12 @@ export interface ExecutionStreamBag {
|
|
|
8
8
|
correlationId?: string;
|
|
9
9
|
};
|
|
10
10
|
chunkIndex: number;
|
|
11
|
+
/** 本轮 reasoning.chunk 序号,与 chunkIndex 分开从 0 计 */
|
|
12
|
+
reasoningChunkIndex: number;
|
|
11
13
|
iteration: number;
|
|
12
14
|
reasoningStartedThisTurn: boolean;
|
|
13
15
|
streamedTextThisTurn: boolean;
|
|
16
|
+
graphId?: string;
|
|
14
17
|
}
|
|
15
18
|
export interface ExecutionRunContext {
|
|
16
19
|
collector?: ITraceCollector;
|
|
@@ -107,6 +107,11 @@ export interface LLMChunk {
|
|
|
107
107
|
/** thinking 增量;最终聚合后写入 assistant 消息 */
|
|
108
108
|
reasoningContent?: string;
|
|
109
109
|
}
|
|
110
|
+
/** 流式旁路增量:思考与正文分字段,空字段不传 */
|
|
111
|
+
export type LLMStreamDelta = {
|
|
112
|
+
content?: string;
|
|
113
|
+
reasoningContent?: string;
|
|
114
|
+
};
|
|
110
115
|
/**
|
|
111
116
|
* ILLMProvider — LLM 调用提供者接口
|
|
112
117
|
*
|
|
@@ -145,9 +150,9 @@ export declare class LLMNode {
|
|
|
145
150
|
* @param config - LLM 节点配置
|
|
146
151
|
* @param provider - LLM 调用提供者
|
|
147
152
|
* @param streaming - 是否启用流式输出(默认 false)
|
|
148
|
-
* @param onChunk -
|
|
153
|
+
* @param onChunk - 流式旁路:思考走 reasoningContent,正文走 content
|
|
149
154
|
*/
|
|
150
|
-
constructor(nodeId: string, config: LLMNodeConfig, provider: ILLMProvider, streaming?: boolean, onChunk?: (
|
|
155
|
+
constructor(nodeId: string, config: LLMNodeConfig, provider: ILLMProvider, streaming?: boolean, onChunk?: (delta: LLMStreamDelta) => void, interceptors?: Pick<ExecutionInterceptors, "beforeModel" | "afterModel"> | undefined);
|
|
151
156
|
/**
|
|
152
157
|
* execute — StateGraph 节点函数
|
|
153
158
|
*
|
|
@@ -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
|
// ─────────────────────────────────────────────
|
|
@@ -52,7 +53,7 @@ export class LLMNode {
|
|
|
52
53
|
* @param config - LLM 节点配置
|
|
53
54
|
* @param provider - LLM 调用提供者
|
|
54
55
|
* @param streaming - 是否启用流式输出(默认 false)
|
|
55
|
-
* @param onChunk -
|
|
56
|
+
* @param onChunk - 流式旁路:思考走 reasoningContent,正文走 content
|
|
56
57
|
*/
|
|
57
58
|
constructor(nodeId, config, provider, streaming = false, onChunk, interceptors) {
|
|
58
59
|
this.interceptors = interceptors;
|
|
@@ -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);
|
|
@@ -247,8 +249,13 @@ export class LLMNode {
|
|
|
247
249
|
let toolCalls;
|
|
248
250
|
const iterator = this.provider.stream(messages, this.config);
|
|
249
251
|
for await (const chunk of iterator) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
const content = chunk.content || '';
|
|
253
|
+
const reasoningContent = chunk.reasoningContent || '';
|
|
254
|
+
if (this.onChunk && (content || reasoningContent)) {
|
|
255
|
+
this.onChunk({
|
|
256
|
+
...(content ? { content } : {}),
|
|
257
|
+
...(reasoningContent ? { reasoningContent } : {}),
|
|
258
|
+
});
|
|
252
259
|
}
|
|
253
260
|
chunks.push(chunk.content);
|
|
254
261
|
if (chunk.reasoningContent) {
|
|
@@ -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
|
// ─────────────────────────────────────────────
|
|
@@ -119,7 +119,7 @@ export class HttpLLMProvider {
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
catch (err) {
|
|
122
|
-
|
|
122
|
+
throwIfGraphAborted();
|
|
123
123
|
throw new Error(this.formatNetworkError(url, err));
|
|
124
124
|
}
|
|
125
125
|
if (!response.ok) {
|
|
@@ -170,6 +170,7 @@ export class HttpLLMProvider {
|
|
|
170
170
|
return result;
|
|
171
171
|
}
|
|
172
172
|
catch (err) {
|
|
173
|
+
const propagated = graphAbortOr(err);
|
|
173
174
|
await this.writeTurn({
|
|
174
175
|
url,
|
|
175
176
|
messages,
|
|
@@ -180,10 +181,10 @@ export class HttpLLMProvider {
|
|
|
180
181
|
inputTokens: 0,
|
|
181
182
|
outputTokens: 0,
|
|
182
183
|
finishReason: 'error',
|
|
183
|
-
error:
|
|
184
|
+
error: propagated instanceof Error ? propagated.message : String(propagated),
|
|
184
185
|
},
|
|
185
186
|
});
|
|
186
|
-
throw
|
|
187
|
+
throw propagated;
|
|
187
188
|
}
|
|
188
189
|
finally {
|
|
189
190
|
clearTimeout(timeout);
|
|
@@ -244,7 +245,7 @@ export class HttpLLMProvider {
|
|
|
244
245
|
});
|
|
245
246
|
}
|
|
246
247
|
catch (err) {
|
|
247
|
-
|
|
248
|
+
throwIfGraphAborted();
|
|
248
249
|
throw new Error(this.formatNetworkError(url, err));
|
|
249
250
|
}
|
|
250
251
|
if (!response.ok) {
|
|
@@ -397,6 +398,7 @@ export class HttpLLMProvider {
|
|
|
397
398
|
});
|
|
398
399
|
}
|
|
399
400
|
catch (err) {
|
|
401
|
+
const propagated = graphAbortOr(err);
|
|
400
402
|
await this.writeTurn({
|
|
401
403
|
url,
|
|
402
404
|
messages,
|
|
@@ -411,10 +413,10 @@ export class HttpLLMProvider {
|
|
|
411
413
|
inputTokens,
|
|
412
414
|
outputTokens,
|
|
413
415
|
finishReason: 'error',
|
|
414
|
-
error:
|
|
416
|
+
error: propagated instanceof Error ? propagated.message : String(propagated),
|
|
415
417
|
},
|
|
416
418
|
});
|
|
417
|
-
throw
|
|
419
|
+
throw propagated;
|
|
418
420
|
}
|
|
419
421
|
finally {
|
|
420
422
|
clearTimeout(timeout);
|
|
@@ -528,11 +530,26 @@ function linkGraphAbort(controller) {
|
|
|
528
530
|
graphSignal.addEventListener('abort', onAbort, { once: true });
|
|
529
531
|
return () => graphSignal.removeEventListener('abort', onAbort);
|
|
530
532
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
533
|
+
/** 这一轮 signal 已 abort 就抛出 reason。超时和用户停止都走这里,不再只认 timed out。 */
|
|
534
|
+
function throwIfGraphAborted() {
|
|
535
|
+
const reason = graphAbortReason();
|
|
536
|
+
if (reason)
|
|
534
537
|
throw reason;
|
|
535
|
-
|
|
538
|
+
}
|
|
539
|
+
/** 流式 read 抛出的 DOMException 换成这一轮 signal 上的 reason。 */
|
|
540
|
+
function graphAbortOr(err) {
|
|
541
|
+
return graphAbortReason() ?? err;
|
|
542
|
+
}
|
|
543
|
+
function graphAbortReason() {
|
|
544
|
+
const signal = getExecutionTrace()?.abortSignal;
|
|
545
|
+
if (!signal?.aborted)
|
|
546
|
+
return undefined;
|
|
547
|
+
const reason = signal.reason;
|
|
548
|
+
if (reason instanceof Error)
|
|
549
|
+
return reason;
|
|
550
|
+
const err = new Error(reason == null ? 'Task aborted' : String(reason));
|
|
551
|
+
err.name = 'AbortError';
|
|
552
|
+
return err;
|
|
536
553
|
}
|
|
537
554
|
function formatNetworkDetail(error, depth = 0) {
|
|
538
555
|
if (depth > 5)
|
|
@@ -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
|
};
|
|
@@ -11,6 +11,7 @@ import type { ICheckpointSaver } from '../checkpoint/contracts/checkpoint-saver.
|
|
|
11
11
|
import type { ITraceCollector } from '../contracts/trace-collector.js';
|
|
12
12
|
import type { IStateGraphEngine } from '../graph/contracts/state-graph.contract.js';
|
|
13
13
|
import type { TaskTimeoutManager } from '../task/task-timeout.js';
|
|
14
|
+
import { type TaskAbortKind } from '../task/task-abort.js';
|
|
14
15
|
import { HitlHandler } from '../graph/hitl/hitl-handler.js';
|
|
15
16
|
import { type GraphLoader } from '../registry/graph-loader.js';
|
|
16
17
|
import type { SessionContextService } from '../session/session-context.service.js';
|
|
@@ -88,6 +89,12 @@ export interface ConversationDetail {
|
|
|
88
89
|
export declare class AgentRuntime {
|
|
89
90
|
private readonly deps;
|
|
90
91
|
constructor(deps: AgentRuntimeDeps);
|
|
92
|
+
/**
|
|
93
|
+
* 停止这一轮。超时和用户停止共用 timeoutManager 上的那条 AbortSignal。
|
|
94
|
+
* 计时器还没启动时先记下,startTimer 会返回已 abort 的 signal,且不调用 onTimeout。
|
|
95
|
+
* @returns 已 abort 或已记下。没有 timeoutManager 时返回 false。
|
|
96
|
+
*/
|
|
97
|
+
abort(taskId: string, kind?: TaskAbortKind): boolean;
|
|
91
98
|
/** 从 GraphLoader 解析 tool manifest 映射(invoke/stream/resume/fork 共用) */
|
|
92
99
|
private resolveLoadedTools;
|
|
93
100
|
/** 从预装重载 Skill 定义(HITL/Fork 时 ephemeral 字段可能丢失) */
|