@gt-fe/eap-runtime 0.0.33 → 0.0.34

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 (34) 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 +19 -17
  5. package/dist/common/types.d.ts +4 -3
  6. package/dist/graph/adapters/langgraph.adapter.js +109 -64
  7. package/dist/graph/execution-trace-context.d.ts +3 -0
  8. package/dist/graph/nodes/llm-node.d.ts +7 -2
  9. package/dist/graph/nodes/llm-node.js +8 -3
  10. package/dist/providers/http-llm-provider.js +27 -10
  11. package/dist/runtime/agent-runtime.d.ts +7 -0
  12. package/dist/runtime/agent-runtime.js +59 -37
  13. package/dist/runtime/types.d.ts +1 -1
  14. package/dist/session/persist-turn-messages.d.ts +10 -2
  15. package/dist/session/persist-turn-messages.js +35 -10
  16. package/dist/sse/events/index.d.ts +6 -4
  17. package/dist/sse/events/index.js +3 -1
  18. package/dist/sse/events/reasoning-events.d.ts +24 -2
  19. package/dist/sse/events/reasoning-events.js +3 -1
  20. package/dist/sse/events/skill-events.d.ts +2 -0
  21. package/dist/task/contracts/task-manager.contract.d.ts +2 -2
  22. package/dist/task/contracts/task-manager.contract.js +1 -1
  23. package/dist/task/index.d.ts +2 -0
  24. package/dist/task/index.js +1 -0
  25. package/dist/task/task-abort.d.ts +14 -0
  26. package/dist/task/task-abort.js +33 -0
  27. package/dist/task/task-timeout.d.ts +11 -0
  28. package/dist/task/task-timeout.js +40 -4
  29. package/dist/task/task.service.js +11 -5
  30. package/dist/tool/index.d.ts +1 -1
  31. package/dist/tool/index.js +1 -1
  32. package/dist/tool/system-prompt-builder.d.ts +5 -9
  33. package/dist/tool/system-prompt-builder.js +13 -31
  34. 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,7 +19,7 @@
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
24
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
25
25
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
@@ -42,6 +42,7 @@ import { buildHitlWaitedPayload } from '../../graph/hitl/hitl-handler.js';
42
42
  import { resolveTraceId } from '../../common/trace-id.js';
43
43
  import { DEFAULT_GRAPH_TIMEOUT_MS } from '../../common/types.js';
44
44
  import { resolveTaskTimeoutMs } from '../../task/resolve-task-timeout.js';
45
+ import { taskAbortKindOf } from '../../task/task-abort.js';
45
46
  import { runWithExecutionTrace } from '../../graph/execution-trace-context.js';
46
47
  // ─────────────────────────────────────────────
47
48
  // Placeholder Graph DSL (Agent Registry 接入前)
@@ -266,8 +267,8 @@ export class StreamOrchestrator {
266
267
  });
267
268
  const skillInput = resolveSkillInputFromInvokeInput(req.input);
268
269
  const assistantBuckets = createStreamAssistantBuckets();
270
+ const reasoningState = createStreamReasoningState();
269
271
  let toolCallReasoningContent;
270
- let assistantReasoningContent;
271
272
  const pendingToolCalls = [];
272
273
  const toolResults = [];
273
274
  const artifactLinks = [];
@@ -325,15 +326,11 @@ export class StreamOrchestrator {
325
326
  arguments: p.arguments ?? {},
326
327
  });
327
328
  }
328
- if (typeof p.reasoningContent === 'string' && p.reasoningContent) {
329
- toolCallReasoningContent = p.reasoningContent;
330
- }
329
+ toolCallReasoningContent = resolvedStreamReasoning(reasoningState);
331
330
  }
332
331
  appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
333
332
  collectPublicArtifactLinksFromEvent(runtimeEvent.type, runtimeEvent.payload, artifactLinks);
334
- const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
335
- if (streamedReasoning)
336
- assistantReasoningContent = streamedReasoning;
333
+ applyStreamReasoningEvent(reasoningState, runtimeEvent.type, runtimeEvent.payload);
337
334
  applyStreamTextDelta(assistantBuckets, runtimeEvent.type, runtimeEvent.payload);
