@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
@@ -81,7 +81,6 @@ export declare class InvokeOrchestrator {
81
81
  execute(req: InvokeRequest, userId: string): Promise<InvokeResponse>;
82
82
  private buildResponse;
83
83
  private buildErrorResponse;
84
- private isTimeoutError;
85
84
  private persistInvokeUsages;
86
85
  }
87
86
  /** @deprecated 请直接从 `graph/hitl/hitl-handler` 导入;此处保留 re-export 兼容旧测试引用 */
@@ -26,6 +26,7 @@ import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
26
26
  import { classifyRuntimeError } from '../../utils/http-error.js';
27
27
  import { DEFAULT_GRAPH_TIMEOUT_MS } from '../../common/types.js';
28
28
  import { resolveTaskTimeoutMs } from '../../task/resolve-task-timeout.js';
29
+ import { taskAbortKindOf } from '../../task/task-abort.js';
29
30
  import { runWithExecutionTrace } from '../../graph/execution-trace-context.js';
30
31
  import { resolveAgentDisplayNameFromLoader, resolveAgentSystemPromptFromLoader, } from '../../registry/graph-loader.js';
31
32
  import { toLoadedToolMap } from '../../tool/loaded-tool-definition.js';
@@ -313,8 +314,8 @@ export class InvokeOrchestrator {
313
314
  status: 'token_exhausted',
314
315
  });
315
316
  }
316
- // ─── 错误处理:Timeout ───
317
- if (this.isTimeoutError(error)) {
317
+ const abortKind = taskAbortKindOf(error, abortSignal);
318
+ if (abortKind === 'timeout') {
318
319
  await this.taskService.tryUpdateStatus(task.id, 'running', 'timeout');
319
320
  return this.buildResponse(traceId, {
320
321
  taskId: task.id,
@@ -325,6 +326,25 @@ export class InvokeOrchestrator {
325
326
  status: 'timeout',
326
327
  });
327
328
  }
329
+ if (abortKind === 'cancelled') {
330
+ const moved = await this.taskService.tryUpdateStatus(task.id, 'running', 'cancelled');
331
+ if (!moved) {
332
+ await this.taskService.tryUpdateStatus(task.id, 'waiting_hitl', 'cancelled');
333
+ }
334
+ // @gt-fe/eap-contracts 的 InvokeStatus 还没有 cancelled。这次不改包,响应体仍写 cancelled。
335
+ return {
336
+ code: 'runtime.task.cancelled',
337
+ data: {
338
+ taskId: task.id,
339
+ sessionId,
340
+ output: 'Task cancelled',
341
+ tokenUsage,
342
+ duration: Date.now() - startTime,
343
+ status: 'cancelled',
344
+ },
345
+ traceId,
346
+ };
347
+ }
328
348
  // ─── 错误处理:通用错误 ───
329
349
  const classified = classifyRuntimeError(error);
330
350
  const fuse = classified.code === ErrorCodes.GRAPH_MAX_ITERATIONS ? classified : undefined;
@@ -432,9 +452,6 @@ export class InvokeOrchestrator {
432
452
  }
433
453
  return { code: 'runtime.graph.execution_error', data, traceId };
434
454
  }
435
- isTimeoutError(error) {
436
- return error instanceof Error && /timed out/i.test(error.message);
437
- }
438
455
  async persistInvokeUsages(taskId, sessionId) {
439
456
  if (!this.checkpointSaver)
440
457
  return [];
@@ -98,5 +98,4 @@ export declare class StreamOrchestrator {
98
98
  * 发射错误事件并关闭连接
99
99
  */
100
100
  private emitErrorAndClose;
101
- private isTimeoutError;
102
101
  }
@@ -19,8 +19,9 @@
19
19
  * @see design.md §1.5 /stream 内部执行逻辑
20
20
  * @see requirements.md Requirement 11 AC2
21
21
  */
22
- import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, appendTerminalToolResult, } from '../../session/persist-turn-messages.js';
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';
@@ -42,6 +43,7 @@ import { buildHitlWaitedPayload } from '../../graph/hitl/hitl-handler.js';
42
43
  import { resolveTraceId } from '../../common/trace-id.js';
43
44
  import { DEFAULT_GRAPH_TIMEOUT_MS } from '../../common/types.js';
44
45
  import { resolveTaskTimeoutMs } from '../../task/resolve-task-timeout.js';
46
+ import { taskAbortKindOf } from '../../task/task-abort.js';
45
47
  import { runWithExecutionTrace } from '../../graph/execution-trace-context.js';
46
48
  // ─────────────────────────────────────────────
47
49
  // Placeholder Graph DSL (Agent Registry 接入前)
@@ -266,8 +268,8 @@ export class StreamOrchestrator {
266
268
  });
