@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.
Files changed (41) hide show
  1. package/dist/api/handlers/invoke.handler.d.ts +0 -1
  2. package/dist/api/handlers/invoke.handler.js +22 -5
  3. package/dist/api/handlers/stream.handler.d.ts +0 -1
  4. package/dist/api/handlers/stream.handler.js +30 -23
  5. package/dist/common/types.d.ts +4 -3
  6. package/dist/graph/adapters/langgraph.adapter.js +181 -86
  7. package/dist/graph/adapters/subgraph-compiler.js +25 -0
  8. package/dist/graph/contracts/executable-graph.d.ts +9 -0
  9. package/dist/graph/execution-trace-context.d.ts +3 -0
  10. package/dist/graph/nodes/llm-node.d.ts +7 -2
  11. package/dist/graph/nodes/llm-node.js +11 -4
  12. package/dist/graph/nodes/tool-node.d.ts +4 -0
  13. package/dist/graph/nodes/tool-node.js +132 -4
  14. package/dist/providers/http-llm-provider.js +27 -10
  15. package/dist/registry/graph-loader.js +10 -1
  16. package/dist/runtime/agent-runtime.d.ts +7 -0
  17. package/dist/runtime/agent-runtime.js +91 -50
  18. package/dist/runtime/types.d.ts +1 -1
  19. package/dist/session/persist-turn-messages.d.ts +10 -2
  20. package/dist/session/persist-turn-messages.js +35 -10
  21. package/dist/sse/events/index.d.ts +6 -4
  22. package/dist/sse/events/index.js +3 -1
  23. package/dist/sse/events/reasoning-events.d.ts +24 -2
  24. package/dist/sse/events/reasoning-events.js +3 -1
  25. package/dist/sse/events/skill-events.d.ts +2 -0
  26. package/dist/task/contracts/task-manager.contract.d.ts +2 -2
  27. package/dist/task/contracts/task-manager.contract.js +1 -1
  28. package/dist/task/index.d.ts +2 -0
  29. package/dist/task/index.js +1 -0
  30. package/dist/task/task-abort.d.ts +14 -0
  31. package/dist/task/task-abort.js +33 -0
  32. package/dist/task/task-timeout.d.ts +11 -0
  33. package/dist/task/task-timeout.js +40 -4
  34. package/dist/task/task.service.js +11 -5
  35. package/dist/tool/index.d.ts +2 -1
  36. package/dist/tool/index.js +2 -1
  37. package/dist/tool/mcp-tool-exposure.d.ts +113 -0
  38. package/dist/tool/mcp-tool-exposure.js +479 -0
  39. package/dist/tool/system-prompt-builder.d.ts +13 -10
  40. package/dist/tool/system-prompt-builder.js +18 -32
  41. 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 - 流式模式下每个文本片段回调(用于 SSE response.chunk)
153
+ * @param onChunk - 流式旁路:思考走 reasoningContent,正文走 content
149
154
  */
150
- constructor(nodeId: string, config: LLMNodeConfig, provider: ILLMProvider, streaming?: boolean, onChunk?: (chunk: string) => void, interceptors?: Pick<ExecutionInterceptors, "beforeModel" | "afterModel"> | undefined);
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 - 流式模式下每个文本片段回调(用于 SSE response.chunk)
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 messagesForModel = gate.messages ?? messagesWithSystem;
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
- if (chunk.content && this.onChunk) {
251
- this.onChunk(chunk.content);
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 = lastAssistantMessage.toolCalls.map((tc) => ({
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 = lastAssistantMessage.toolCalls.map((tc) => {
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: toolMessages,
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
- throwIfGraphTimedOut();
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: err instanceof Error ? err.message : String(err),
184
+ error: propagated instanceof Error ? propagated.message : String(propagated),
184
185
  },
185
186
  });
186
- throw err;
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
- throwIfGraphTimedOut();
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: err instanceof Error ? err.message : String(err),
416
+ error: propagated instanceof Error ? propagated.message : String(propagated),
415
417
  },
416
418
  });
417
- throw err;
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
- function throwIfGraphTimedOut() {
532
- const reason = getExecutionTrace()?.abortSignal?.reason;
533
- if (reason instanceof Error && /timed out/i.test(reason.message)) {
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: false,
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 字段可能丢失) */