338
335
  if (shouldEmitTokenSse(tokenDetail, runtimeEvent.type)) {
339
336
  emitter.emit(this.runtimeEventToSSE(runtimeEvent, correlationId, taskId));
@@ -369,13 +366,21 @@ export class StreamOrchestrator {
369
366
  this.emitErrorAndClose(emitter, correlationId, taskId, error);
370
367
  return;
371
368
  }
372
- // ─── 错误处理:Timeout ───
373
- if (this.isTimeoutError(error)) {
369
+ const abortKind = taskAbortKindOf(error, abortSignal);
370
+ if (abortKind === 'timeout' || abortKind === 'cancelled') {
374
371
  await recordTaskTokenUsages(this.taskService, taskId, this.tokenStatsService.list(taskId));
375
- await this.taskService.tryUpdateStatus(taskId, 'running', 'timeout');
372
+ if (abortKind === 'timeout') {
373
+ await this.taskService.tryUpdateStatus(taskId, 'running', 'timeout');
374
+ }
375
+ else {
376
+ const moved = await this.taskService.tryUpdateStatus(taskId, 'running', 'cancelled');
377
+ if (!moved) {
378
+ await this.taskService.tryUpdateStatus(taskId, 'waiting_hitl', 'cancelled');
379
+ }
380
+ }
376
381
  emitter.emit(this.buildSSEEvent('task.error', correlationId, taskId, {
377
- code: ErrorCodes.TASK_TIMEOUT,
378
- message: 'Task execution timed out',
382
+ code: abortKind === 'timeout' ? ErrorCodes.TASK_TIMEOUT : 'runtime.task.cancelled',
383
+ message: abortKind === 'timeout' ? 'Task execution timed out' : 'Task cancelled',
379
384
  recoverable: false,
380
385
  }));
381
386
  emitter.close();
@@ -441,7 +446,7 @@ export class StreamOrchestrator {
441
446
  assistantText: injected,
442
447
  finishPoints: graph.finishPoints,
443
448
  toolCallReasoningContent,
444
- assistantReasoningContent,
449
+ assistantReasoningContent: resolvedStreamReasoning(reasoningState),
445
450
  }),
446
451
  assistantTokenCount: tokenStats.total.output ?? 0,
447
452
  assistantDurationMs: duration,
@@ -489,7 +494,4 @@ export class StreamOrchestrator {
489
494
  }));
490
495
  emitter.close();
491
496
  }
492
- isTimeoutError(error) {
493
- return error instanceof Error && /timed out/i.test(error.message);
494
- }
495
497
  }
@@ -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,7 @@ 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, loadedToolsToApiTools, resolveLlmPromptInputs, spokenName } from '../../tool/system-prompt-builder.js';
27
27
  import { resolveJailFromState, resolveJailRoots } from '../../tool/harness-primitives.js';
28
28
  import os from 'node:os';
29
29
  import { bindSelectedSkill } from '../nodes/skill-bind-node.js';
@@ -39,6 +39,81 @@ import { recordSkillEntrySpan, skillEntryInvocationFromTool } from './skill-entr
39
39
  import { llmSpanInput, llmSpanOutputFromResult, withGraphSpan } from '../trace-span.js';