267
269
  const skillInput = resolveSkillInputFromInvokeInput(req.input);
268
270
  const assistantBuckets = createStreamAssistantBuckets();
271
+ const reasoningState = createStreamReasoningState();
269
272
  let toolCallReasoningContent;
270
- let assistantReasoningContent;
271
273
  const pendingToolCalls = [];
272
274
  const toolResults = [];
273
275
  const artifactLinks = [];
@@ -325,18 +327,16 @@ export class StreamOrchestrator {
325
327
  arguments: p.arguments ?? {},
326
328
  });
327
329
  }
328
- if (typeof p.reasoningContent === 'string' && p.reasoningContent) {
329
- toolCallReasoningContent = p.reasoningContent;
330
- }
330
+ toolCallReasoningContent = resolvedStreamReasoning(reasoningState);
331
331
  }
332
332
  appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
333
333
  collectPublicArtifactLinksFromEvent(runtimeEvent.type, runtimeEvent.payload, artifactLinks);
334
- const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
335
- if (streamedReasoning)
336
- assistantReasoningContent = streamedReasoning;
334
+ applyStreamReasoningEvent(reasoningState, runtimeEvent.type, runtimeEvent.payload);
337
335
  applyStreamTextDelta(assistantBuckets, runtimeEvent.type, runtimeEvent.payload);
338
- if (shouldEmitTokenSse(tokenDetail, runtimeEvent.type)) {
339
- emitter.emit(this.runtimeEventToSSE(runtimeEvent, correlationId, taskId));
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
+ }
340
340
  }
341
341
  }
342
342
  }
@@ -369,13 +369,21 @@ export class StreamOrchestrator {
369
369
  this.emitErrorAndClose(emitter, correlationId, taskId, error);
370
370
  return;
371
371
  }
372
- // ─── 错误处理:Timeout ───
373
- if (this.isTimeoutError(error)) {
372
+ const abortKind = taskAbortKindOf(error, abortSignal);
373
+ if (abortKind === 'timeout' || abortKind === 'cancelled') {
374
374
  await recordTaskTokenUsages(this.taskService, taskId, this.tokenStatsService.list(taskId));
375
- await this.taskService.tryUpdateStatus(taskId, 'running', 'timeout');
375
+ if (abortKind === 'timeout') {
376
+ await this.taskService.tryUpdateStatus(taskId, 'running', 'timeout');
377
+ }
378
+ else {
379
+ const moved = await this.taskService.tryUpdateStatus(taskId, 'running', 'cancelled');
380
+ if (!moved) {
381
+ await this.taskService.tryUpdateStatus(taskId, 'waiting_hitl', 'cancelled');
382
+ }
383
+ }
376
384
  emitter.emit(this.buildSSEEvent('task.error', correlationId, taskId, {
377
- code: ErrorCodes.TASK_TIMEOUT,
378
- message: 'Task execution timed out',
385
+ code: abortKind === 'timeout' ? ErrorCodes.TASK_TIMEOUT : 'runtime.task.cancelled',
386
+ message: abortKind === 'timeout' ? 'Task execution timed out' : 'Task cancelled',
379
387
  recoverable: false,
380
388
  }));
381
389
  emitter.close();
@@ -393,12 +401,14 @@ export class StreamOrchestrator {
393
401
  }
394
402
  // ─── Step 7: 发送结束事件序列 ───
395
403
  const tokenStats = this.tokenStatsService.getTaskStats(taskId);
396
- const visible = visibleStreamAssistantText(assistantBuckets);
404
+ const rawVisible = visibleStreamAssistantText(assistantBuckets);
405
+ const visible = correctEmployeeVisibleReply(rawVisible, toolResults);
397
406
  const links = mergeArtifactLinks(artifactLinks, collectPublicArtifactLinks(toolResults));
398
407
  const { injected, suffix } = injectPublicArtifactLinks(visible, links);
399
- if (suffix) {
408
+ const close = employeeStreamClose({ raw: rawVisible, tools: toolResults, suffix });
409
+ for (const content of close.chunks) {
400
410
  emitter.emit(this.buildSSEEvent('response.chunk', correlationId, taskId, {
401
- content: suffix,
411
+ content,
402
412
  format: 'text',
403
413
  }));
404
414
  }
@@ -437,11 +447,11 @@ export class StreamOrchestrator {
437
447
  messages: buildTurnTranscriptFromStreamEvents({
438
448
  pendingToolCalls,
439
449
  toolResults,
440
- assistantTexts: suffix ? undefined : streamAssistantTexts(assistantBuckets),
450
+ assistantTexts: close.persistCorrected ? undefined : streamAssistantTexts(assistantBuckets),
441
451
  assistantText: injected,
442
452
  finishPoints: graph.finishPoints,
443
453
  toolCallReasoningContent,
444
- assistantReasoningContent,
454
+ assistantReasoningContent: resolvedStreamReasoning(reasoningState),
445
455
  }),
446
456
  assistantTokenCount: tokenStats.total.output ?? 0,
447
457
  assistantDurationMs: duration,
@@ -489,7 +499,4 @@ export class StreamOrchestrator {
489
499
  }));
490
500
  emitter.close();
491
501
  }
492
- isTimeoutError(error) {
493
- return error instanceof Error && /timed out/i.test(error.message);
494
- }
495
502
  }
@@ -58,11 +58,12 @@ export interface RuntimeRunContext {
58
58
  /**
59
59
  * Task 执行状态(Runtime 内部使用)
60
60
  *
61
- * 状态流转:pending → running → success | failed | timeout | token_exhausted | waiting_hitl
61
+ * 状态流转:pending → running → success | failed | timeout | token_exhausted | cancelled | waiting_hitl
62
62
  *
63
- * waiting_hitl:等待人工介入确认,可通过 HITL API 恢复为 running
63
+ * waiting_hitl:等待人工介入确认,可通过 HITL API 恢复为 running。
64
+ * cancelled:调用方停止这一轮(Hosting `POST /tasks/:id/cancel`),不是超时。
64
65
  */
65
- export type TaskStatus = 'pending' | 'running' | 'waiting_hitl' | 'success' | 'failed' | 'timeout' | 'token_exhausted';
66
+ export type TaskStatus = 'pending' | 'running' | 'waiting_hitl' | 'success' | 'failed' | 'timeout' | 'token_exhausted' | 'cancelled';
66
67
  /**
67
68
  * Runtime 执行上下文绑定状态(非平台 Session 生命周期)
68
69
  *
@@ -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, buildToolCatalog, loadedToolsToApiTools, resolveLlmPromptInputs, spokenName } from '../../tool/system-prompt-builder.js';
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';
@@ -39,6 +40,81 @@ import { recordSkillEntrySpan, skillEntryInvocationFromTool } from './skill-entr
39
40
  import { llmSpanInput, llmSpanOutputFromResult, withGraphSpan } from '../trace-span.js';
40
41
  import { executionTraceFromState, getExecutionStream, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
41
42
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
43
+ function emitReasoningStartedIfNeeded(live, model, timestamp) {
44
+ if (live.reasoningStartedThisTurn)
45
+ return;
46
+ live.reasoningStartedThisTurn = true;
47
+ live.iteration += 1;
48
+ live.sink({
49
+ type: 'reasoning.started',
50
+ taskId: undefined,
51
+ correlationId: undefined,
52
+ timestamp,
53
+ payload: {
54
+ iterationCount: live.iteration,
55
+ model,
56
+ },
57
+ });
58
+ }
59
+ function nodeGraphExtra(live, nodeId) {
60
+ return {
61
+ node: nodeId,
62
+ ...(live.graphId ? { graphId: live.graphId } : {}),
63
+ };
64
+ }
65
+ function emitLlmStreamDelta(live, nodeId, model, delta) {
66
+ const timestamp = new Date().toISOString();
67
+ const extra = nodeGraphExtra(live, nodeId);
68
+ if (delta.reasoningContent) {
69
+ emitReasoningStartedIfNeeded(live, model, timestamp);
70
+ live.sink({
71
+ type: 'reasoning.chunk',
72
+ taskId: undefined,
73
+ correlationId: undefined,
74
+ timestamp,
75
+ payload: {
76
+ content: delta.reasoningContent,
77
+ index: live.reasoningChunkIndex++,
78
+ ...extra,
79
+ },
80
+ });
81
+ }
82
+ if (delta.content) {
83
+ emitReasoningStartedIfNeeded(live, model, timestamp);
84
+ live.streamedTextThisTurn = true;
85
+ live.sink({
86
+ type: 'response.chunk',
87
+ taskId: undefined,
88
+ correlationId: undefined,
89
+ timestamp,
90
+ payload: {
91
+ content: delta.content,
92
+ index: live.chunkIndex++,
93
+ ...extra,
94
+ },
95
+ });
96
+ }
97
+ }
98
+ function emitReasoningCompletedIfAny(live, nodeId, next) {
99
+ if (!live)
100
+ return;
101
+ const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
102
+ const reasoning = added && typeof added === 'object' && typeof added.reasoningContent === 'string'
103
+ ? added.reasoningContent
104
+ : '';
105
+ if (!reasoning)
106
+ return;
107
+ live.sink({
108
+ type: 'reasoning.completed',
109
+ taskId: undefined,
110
+ correlationId: undefined,
111
+ timestamp: new Date().toISOString(),
112
+ payload: {
113
+ content: reasoning,
114
+ ...nodeGraphExtra(live, nodeId),
115
+ },
116
+ });
117
+ }
42
118
  /**
43
119
  * 传给 LangGraph 的 `recursionLimit`:一轮调度(superstep)+1。
44
120
  * 同一步里并行的多个节点只 +1。ReAct 的 llm 再 tool 大约 +2。
@@ -342,7 +418,7 @@ export class LangGraphAdapter {
342
418
  });
343
419
  }
344
420
  }
345
- if (progress.status === 'started') {
421
+ if (progress.status === 'started' && (!progress.quiet || progress.modelOnly)) {
346
422
  stream.sink({
347
423
  type: 'tool.called',
348
424
  taskId,
@@ -354,19 +430,22 @@ export class LangGraphAdapter {
354
430
  ...(progress.toolset ? { toolset: progress.toolset } : {}),
355
431
  ...(progress.toolVersion ? { toolVersion: progress.toolVersion } : {}),
356
432
  arguments: progress.arguments ?? {},
433
+ ...(progress.modelOnly ? { modelOnly: true } : {}),
357
434
  ...loc,
358
435
  },
359
436
  });
360
437
  }
361
438
  // resolvedPath / skillDirs 只给上面 classifySkillFile;inSkillSubgraph 只给 skill.started 去重。不进对话流。
362
- const { resolvedPath: _resolvedPath, skillDirs: _skillDirs, inSkillSubgraph: _inSkillSubgraph, ...progressView } = progress;
363
- stream.sink({
364
- type: 'tool.progress',
365
- taskId,
366
- correlationId,
367
- timestamp,
368
- payload: progressView,
369
- });
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
+ }
370
449
  if (progress.status === 'completed') {
371
450
  stream.sink({
372
451
  type: 'tool.result',
@@ -381,6 +460,7 @@ export class LangGraphAdapter {
381
460
  success: true,
382
461
  ...(progress.file ? { file: progress.file } : {}),
383
462
  ...(progress.files?.length ? { files: progress.files } : {}),
463
+ ...(progress.modelOnly ? { modelOnly: true } : {}),
384
464
  ...loc,
385
465
  },
386
466
  });
@@ -397,6 +477,7 @@ export class LangGraphAdapter {
397
477
  ...(progress.toolset ? { toolset: progress.toolset } : {}),
398
478
  error: progress.error ?? 'Unknown error',
399
479
  willRetry: progress.willRetry === true,
480
+ ...(progress.modelOnly ? { modelOnly: true } : {}),
400
481
  ...(progress.content ? { content: progress.content } : {}),
401
482
  ...(progress.file ? { file: progress.file } : {}),
402
483
  ...(progress.files?.length ? { files: progress.files } : {}),
@@ -470,6 +551,9 @@ export class LangGraphAdapter {
470
551
  checkpointNs: ephemeral(undefined),
471
552
  skillIsolateContext: ephemeral(undefined),
472
553
  loadedTools: ephemeral(undefined),
554
+ exposedMcpTools: ephemeral(undefined),
555
+ exposedMcpScopeId: ephemeral(undefined),
556
+ sentMcpToolNames: ephemeral(undefined),
473
557
  loadedSkills: ephemeral(undefined),
474
558
  skillDefinitions: ephemeral(undefined),
475
559
  skillInput: ephemeral(undefined),
@@ -596,15 +680,25 @@ export class LangGraphAdapter {
596
680
  sink: enqueue,
597
681
  eventContext: { taskId: state.taskId, correlationId: state.correlationId },
598
682
  chunkIndex: 0,
683
+ reasoningChunkIndex: 0,
599
684
  iteration: 0,
600
685
  reasoningStartedThisTurn: false,
601
686
  streamedTextThisTurn: false,
687
+ graphId: graph.id,
602
688
  };
603
689
  const traceCtx = executionTraceFromState(state);
604
690
  const graphSignal = graphAbortSignal(state);
691
+ let graphIterator;
692
+ const stopGraphIterator = () => {
693
+ const iterator = graphIterator;
694
+ if (!iterator?.return)
695
+ return;
696
+ void iterator.return().catch(() => undefined);
697
+ };
605
698
  const onGraphAbort = () => {
606
699
  failure = graphSignal ? abortReason(graphSignal) : new Error('aborted');
607
700
  finished = true;
701
+ stopGraphIterator();
608
702
  signal();
609
703
  };
610
704
  graphSignal?.addEventListener('abort', onGraphAbort, { once: true });
@@ -619,9 +713,19 @@ export class LangGraphAdapter {
619
713
  const streamIterator = await withGraphAbort(Promise.resolve(ref.compiled.stream(input, langGraphRunOptions({
620
714
  configurable: this.checkpointConfigurable(state, graph),
621
715
  streamMode: 'updates',
716
+ ...(graphSignal ? { signal: graphSignal } : {}),
622
717
  }))), graphSignal);
623
- for await (const update of streamIterator) {
624
- for (const event of this.convertUpdatesToEvents(update, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
718
+ const iterator = streamIterator[Symbol.asyncIterator]();
719
+ graphIterator = iterator;
720
+ while (true) {
721
+ if (graphSignal?.aborted) {
722
+ failure = failure ?? abortReason(graphSignal);
723
+ break;
724
+ }
725
+ const next = await iterator.next();
726
+ if (next.done)
727
+ break;
728
+ for (const event of this.convertUpdatesToEvents(next.value, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
625
729
  enqueue(event);
626
730
  }
627
731
  }
@@ -667,6 +771,7 @@ export class LangGraphAdapter {
667
771
  abortDeadlineMs,
668
772
  }, () => ref.compiled.invoke(input, langGraphRunOptions({
669
773
  configurable: this.checkpointConfigurable(state, graph),
774
+ ...(abortSignal ? { signal: abortSignal } : {}),
670
775
  }))), abortSignal);
671
776
  }
672
777
  catch (error) {
@@ -674,10 +779,8 @@ export class LangGraphAdapter {
674
779
  }
675
780
  const duration = Date.now() - startTime;
676
781
  const messages = Array.isArray(result['messages']) ? result['messages'] : [];
677
- const lastMessage = messages[messages.length - 1];
678
- const output = typeof lastMessage === 'object' && lastMessage !== null && 'content' in lastMessage
679
- ? String(lastMessage.content)
680
- : '';
782
+ const rawOutput = lastAssistantTextSinceLastUser(messages);
783
+ const output = correctEmployeeVisibleReply(rawOutput, toolFactsSinceLastUser(messages));
681
784
  // ── Token 累计:从 __llmTokenUsage 数组累加器读取每轮 LLM 明细 + 汇总
682
785
  // 数组由 channel reducer 自动累加(每次 LLM 节点追加一条)。
683
786
  const llmUsageArray = Array.isArray(result['__llmTokenUsage'])
@@ -702,6 +805,12 @@ export class LangGraphAdapter {
702
805
  : state.messages,
703
806
  userId: state.userId,
704
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,
705
814
  forceToolServer: state.forceToolServer ?? result.forceToolServer,
706
815
  executionContext: state.executionContext ?? result.executionContext,
707
816
  },
@@ -1082,7 +1191,6 @@ export class LangGraphAdapter {
1082
1191
  console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1083
1192
  }
1084
1193
  const catalog = includeSkillCatalog ? buildSkillCatalog(skillDefs) : undefined;
1085
- const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1086
1194
  const jail = resolveJailFromState({
1087
1195
  agentDir: state['agentDir'],
1088
1196
  userId,
@@ -1107,11 +1215,18 @@ export class LangGraphAdapter {
1107
1215
  agentSystemPrompt: state['agentSystemPrompt'],
1108
1216
  nodePrompt: resolvedPrompt,
1109
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);
1110
1225
  const assembled = this.systemPromptBuilder.build({
1111
1226
  agentPrompt: promptInputs.agentPrompt,
1112
1227
  nodePrompt: promptInputs.nodePrompt,
1113
1228
  skillCatalog: catalog,
1114
- toolCatalog,
1229
+ mcpToolIndex: surface.mcpToolIndex,
1115
1230
  agentName,
1116
1231
  agentCode,
1117
1232
  userId,
@@ -1120,7 +1235,10 @@ export class LangGraphAdapter {
1120
1235
  jailRoots,
1121
1236
  skillDirs,
1122
1237
  });
1123
- const apiTools = disableTools ? [] : loadedToolsToApiTools(loadedTools);
1238
+ const apiTools = surface.apiTools;
1239
+ const sentTools = disableTools
1240
+ ? undefined
1241
+ : (apiTools.length > 0 ? apiTools : baseConfig.tools);
1124
1242
  const finalConfig = {
1125
1243
  ...baseConfig,
1126
1244
  systemPrompt: assembled.text,
@@ -1132,7 +1250,7 @@ export class LangGraphAdapter {
1132
1250
  agentCode,
1133
1251
  correlationId: typeof state['correlationId'] === 'string' ? state['correlationId'] : undefined,
1134
1252
  },
1135
- tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
1253
+ tools: sentTools,
1136
1254
  toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
1137
1255
  };
1138
1256
  // 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
@@ -1141,49 +1259,30 @@ export class LangGraphAdapter {
1141
1259
  if (stream) {
1142
1260
  stream.reasoningStartedThisTurn = false;
1143
1261
  stream.streamedTextThisTurn = false;
1262
+ stream.reasoningChunkIndex = 0;
1144
1263
  }
1264
+ const holdEmployeeText = holdEmployeeTextForUnnamedMcp(loadedTools, sentMcpToolNames);
1265
+ const heldDeltas = [];
1145
1266
  const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
1146
- ? (text) => {
1267
+ ? (delta) => {
1147
1268
  const live = getExecutionStream();
1148
- if (!live || !text)
1269
+ if (!live)
1270
+ return;
1271
+ if (holdEmployeeText) {
1272
+ heldDeltas.push(delta);
1149
1273
  return;
1150
- const timestamp = new Date().toISOString();
1151
- if (!live.reasoningStartedThisTurn) {
1152
- live.reasoningStartedThisTurn = true;
1153
- live.iteration += 1;
1154
- live.sink({
1155
- type: 'reasoning.started',
1156
- taskId: undefined,
1157
- correlationId: undefined,
1158
- timestamp,
1159
- payload: {
1160
- iterationCount: live.iteration,
1161
- model: finalConfig.model,
1162
- },
1163
- });
1164
1274
  }
1165
- live.streamedTextThisTurn = true;
1166
- live.sink({
1167
- type: 'response.chunk',
1168
- taskId: undefined,
1169
- correlationId: undefined,
1170
- timestamp,
1171
- payload: {
1172
- content: text,
1173
- index: live.chunkIndex++,
1174
- node: node.id,
1175
- },
1176
- });
1275
+ emitLlmStreamDelta(live, node.id, finalConfig.model, delta);
1177
1276
  }
1178
1277
  : undefined, this.interceptors);
1179
- const toolsForSpan = disableTools || !loadedTools
1180
- ? undefined
1181
- : Object.values(loadedTools).map((t) => ({
1182
- name: t.name,
1183
- ...(t.manifest.description?.trim()
1184
- ? { 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() }
1185
1283
  : {}),
1186
- }));
1284
+ }))
1285
+ : undefined;
1187
1286
  // 组装后再 createNode:input 必须是发给模型的 system/tools/messages;completeNode 不能补 input。
1188
1287
  return withGraphSpan(state, {
1189
1288
  name: node.id,
@@ -1197,9 +1296,31 @@ export class LangGraphAdapter {
1197
1296
  },
1198
1297
  }, async () => {
1199
1298
  const next = await llmNode.execute(state);
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) {
1318
+ emitReasoningCompletedIfAny(getExecutionStream(), node.id, next);
1319
+ }
1200
1320
  const hydratedUpdate = loadedTools && loadedTools !== loadedToolsBase ? { loadedTools } : {};
1321
+ const sentUpdate = { sentMcpToolNames };
1201
1322
  if (!silent)
1202
- return { ...next, ...hydratedUpdate };
1323
+ return { ...next, ...hydratedUpdate, ...sentUpdate };
1203
1324
  const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
1204
1325
  const raw = added && typeof added === 'object' && typeof added.content === 'string'
1205
1326
  ? added.content
@@ -1209,6 +1330,7 @@ export class LangGraphAdapter {
1209
1330
  messages: [],
1210
1331
  silentOutput: raw,
1211
1332
  ...hydratedUpdate,
1333
+ ...sentUpdate,
1212
1334
  };
1213
1335
  }, { outputFrom: llmSpanOutputFromResult });
1214
1336
  };
@@ -1542,23 +1664,17 @@ export class LangGraphAdapter {
1542
1664
  continue;
1543
1665
  }
1544
1666
  const hasText = typeof msg.content === 'string' && msg.content.trim().length > 0;
1545
- if ((hasText || msg.reasoningContent) &&
1546
- (!streamedTextThisTurn || msg.reasoningContent)) {
1547
- if (streamedTextThisTurn && !msg.reasoningContent)
1548
- continue;
1667
+ if (hasText && !streamedTextThisTurn) {
1549
1668
  yield {
1550
1669
  type: 'response.chunk',
1551
1670
  taskId: undefined,
1552
1671
  correlationId: undefined,
1553
1672
  timestamp,
1554
1673
  payload: {
1555
- content: streamedTextThisTurn ? '' : (msg.content ?? ''),
1674
+ content: msg.content ?? '',
1556
1675
  index: nextChunkIndex(),
1557
1676
  node: nodeId,
1558
1677
  graphId,
1559
- ...(msg.reasoningContent
1560
- ? { reasoningContent: msg.reasoningContent }
1561
- : {}),
1562
1678
  },
1563
1679
  };
1564
1680
  }
@@ -1569,26 +1685,8 @@ export class LangGraphAdapter {
1569
1685
  continue;
1570
1686
  if (subgraphNodeIds.has(nodeId) && streamedTextThisTurn)
1571
1687
  continue;
1572
- // 已通过 onChunk 推送过 token,避免整段重复
1573
- if (streamedTextThisTurn) {
1574
- // token 已流过:仍补发一条带 reasoning 的标记 chunk(空 content),供落库回传
1575
- if (msg.reasoningContent) {
1576
- yield {
1577
- type: 'response.chunk',
1578
- taskId: undefined,
1579
- correlationId: undefined,
1580
- timestamp,
1581
- payload: {
1582
- content: '',
1583
- index: nextChunkIndex(),
1584
- node: nodeId,
1585
- graphId,
1586
- reasoningContent: msg.reasoningContent,
1587
- },
1588
- };
1589
- }
1688
+ if (streamedTextThisTurn)
1590
1689
  continue;
1591
- }
1592
1690
  yield {
1593
1691
  type: 'response.chunk',
1594
1692
  taskId: undefined,
@@ -1599,9 +1697,6 @@ export class LangGraphAdapter {
1599
1697
  index: nextChunkIndex(),
1600
1698
  node: nodeId,
1601
1699
  graphId,
1602
- ...(msg.reasoningContent
1603
- ? { reasoningContent: msg.reasoningContent }
1604
- : {}),
1605
1700
  },
1606
1701
  };
1607
1702
  continue;