40
40
  import { executionTraceFromState, getExecutionStream, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
41
41
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
42
+ function emitReasoningStartedIfNeeded(live, model, timestamp) {
43
+ if (live.reasoningStartedThisTurn)
44
+ return;
45
+ live.reasoningStartedThisTurn = true;
46
+ live.iteration += 1;
47
+ live.sink({
48
+ type: 'reasoning.started',
49
+ taskId: undefined,
50
+ correlationId: undefined,
51
+ timestamp,
52
+ payload: {
53
+ iterationCount: live.iteration,
54
+ model,
55
+ },
56
+ });
57
+ }
58
+ function nodeGraphExtra(live, nodeId) {
59
+ return {
60
+ node: nodeId,
61
+ ...(live.graphId ? { graphId: live.graphId } : {}),
62
+ };
63
+ }
64
+ function emitLlmStreamDelta(live, nodeId, model, delta) {
65
+ const timestamp = new Date().toISOString();
66
+ const extra = nodeGraphExtra(live, nodeId);
67
+ if (delta.reasoningContent) {
68
+ emitReasoningStartedIfNeeded(live, model, timestamp);
69
+ live.sink({
70
+ type: 'reasoning.chunk',
71
+ taskId: undefined,
72
+ correlationId: undefined,
73
+ timestamp,
74
+ payload: {
75
+ content: delta.reasoningContent,
76
+ index: live.reasoningChunkIndex++,
77
+ ...extra,
78
+ },
79
+ });
80
+ }
81
+ if (delta.content) {
82
+ emitReasoningStartedIfNeeded(live, model, timestamp);
83
+ live.streamedTextThisTurn = true;
84
+ live.sink({
85
+ type: 'response.chunk',
86
+ taskId: undefined,
87
+ correlationId: undefined,
88
+ timestamp,
89
+ payload: {
90
+ content: delta.content,
91
+ index: live.chunkIndex++,
92
+ ...extra,
93
+ },
94
+ });
95
+ }
96
+ }
97
+ function emitReasoningCompletedIfAny(live, nodeId, next) {
98
+ if (!live)
99
+ return;
100
+ const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
101
+ const reasoning = added && typeof added === 'object' && typeof added.reasoningContent === 'string'
102
+ ? added.reasoningContent
103
+ : '';
104
+ if (!reasoning)
105
+ return;
106
+ live.sink({
107
+ type: 'reasoning.completed',
108
+ taskId: undefined,
109
+ correlationId: undefined,
110
+ timestamp: new Date().toISOString(),
111
+ payload: {
112
+ content: reasoning,
113
+ ...nodeGraphExtra(live, nodeId),
114
+ },
115
+ });
116
+ }
42
117
  /**
43
118
  * 传给 LangGraph 的 `recursionLimit`:一轮调度(superstep)+1。
44
119
  * 同一步里并行的多个节点只 +1。ReAct 的 llm 再 tool 大约 +2。
@@ -596,15 +671,25 @@ export class LangGraphAdapter {
596
671
  sink: enqueue,
597
672
  eventContext: { taskId: state.taskId, correlationId: state.correlationId },
598
673
  chunkIndex: 0,
674
+ reasoningChunkIndex: 0,
599
675
  iteration: 0,
600
676
  reasoningStartedThisTurn: false,
601
677
  streamedTextThisTurn: false,
678
+ graphId: graph.id,
602
679
  };
603
680
  const traceCtx = executionTraceFromState(state);
604
681
  const graphSignal = graphAbortSignal(state);
682
+ let graphIterator;
683
+ const stopGraphIterator = () => {
684
+ const iterator = graphIterator;
685
+ if (!iterator?.return)
686
+ return;
687
+ void iterator.return().catch(() => undefined);
688
+ };
605
689
  const onGraphAbort = () => {
606
690
  failure = graphSignal ? abortReason(graphSignal) : new Error('aborted');
607
691
  finished = true;
692
+ stopGraphIterator();
608
693
  signal();
609
694
  };
610
695
  graphSignal?.addEventListener('abort', onGraphAbort, { once: true });
@@ -619,9 +704,19 @@ export class LangGraphAdapter {
619
704
  const streamIterator = await withGraphAbort(Promise.resolve(ref.compiled.stream(input, langGraphRunOptions({
620
705
  configurable: this.checkpointConfigurable(state, graph),
621
706
  streamMode: 'updates',
707
+ ...(graphSignal ? { signal: graphSignal } : {}),
622
708
  }))), graphSignal);
623
- for await (const update of streamIterator) {
624
- for (const event of this.convertUpdatesToEvents(update, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
709
+ const iterator = streamIterator[Symbol.asyncIterator]();
710
+ graphIterator = iterator;
711
+ while (true) {
712
+ if (graphSignal?.aborted) {
713
+ failure = failure ?? abortReason(graphSignal);
714
+ break;
715
+ }
716
+ const next = await iterator.next();
717
+ if (next.done)
718
+ break;
719
+ for (const event of this.convertUpdatesToEvents(next.value, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
625
720
  enqueue(event);
626
721
  }
627
722
  }
@@ -667,6 +762,7 @@ export class LangGraphAdapter {
667
762
  abortDeadlineMs,
668
763
  }, () => ref.compiled.invoke(input, langGraphRunOptions({
669
764
  configurable: this.checkpointConfigurable(state, graph),
765
+ ...(abortSignal ? { signal: abortSignal } : {}),
670
766
  }))), abortSignal);
671
767
  }
672
768
  catch (error) {
@@ -1082,7 +1178,6 @@ export class LangGraphAdapter {
1082
1178
  console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1083
1179
  }
1084
1180
  const catalog = includeSkillCatalog ? buildSkillCatalog(skillDefs) : undefined;
1085
- const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1086
1181
  const jail = resolveJailFromState({
1087
1182
  agentDir: state['agentDir'],
1088
1183
  userId,
@@ -1111,7 +1206,6 @@ export class LangGraphAdapter {
1111
1206
  agentPrompt: promptInputs.agentPrompt,
1112
1207
  nodePrompt: promptInputs.nodePrompt,
1113
1208
  skillCatalog: catalog,
1114
- toolCatalog,
1115
1209
  agentName,
1116
1210
  agentCode,
1117
1211
  userId,
@@ -1141,39 +1235,14 @@ export class LangGraphAdapter {
1141
1235
  if (stream) {
1142
1236
  stream.reasoningStartedThisTurn = false;
1143
1237
  stream.streamedTextThisTurn = false;
1238
+ stream.reasoningChunkIndex = 0;
1144
1239
  }
1145
1240
  const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
1146
- ? (text) => {
1241
+ ? (delta) => {
1147
1242
  const live = getExecutionStream();
1148
- if (!live || !text)
1243
+ if (!live)
1149
1244
  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
- }
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
- });
1245
+ emitLlmStreamDelta(live, node.id, finalConfig.model, delta);
1177
1246
  }
1178
1247
  : undefined, this.interceptors);
1179
1248
  const toolsForSpan = disableTools || !loadedTools
@@ -1197,6 +1266,9 @@ export class LangGraphAdapter {
1197
1266
  },
1198
1267
  }, async () => {
1199
1268
  const next = await llmNode.execute(state);
1269
+ if (enableStream) {
1270
+ emitReasoningCompletedIfAny(getExecutionStream(), node.id, next);
1271
+ }
1200
1272
  const hydratedUpdate = loadedTools && loadedTools !== loadedToolsBase ? { loadedTools } : {};
1201
1273
  if (!silent)
1202
1274
  return { ...next, ...hydratedUpdate };
@@ -1542,23 +1614,17 @@ export class LangGraphAdapter {
1542
1614
  continue;
1543
1615
  }
1544
1616
  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;
1617
+ if (hasText && !streamedTextThisTurn) {
1549
1618
  yield {
1550
1619
  type: 'response.chunk',
1551
1620
  taskId: undefined,
1552
1621
  correlationId: undefined,
1553
1622
  timestamp,
1554
1623
  payload: {
1555
- content: streamedTextThisTurn ? '' : (msg.content ?? ''),
1624
+ content: msg.content ?? '',
1556
1625
  index: nextChunkIndex(),
1557
1626
  node: nodeId,
1558
1627
  graphId,
1559
- ...(msg.reasoningContent
1560
- ? { reasoningContent: msg.reasoningContent }
1561
- : {}),
1562
1628
  },
1563
1629
  };
1564
1630
  }
@@ -1569,26 +1635,8 @@ export class LangGraphAdapter {
1569
1635
  continue;
1570
1636
  if (subgraphNodeIds.has(nodeId) && streamedTextThisTurn)
1571
1637
  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
- }
1638
+ if (streamedTextThisTurn)
1590
1639
  continue;
1591
- }
1592
1640
  yield {
1593
1641
  type: 'response.chunk',
1594
1642
  taskId: undefined,
@@ -1599,9 +1647,6 @@ export class LangGraphAdapter {
1599
1647
  index: nextChunkIndex(),
1600
1648
  node: nodeId,
1601
1649
  graphId,
1602
- ...(msg.reasoningContent
1603
- ? { reasoningContent: msg.reasoningContent }
1604
- : {}),
1605
1650
  },
1606
1651
  };
1607
1652
  continue;
@@ -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
  *
@@ -52,7 +52,7 @@ export class LLMNode {
52
52
  * @param config - LLM 节点配置
53
53
  * @param provider - LLM 调用提供者
54
54
  * @param streaming - 是否启用流式输出(默认 false)
55
- * @param onChunk - 流式模式下每个文本片段回调(用于 SSE response.chunk)
55
+ * @param onChunk - 流式旁路:思考走 reasoningContent,正文走 content
56
56
  */
57
57
  constructor(nodeId, config, provider, streaming = false, onChunk, interceptors) {
58
58
  this.interceptors = interceptors;
@@ -247,8 +247,13 @@ export class LLMNode {
247
247
  let toolCalls;
248
248
  const iterator = this.provider.stream(messages, this.config);
249
249
  for await (const chunk of iterator) {
250
- if (chunk.content && this.onChunk) {
251
- this.onChunk(chunk.content);
250
+ const content = chunk.content || '';
251
+ const reasoningContent = chunk.reasoningContent || '';
252
+ if (this.onChunk && (content || reasoningContent)) {
253
+ this.onChunk({
254
+ ...(content ? { content } : {}),
255
+ ...(reasoningContent ? { reasoningContent } : {}),
256
+ });
252
257
  }
253
258
  chunks.push(chunk.content);
254
259
  if (chunk.reasoningContent) {
@@ -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)
@@ -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 字段可能丢失) */