@gt-fe/eap-runtime 0.0.22 → 0.0.24

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 (50) hide show
  1. package/dist/api/handlers/invoke.handler.d.ts +4 -0
  2. package/dist/api/handlers/invoke.handler.js +38 -19
  3. package/dist/api/handlers/stream.handler.d.ts +4 -0
  4. package/dist/api/handlers/stream.handler.js +24 -15
  5. package/dist/api/routes/agent.routes.js +6 -5
  6. package/dist/checkpoint/checkpoint-ref-notify.js +1 -1
  7. package/dist/common/types.d.ts +6 -2
  8. package/dist/common/types.js +3 -1
  9. package/dist/graph/adapters/langgraph.adapter.d.ts +6 -4
  10. package/dist/graph/adapters/langgraph.adapter.js +103 -18
  11. package/dist/graph/contracts/executable-graph.d.ts +9 -0
  12. package/dist/graph/execution-trace-context.d.ts +4 -0
  13. package/dist/graph/execution-trace-context.js +3 -1
  14. package/dist/graph/nodes/rag/types.d.ts +4 -1
  15. package/dist/graph/nodes/tool-node.js +10 -1
  16. package/dist/providers/http-llm-provider.js +25 -0
  17. package/dist/registry/graph-loader.js +65 -9
  18. package/dist/runtime/agent-runtime.d.ts +3 -0
  19. package/dist/runtime/agent-runtime.js +95 -15
  20. package/dist/runtime/dev-runtime.js +9 -2
  21. package/dist/runtime/types.d.ts +1 -1
  22. package/dist/sse/events/response-events.d.ts +2 -2
  23. package/dist/sse/events/response-events.js +1 -1
  24. package/dist/task/contracts/task-manager.contract.d.ts +2 -0
  25. package/dist/task/index.d.ts +1 -0
  26. package/dist/task/index.js +1 -0
  27. package/dist/task/resolve-task-timeout.d.ts +6 -0
  28. package/dist/task/resolve-task-timeout.js +16 -0
  29. package/dist/task/task-timeout.d.ts +1 -1
  30. package/dist/task/task-timeout.js +3 -2
  31. package/dist/task/task.service.js +1 -0
  32. package/dist/tool/http-tool-executor.js +7 -3
  33. package/dist/tool/index.d.ts +2 -1
  34. package/dist/tool/index.js +2 -1
  35. package/dist/tool/loaded-tool-definition.d.ts +1 -1
  36. package/dist/tool/local-mcp-client.d.ts +1 -1
  37. package/dist/tool/local-mcp-client.js +2 -39
  38. package/dist/tool/local-tool-executor.js +32 -5
  39. package/dist/tool/mcp-list.d.ts +21 -0
  40. package/dist/tool/mcp-list.js +49 -3
  41. package/dist/tool/mcp-tool-executor.d.ts +1 -1
  42. package/dist/tool/mcp-tool-executor.js +58 -22
  43. package/dist/tool/model-visible-tool-content.d.ts +16 -0
  44. package/dist/tool/model-visible-tool-content.js +266 -0
  45. package/dist/tool/timeout.d.ts +5 -12
  46. package/dist/tool/timeout.js +12 -34
  47. package/dist/tool/tool-executor.interface.d.ts +7 -1
  48. package/dist/tool/tool-orchestrator.js +6 -1
  49. package/dist/tool/tool-result-injector.js +2 -1
  50. package/package.json +2 -2
@@ -22,6 +22,7 @@ import type { TokenStatsService } from '../../token/token-stats.service.js';
22
22
  import type { TokenBudgetGuard } from '../../token/token-budget-guard.js';
23
23
  import type { SessionBudgetChecker } from '../../token/session-budget-checker.js';
24
24
  import type { InvokeRequest, InvokeResponse } from '../routes/agent.routes.js';
25
+ import type { TaskTimeoutManager } from '../../task/task-timeout.js';
25
26
  import { type GraphLoader } from '../../registry/graph-loader.js';
26
27
  import type { ICheckpointSaver } from '../../checkpoint/contracts/checkpoint-saver.contract.js';
27
28
  import type { ITraceCollector } from '../../contracts/trace-collector.js';
@@ -46,6 +47,8 @@ export interface InvokeOrchestratorDeps {
46
47
  traceCollector?: ITraceCollector;
47
48
  sessionCompression?: SessionCompressionOptions;
48
49
  sessionMemory?: SessionMemoryOptions;
50
+ /** 未注入时不启动整图超时(测试桩) */
51
+ timeoutManager?: TaskTimeoutManager;
49
52
  }
50
53
  /**
51
54
  * InvokeOrchestrator — 同步调用全流程编排
@@ -66,6 +69,7 @@ export declare class InvokeOrchestrator {
66
69
  private readonly traceCollector?;
67
70
  private readonly sessionCompression?;
68
71
  private readonly sessionMemory?;
72
+ private readonly timeoutManager?;
69
73
  constructor(deps: InvokeOrchestratorDeps);
70
74
  /**
71
75
  * 执行同步调用编排
@@ -21,6 +21,11 @@ import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
21
21
  import { recordTaskTokenUsages, tokenUsagesFromLlmChannel, } from '../../token/record-task-token-usage.js';
22
22
  import { resolveExecutionContext } from '../../governance/governance-client.js';
23
23
  import { resolveSkillInputFromInvokeInput } from '../../registry/skill-io-guard.js';
24
+ import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
25
+ import { classifyRuntimeError } from '../../utils/http-error.js';
26
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../../common/types.js';
27
+ import { resolveTaskTimeoutMs } from '../../task/resolve-task-timeout.js';
28
+ import { runWithExecutionTrace } from '../../graph/execution-trace-context.js';
24
29
  import { resolveAgentSystemPromptFromLoader } from '../../registry/graph-loader.js';
25
30
  import { toLoadedToolMap } from '../../tool/loaded-tool-definition.js';
26
31
  import { toLoadedSkillMap } from '../../tool/system-prompt-builder.js';
@@ -44,7 +49,7 @@ const DEFAULT_GRAPH_DSL = {
44
49
  { from: '__start__', to: 'llm' },
45
50
  { from: 'llm', to: '__end__' },
46
51
  ],
47
- config: { timeout: 60000 },
52
+ config: { timeout: DEFAULT_GRAPH_TIMEOUT_MS },
48
53
  };
49
54
  // ─────────────────────────────────────────────
50
55
  // InvokeOrchestrator
@@ -68,6 +73,7 @@ export class InvokeOrchestrator {
68
73
  traceCollector;
69
74
  sessionCompression;
70
75
  sessionMemory;
76
+ timeoutManager;
71
77
  constructor(deps) {
72
78
  this.sessionContextService = deps.sessionContextService;
73
79
  this.taskService = deps.taskService;
@@ -81,6 +87,7 @@ export class InvokeOrchestrator {
81
87
  this.traceCollector = deps.traceCollector;
82
88
  this.sessionCompression = deps.sessionCompression;
83
89
  this.sessionMemory = deps.sessionMemory;
90
+ this.timeoutManager = deps.timeoutManager;
84
91
  }
85
92
  /**
86
93
  * 执行同步调用编排
@@ -143,8 +150,12 @@ export class InvokeOrchestrator {
143
150
  throw error;
144
151
  }
145
152
  }
146
- // ─── Step 3: 创建 Task(Hosting 传入 taskId 时复用)───
147
- const timeout = req.config?.timeout ?? 60000;
153
+ // ─── Step 3: 加载图并创建 Task(Hosting 传入 taskId 时复用)───
154
+ const loadOpts = { agentVersion: req.agentVersion };
155
+ const graphDsl = this.graphLoader
156
+ ? await this.graphLoader.loadGraph(agentCode, loadOpts)
157
+ : DEFAULT_GRAPH_DSL;
158
+ const timeout = resolveTaskTimeoutMs(req.config?.timeout, graphDsl.config?.timeout);
148
159
  const task = await this.taskService.create({
149
160
  id: req.taskId,
150
161
  agentId: agentCode,
@@ -156,6 +167,9 @@ export class InvokeOrchestrator {
156
167
  });
157
168
  // ─── Step 4: 启动 Task (pending → running) ───
158
169
  await this.taskService.updateStatus(task.id, 'running');
170
+ const abortSignal = this.timeoutManager?.startTimer(task.id, timeout, async () => {
171
+ await this.taskService.tryUpdateStatus(task.id, 'running', 'timeout', 'Task execution timed out');
172
+ });
159
173
  // ─── Step 4b: 持久化本轮 user(多轮短期记忆)───
160
174
  await persistUserTurnMessage(this.messageService, {
161
175
  sessionId,
@@ -168,10 +182,6 @@ export class InvokeOrchestrator {
168
182
  let finishPoints;
169
183
  try {
170
184
  // 构建图 — 每次请求创建独立 StateGraph 实例,确保请求间无共享可变状态
171
- const loadOpts = { agentVersion: req.agentVersion };
172
- const graphDsl = this.graphLoader
173
- ? await this.graphLoader.loadGraph(agentCode, loadOpts)
174
- : DEFAULT_GRAPH_DSL;
175
185
  const graph = await this.graphEngine.buildGraph(graphDsl);
176
186
  finishPoints = graph.finishPoints;
177
187
  const loadedToolsBase = this.graphLoader
@@ -216,7 +226,7 @@ export class InvokeOrchestrator {
216
226
  });
217
227
  const skillInput = resolveSkillInputFromInvokeInput(req.input);
218
228
  // 同步执行图
219
- graphResult = await this.graphEngine.invoke(graph, {
229
+ graphResult = await runWithExecutionTrace({ abortSignal }, () => this.graphEngine.invoke(graph, {
220
230
  messages,
221
231
  userId: effectiveUserId,
222
232
  delegationToken: ctx.delegationToken,
@@ -238,7 +248,9 @@ export class InvokeOrchestrator {
238
248
  agentSystemPrompt,
239
249
  traceId: resolveTraceId(req),
240
250
  traceCollector: this.traceCollector,
241
- });
251
+ abortSignal,
252
+ abortDeadlineMs: Date.now() + timeout,
253
+ }));
242
254
  }
243
255
  catch (error) {
244
256
  const usages = await this.persistInvokeUsages(task.id, sessionId);
@@ -286,7 +298,7 @@ export class InvokeOrchestrator {
286
298
  }
287
299
  // ─── 错误处理:Timeout ───
288
300
  if (this.isTimeoutError(error)) {
289
- await this.taskService.updateStatus(task.id, 'timeout');
301
+ await this.taskService.tryUpdateStatus(task.id, 'running', 'timeout');
290
302
  return this.buildResponse(traceId, {
291
303
  taskId: task.id,
292
304
  sessionId,
@@ -297,7 +309,9 @@ export class InvokeOrchestrator {
297
309
  });
298
310
  }
299
311
  // ─── 错误处理:通用错误 ───
300
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
312
+ const classified = classifyRuntimeError(error);
313
+ const fuse = classified.code === ErrorCodes.GRAPH_MAX_ITERATIONS ? classified : undefined;
314
+ const errorMessage = fuse?.message ?? (error instanceof Error ? error.message : 'Unknown error');
301
315
  await this.taskService.updateStatus(task.id, 'failed', errorMessage);
302
316
  return this.buildErrorResponse(traceId, {
303
317
  taskId: task.id,
@@ -306,7 +320,10 @@ export class InvokeOrchestrator {
306
320
  tokenUsage,
307
321
  duration: Date.now() - startTime,
308
322
  status: 'failed',
309
- });
323
+ }, fuse ?? error);
324
+ }
325
+ finally {
326
+ this.timeoutManager?.clearTimer(task.id);
310
327
  }
311
328
  // ─── Step 6: 记录 Token 消耗 ───
312
329
  //
@@ -387,15 +404,17 @@ export class InvokeOrchestrator {
387
404
  buildResponse(traceId, data) {
388
405
  return { code: 'ok', data, traceId };
389
406
  }
390
- buildErrorResponse(traceId, data) {
391
- return { code: data.status === 'token_exhausted' ? 'runtime.budget.task_exhausted' : 'runtime.graph.execution_error', data, traceId };
407
+ buildErrorResponse(traceId, data, error) {
408
+ if (data.status === 'token_exhausted') {
409
+ return { code: 'runtime.budget.task_exhausted', data, traceId };
410
+ }
411
+ if (error instanceof AppError) {
412
+ return { code: error.code, data, traceId };
413
+ }
414
+ return { code: 'runtime.graph.execution_error', data, traceId };
392
415
  }
393
416
  isTimeoutError(error) {
394
- if (error instanceof Error) {
395
- const msg = error.message.toLowerCase();
396
- return msg.includes('timeout') || msg.includes('timed out');
397
- }
398
- return false;
417
+ return error instanceof Error && /timed out/i.test(error.message);
399
418
  }
400
419
  async persistInvokeUsages(taskId, sessionId) {
401
420
  if (!this.checkpointSaver)
@@ -33,6 +33,7 @@ import type { StreamRequest } from '../routes/agent.routes.js';
33
33
  import { type GraphLoader } from '../../registry/graph-loader.js';
34
34
  import type { ICheckpointSaver } from '../../checkpoint/contracts/checkpoint-saver.contract.js';
35
35
  import type { ITraceCollector } from '../../contracts/trace-collector.js';
36
+ import type { TaskTimeoutManager } from '../../task/task-timeout.js';
36
37
  /**
37
38
  * StreamOrchestrator 构造器依赖
38
39
  */
@@ -52,6 +53,8 @@ export interface StreamOrchestratorDeps {
52
53
  traceCollector?: ITraceCollector;
53
54
  sessionCompression?: SessionCompressionOptions;
54
55
  sessionMemory?: SessionMemoryOptions;
56
+ /** 未注入时不启动整图超时(测试桩) */
57
+ timeoutManager?: TaskTimeoutManager;
55
58
  }
56
59
  /**
57
60
  * StreamOrchestrator — 流式调用全流程编排
@@ -72,6 +75,7 @@ export declare class StreamOrchestrator {
72
75
  private readonly traceCollector?;
73
76
  private readonly sessionCompression?;
74
77
  private readonly sessionMemory?;
78
+ private readonly timeoutManager?;
75
79
  constructor(deps: StreamOrchestratorDeps);
76
80
  /**
77
81
  * 执行流式调用编排
@@ -39,6 +39,9 @@ import { resolveAgentCode } from '../../common/agent-code.js';
39
39
  import { unwrapHitlPaused } from './invoke.handler.js';
40
40
  import { buildHitlWaitedPayload } from '../../graph/hitl/hitl-handler.js';
41
41
  import { resolveTraceId } from '../../common/trace-id.js';
42
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../../common/types.js';
43
+ import { resolveTaskTimeoutMs } from '../../task/resolve-task-timeout.js';
44
+ import { runWithExecutionTrace } from '../../graph/execution-trace-context.js';
42
45
  // ─────────────────────────────────────────────
43
46
  // Placeholder Graph DSL (Agent Registry 接入前)
44
47
  // ─────────────────────────────────────────────
@@ -53,7 +56,7 @@ const DEFAULT_GRAPH_DSL = {
53
56
  { from: '__start__', to: 'llm' },
54
57
  { from: 'llm', to: '__end__' },
55
58
  ],
56
- config: { timeout: 60000 },
59
+ config: { timeout: DEFAULT_GRAPH_TIMEOUT_MS },
57
60
  };
58
61
  // ─────────────────────────────────────────────
59
62
  // StreamOrchestrator
@@ -77,6 +80,7 @@ export class StreamOrchestrator {
77
80
  traceCollector;
78
81
  sessionCompression;
79
82
  sessionMemory;
83
+ timeoutManager;
80
84
  constructor(deps) {
81
85
  this.sessionContextService = deps.sessionContextService;
82
86
  this.taskService = deps.taskService;
@@ -90,6 +94,7 @@ export class StreamOrchestrator {
90
94
  this.traceCollector = deps.traceCollector;
91
95
  this.sessionCompression = deps.sessionCompression;
92
96
  this.sessionMemory = deps.sessionMemory;
97
+ this.timeoutManager = deps.timeoutManager;
93
98
  }
94
99
  /**
95
100
  * 执行流式调用编排
@@ -166,8 +171,12 @@ export class StreamOrchestrator {
166
171
  userId,
167
172
  }));
168
173
  }
169
- // ─── Step 3: 创建 Task(Hosting 传入 taskId 时复用)───
170
- const timeout = req.config?.timeout ?? 60000;
174
+ // ─── Step 3: 加载图并创建 Task(Hosting 传入 taskId 时复用)───
175
+ const loadOpts = { agentVersion: req.agentVersion };
176
+ const graphDsl = this.graphLoader
177
+ ? await this.graphLoader.loadGraph(agentCode, loadOpts)
178
+ : DEFAULT_GRAPH_DSL;
179
+ const timeout = resolveTaskTimeoutMs(req.config?.timeout, graphDsl.config?.timeout);
171
180
  const task = await this.taskService.create({
172
181
  id: req.taskId,
173
182
  agentId: agentCode,
@@ -180,6 +189,9 @@ export class StreamOrchestrator {
180
189
  const taskId = task.id;
181
190
  // ─── Step 4: 启动 Task (pending → running) ───
182
191
  await this.taskService.updateStatus(taskId, 'running');
192
+ const abortSignal = this.timeoutManager?.startTimer(taskId, timeout, async () => {
193
+ await this.taskService.tryUpdateStatus(taskId, 'running', 'timeout', 'Task execution timed out');
194
+ });
183
195
  // ─── Step 4b: 持久化本轮 user(多轮短期记忆)───
184
196
  await persistUserTurnMessage(this.messageService, {
185
197
  sessionId,
@@ -193,10 +205,6 @@ export class StreamOrchestrator {
193
205
  sessionId,
194
206
  }));
195
207
  // ─── Step 5: 构建 Graph — 每次请求创建独立 StateGraph 实例,确保请求间无共享可变状态 ───
196
- const loadOpts = { agentVersion: req.agentVersion };
197
- const graphDsl = this.graphLoader
198
- ? await this.graphLoader.loadGraph(agentCode, loadOpts)
199
- : DEFAULT_GRAPH_DSL;
200
208
  const graph = await this.graphEngine.buildGraph(graphDsl);
201
209
  const loadedToolsBase = this.graphLoader
202
210
  ? toLoadedToolMap(await this.graphLoader.loadToolDefinitions(agentCode, loadOpts))
@@ -253,7 +261,7 @@ export class StreamOrchestrator {
253
261
  const toolResults = [];
254
262
  // ─── Step 6: Graph.stream() 流式执行 ───
255
263
  try {
256
- const eventStream = this.graphEngine.stream(graph, {
264
+ const eventStream = runWithExecutionTrace({ abortSignal }, () => this.graphEngine.stream(graph, {
257
265
  messages,
258
266
  userId,
259
267
  delegationToken: executionContext.delegationToken,
@@ -275,7 +283,9 @@ export class StreamOrchestrator {
275
283
  agentSystemPrompt,
276
284
  traceId: resolveTraceId(req),
277
285
  traceCollector: this.traceCollector,
278
- });
286
+ abortSignal,
287
+ abortDeadlineMs: Date.now() + timeout,
288
+ }));
279
289
  // 逐事件转发到 SSE;顺带把节点 token 记入 TokenStatsService(stream 原先未 record → 结尾全 0)
280
290
  for await (const runtimeEvent of eventStream) {
281
291
  // 连接已断开则提前退出
@@ -347,7 +357,7 @@ export class StreamOrchestrator {
347
357
  // ─── 错误处理:Timeout ───
348
358
  if (this.isTimeoutError(error)) {
349
359
  await recordTaskTokenUsages(this.taskService, taskId, this.tokenStatsService.list(taskId));
350
- await this.taskService.updateStatus(taskId, 'timeout');
360
+ await this.taskService.tryUpdateStatus(taskId, 'running', 'timeout');
351
361
  emitter.emit(this.buildSSEEvent('task.error', correlationId, taskId, {
352
362
  code: ErrorCodes.TASK_TIMEOUT,
353
363
  message: 'Task execution timed out',
@@ -363,6 +373,9 @@ export class StreamOrchestrator {
363
373
  this.emitErrorAndClose(emitter, correlationId, taskId, error);
364
374
  return;
365
375
  }
376
+ finally {
377
+ this.timeoutManager?.clearTimer(taskId);
378
+ }
366
379
  // ─── Step 7: 发送结束事件序列 ───
367
380
  // Emit: response.final(带完整 assistant 文本,供客户端兜底)
368
381
  const tokenStats = this.tokenStatsService.getTaskStats(taskId);
@@ -454,10 +467,6 @@ export class StreamOrchestrator {
454
467
  emitter.close();
455
468
  }
456
469
  isTimeoutError(error) {
457
- if (error instanceof Error) {
458
- const msg = error.message.toLowerCase();
459
- return msg.includes('timeout') || msg.includes('timed out');
460
- }
461
- return false;
470
+ return error instanceof Error && /timed out/i.test(error.message);
462
471
  }
463
472
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * 生产环境由 Hosting 提供对外 HTTP;本模块供 `EAP_DEV_HTTP=1` 本地调试。
5
5
  */
6
+ import { classifyRuntimeError } from '../../utils/http-error.js';
6
7
  // ─────────────────────────────────────────────
7
8
  // Route Handlers
8
9
  // ─────────────────────────────────────────────
@@ -85,18 +86,18 @@ export function createAgentRouteHandlers(deps) {
85
86
  };
86
87
  }
87
88
  catch (error) {
88
- // 错误处理
89
89
  const duration = Date.now() - startTime;
90
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
90
+ const classified = classifyRuntimeError(error);
91
+ const output = classified.exposeMessage ? classified.message : '系统内部错误';
91
92
  return {
92
- code: 'runtime.graph.execution_error',
93
+ code: classified.code,
93
94
  data: {
94
95
  taskId: '',
95
96
  sessionId: sessionId ?? '',
96
- output: '',
97
+ output,
97
98
  tokenUsage: { input: 0, output: 0, total: 0 },
98
99
  duration,
99
- status: 'failed',
100
+ status: classified.code === 'runtime.task.timeout' ? 'timeout' : 'failed',
100
101
  },
101
102
  traceId: '',
102
103
  };
@@ -12,7 +12,7 @@ function pickString(...values) {
12
12
  return undefined;
13
13
  }
14
14
  function omitTraceCollector(record) {
15
- const { traceCollector: _drop, ...rest } = record;
15
+ const { traceCollector: _drop, abortSignal: _abort, abortDeadlineMs: _deadline, ...rest } = record;
16
16
  return rest;
17
17
  }
18
18
  /** 去掉不可序列化的 collector,再交给 Saver。 */
@@ -12,6 +12,8 @@
12
12
  * - detail: 每次 LLM 调用输出 stats.node_token_usage + 结束时汇总
13
13
  */
14
14
  export type TokenDetailLevel = 'none' | 'summary' | 'detail';
15
+ /** 未写请求/图 timeout 时的整图缺省(毫秒)。20 分钟,须大于单次 LLM HTTP 缺省 180s。 */
16
+ export declare const DEFAULT_GRAPH_TIMEOUT_MS = 1200000;
15
17
  /**
16
18
  * Runtime 执行引擎配置
17
19
  *
@@ -20,9 +22,11 @@ export type TokenDetailLevel = 'none' | 'summary' | 'detail';
20
22
  export interface RuntimeConfig {
21
23
  /** Token 统计输出粒度 */
22
24
  tokenDetail: TokenDetailLevel;
23
- /** Task 执行超时时间(毫秒) */
25
+ /** Task 执行超时时间(毫秒)。未写请求/图 timeout 时为 {@link DEFAULT_GRAPH_TIMEOUT_MS} */
24
26
  timeout: number;
25
- /** ReAct 循环最大迭代次数 */
27
+ /**
28
+ * @deprecated Runtime 忽略。图级 / 请求级步数不是限长手段。整图限长用 timeout。
29
+ */
26
30
  maxIterations: number;
27
31
  /** SSE 心跳间隔(毫秒),默认 30000 */
28
32
  heartbeatInterval: number;
@@ -5,10 +5,12 @@
5
5
  * - @gt-fe/eap-contracts 定义跨模块的公共协议(如 Task 实体、Session 实体、Agent Manifest)
6
6
  * - 本文件定义 Runtime 执行引擎内部流转的配置、上下文、枚举等
7
7
  */
8
+ /** 未写请求/图 timeout 时的整图缺省(毫秒)。20 分钟,须大于单次 LLM HTTP 缺省 180s。 */
9
+ export const DEFAULT_GRAPH_TIMEOUT_MS = 1_200_000;
8
10
  /** RuntimeConfig 默认值 */
9
11
  export const DEFAULT_RUNTIME_CONFIG = {
10
12
  tokenDetail: 'detail',
11
- timeout: 60000,
13
+ timeout: DEFAULT_GRAPH_TIMEOUT_MS,
12
14
  maxIterations: 10,
13
15
  heartbeatInterval: 30000,
14
16
  };
@@ -17,12 +17,14 @@ import type { IToolExecutor } from '../../tool/tool-executor.interface.js';
17
17
  import type { IGovernanceClient } from '../../governance/governance-client.js';
18
18
  import { type ConditionEvaluatorFn } from '../condition-evaluator.js';
19
19
  import type { ExecutionInterceptors } from '../../execution/interceptors.js';
20
- /** 图未写 `config.maxIterations` 时的 recursionLimit。`agent ⇄ tools` 一圈约 2,50 ≈ 24 次工具往返。 */
21
- export declare const DEFAULT_GRAPH_RECURSION_LIMIT = 50;
22
20
  /**
23
- * LangGraph `recursionLimit` 计 superstep(每进一个节点 +1),不是 ReAct 圈数。
24
- * 图上 `config.maxIterations` 优先;缺省 {@link DEFAULT_GRAPH_RECURSION_LIMIT}。
21
+ * 传给 LangGraph 的 `recursionLimit`:一轮调度(superstep)+1。
22
+ * 同一步里并行的多个节点只 +1。ReAct 的 llm 再 tool 大约 +2。
23
+ * 不读图级 / 请求级 `maxIterations`。整图运行时长用 `timeout`。
24
+ * 配了 Token 配额才会因预算停止。
25
25
  */
26
+ export declare const DEFAULT_GRAPH_RECURSION_LIMIT = 5000;
27
+ export declare function isGraphRecursionError(error: unknown): boolean;
26
28
  /** 并行节点各自返回 [id] 时合并进同一路径;末项相同不重复。 */
27
29
  export declare function mergeHistoryPath(current: unknown, update: unknown): string[];
28
30
  /**
@@ -37,14 +37,34 @@ import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hit
37
37
  import { classifySkillRead } from './skill-read.js';
38
38
  import { skillEntryInvocationFromTool } from './skill-entry-span.js';
39
39
  import { withGraphSpan } from '../trace-span.js';
40
- import { executionTraceFromState, getExecutionStream, runWithExecutionTrace, } from '../execution-trace-context.js';
40
+ import { executionTraceFromState, getExecutionStream, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
41
41
  import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
42
- /** 图未写 `config.maxIterations` 时的 recursionLimit。`agent ⇄ tools` 一圈约 2,50 ≈ 24 次工具往返。 */
43
- export const DEFAULT_GRAPH_RECURSION_LIMIT = 50;
44
42
  /**
45
- * LangGraph `recursionLimit` 计 superstep(每进一个节点 +1),不是 ReAct 圈数。
46
- * 图上 `config.maxIterations` 优先;缺省 {@link DEFAULT_GRAPH_RECURSION_LIMIT}。
43
+ * 传给 LangGraph 的 `recursionLimit`:一轮调度(superstep)+1。
44
+ * 同一步里并行的多个节点只 +1。ReAct 的 llm 再 tool 大约 +2。
45
+ * 不读图级 / 请求级 `maxIterations`。整图运行时长用 `timeout`。
46
+ * 配了 Token 配额才会因预算停止。
47
47
  */
48
+ export const DEFAULT_GRAPH_RECURSION_LIMIT = 5000;
49
+ const GRAPH_RECURSION_LIMIT_MESSAGE = '图执行超过平台调度步上限。整图限长请用 config.timeout。';
50
+ export function isGraphRecursionError(error) {
51
+ if (!(error instanceof Error))
52
+ return false;
53
+ if (error.name === 'GraphRecursionError')
54
+ return true;
55
+ return /Recursion limit of \d+/i.test(error.message);
56
+ }
57
+ function rethrowGraphRunError(error) {
58
+ if (error instanceof AppError)
59
+ throw error;
60
+ if (isGraphRecursionError(error)) {
61
+ throw new AppError(ErrorCodes.GRAPH_MAX_ITERATIONS, GRAPH_RECURSION_LIMIT_MESSAGE, {
62
+ exposeMessage: true,
63
+ cause: error,
64
+ });
65
+ }
66
+ throw error;
67
+ }
48
68
  /** 并行节点各自返回 [id] 时合并进同一路径;末项相同不重复。 */
49
69
  export function mergeHistoryPath(current, update) {
50
70
  const path = Array.isArray(current)
@@ -63,16 +83,54 @@ export function mergeHistoryPath(current, update) {
63
83
  }
64
84
  return path.length > 1000 ? path.slice(path.length - 1000) : path;
65
85
  }
66
- function langGraphRunOptions(dsl, extra) {
67
- const raw = dsl.config?.maxIterations;
68
- const recursionLimit = typeof raw === 'number' && Number.isFinite(raw) && raw > 0
69
- ? Math.trunc(raw)
70
- : DEFAULT_GRAPH_RECURSION_LIMIT;
86
+ function langGraphRunOptions(extra) {
71
87
  return {
72
88
  ...extra,
73
- recursionLimit,
89
+ recursionLimit: DEFAULT_GRAPH_RECURSION_LIMIT,
74
90
  };
75
91
  }
92
+ function graphAbortSignal(state) {
93
+ const fromState = state?.abortSignal;
94
+ if (fromState instanceof AbortSignal)
95
+ return fromState;
96
+ return getExecutionTrace()?.abortSignal;
97
+ }
98
+ function abortReason(signal) {
99
+ return signal.reason instanceof Error
100
+ ? signal.reason
101
+ : new Error(String(signal.reason ?? 'aborted'));
102
+ }
103
+ function graphAbortDeadlineMs(state) {
104
+ const fromState = state?.abortDeadlineMs;
105
+ if (typeof fromState === 'number' && Number.isFinite(fromState))
106
+ return fromState;
107
+ return getExecutionTrace()?.abortDeadlineMs;
108
+ }
109
+ function throwIfGraphAborted(state) {
110
+ const deadline = graphAbortDeadlineMs(state);
111
+ if (deadline !== undefined && Date.now() >= deadline) {
112
+ const signal = graphAbortSignal(state);
113
+ throw signal?.aborted
114
+ ? abortReason(signal)
115
+ : new Error('Task timed out after graph deadline');
116
+ }
117
+ const signal = graphAbortSignal(state);
118
+ if (signal?.aborted)
119
+ throw abortReason(signal);
120
+ }
121
+ function withGraphAbort(promise, signal) {
122
+ if (!signal)
123
+ return promise;
124
+ if (signal.aborted)
125
+ return Promise.reject(abortReason(signal));
126
+ return new Promise((resolve, reject) => {
127
+ const onAbort = () => reject(abortReason(signal));
128
+ signal.addEventListener('abort', onAbort, { once: true });
129
+ promise.then(resolve, reject).finally(() => {
130
+ signal.removeEventListener('abort', onAbort);
131
+ });
132
+ });
133
+ }
76
134
  // ─────────────────────────────────────────────
77
135
  // LangGraphAdapter
78
136
  // ─────────────────────────────────────────────
@@ -532,12 +590,25 @@ export class LangGraphAdapter {
532
590
  streamedTextThisTurn: false,
533
591
  };
534
592
  const traceCtx = executionTraceFromState(state);
535
- const producer = runWithExecutionTrace({ ...traceCtx, stream: streamBag }, () => (async () => {
593
+ const graphSignal = graphAbortSignal(state);
594
+ const onGraphAbort = () => {
595
+ failure = graphSignal ? abortReason(graphSignal) : new Error('aborted');
596
+ finished = true;
597
+ signal();
598
+ };
599
+ graphSignal?.addEventListener('abort', onGraphAbort, { once: true });
600
+ const producer = runWithExecutionTrace({
601
+ ...traceCtx,
602
+ stream: streamBag,
603
+ abortSignal: graphSignal,
604
+ abortDeadlineMs: graphAbortDeadlineMs(state),
605
+ }, () => (async () => {
536
606
  try {
537
- const streamIterator = await ref.compiled.stream(input, langGraphRunOptions(ref.dsl, {
607
+ throwIfGraphAborted(state);
608
+ const streamIterator = await withGraphAbort(Promise.resolve(ref.compiled.stream(input, langGraphRunOptions({
538
609
  configurable: this.checkpointConfigurable(state, graph),
539
610
  streamMode: 'updates',
540
- }));
611
+ }))), graphSignal);
541
612
  for await (const update of streamIterator) {
542
613
  for (const event of this.convertUpdatesToEvents(update, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
543
614
  enqueue(event);
@@ -562,8 +633,9 @@ export class LangGraphAdapter {
562
633
  yield queue.shift();
563
634
  }
564
635
  await producer;
636
+ graphSignal?.removeEventListener('abort', onGraphAbort);
565
637
  if (failure)
566
- throw failure;
638
+ rethrowGraphRunError(failure);
567
639
  }
568
640
  /**
569
641
  * 同步调用图执行,返回完整结果
@@ -573,10 +645,22 @@ export class LangGraphAdapter {
573
645
  const input = this.graphStateToLangGraphInput(this.applyDeclaredSearchFace(state, ref.dsl));
574
646
  const startTime = Date.now();
575
647
  const traceCtx = executionTraceFromState(state);
648
+ const abortSignal = graphAbortSignal(state);
649
+ const abortDeadlineMs = graphAbortDeadlineMs(state);
576
650
  // 只补 collector/traceId。子图套娃时外层 stream 袋经 ?? 保留。
577
- const result = await runWithExecutionTrace(traceCtx, () => ref.compiled.invoke(input, langGraphRunOptions(ref.dsl, {
578
- configurable: this.checkpointConfigurable(state, graph),
579
- })));
651
+ let result;
652
+ try {
653
+ result = await withGraphAbort(runWithExecutionTrace({
654
+ ...traceCtx,
655
+ abortSignal,
656
+ abortDeadlineMs,
657
+ }, () => ref.compiled.invoke(input, langGraphRunOptions({
658
+ configurable: this.checkpointConfigurable(state, graph),
659
+ }))), abortSignal);
660
+ }
661
+ catch (error) {
662
+ rethrowGraphRunError(error);
663
+ }
580
664
  const duration = Date.now() - startTime;
581
665
  const messages = Array.isArray(result['messages']) ? result['messages'] : [];
582
666
  const lastMessage = messages[messages.length - 1];
@@ -684,6 +768,7 @@ export class LangGraphAdapter {
684
768
  createNodeFunction(node) {
685
769
  const impl = this.createNodeFunctionBody(node);
686
770
  return async (state) => {
771
+ throwIfGraphAborted(state);
687
772
  const update = await impl(state);
688
773
  const patch = update && typeof update === 'object' ? { ...update } : {};
689
774
  const currentNode = typeof patch['currentNode'] === 'string' && String(patch['currentNode']).trim()
@@ -243,6 +243,15 @@ export interface GraphState {
243
243
  * 仅 stream/invoke 入口使用,不入持久化语义。
244
244
  */
245
245
  replaceCheckpointMessages?: boolean;
246
+ /**
247
+ * 整图超时用的 AbortSignal。不入 Checkpoint。不传给 LangGraph。
248
+ */
249
+ abortSignal?: AbortSignal;
250
+ /**
251
+ * 整图超时截止(`Date.now()`)。不入 Checkpoint。
252
+ * 节点入口检查,避免同步循环占满事件循环导致 timer 不能触发。
253
+ */
254
+ abortDeadlineMs?: number;
246
255
  /**
247
256
  * 子图 invoke 写入 LangGraph `checkpoint_ns`。
248
257
  * 父图为空;子图为父节点 id。存储槽与父 thread 分开,不覆盖父 latest。
@@ -16,6 +16,10 @@ export interface ExecutionRunContext {
16
16
  collector?: ITraceCollector;
17
17
  traceId?: string;
18
18
  stream?: ExecutionStreamBag;
19
+ /** 整图超时 AbortSignal;不入 Checkpoint;不传给 LangGraph */
20
+ abortSignal?: AbortSignal;
21
+ /** 整图超时截止(Date.now());不入 Checkpoint */
22
+ abortDeadlineMs?: number;
19
23
  }
20
24
  /** 入口从本次入参取出可绑定的 collector+traceId;缺一则不算。 */
21
25
  export declare function executionTraceFromState(state: GraphState | Record<string, unknown> | undefined): {
@@ -33,8 +33,10 @@ export function runWithExecutionTrace(patch, fn) {
33
33
  collector: patch.collector ?? outer?.collector,
34
34
  traceId: patch.traceId ?? outer?.traceId,
35
35
  stream: patch.stream ?? outer?.stream,
36
+ abortSignal: patch.abortSignal ?? outer?.abortSignal,
37
+ abortDeadlineMs: patch.abortDeadlineMs ?? outer?.abortDeadlineMs,
36
38
  };
37
- if (!next.collector && !next.traceId && !next.stream)
39
+ if (!next.collector && !next.traceId && !next.stream && !next.abortSignal && next.abortDeadlineMs === undefined)
38
40
  return fn();
39
41
  return storage.run(next, fn);
40
42
  }
@@ -173,7 +173,10 @@ export interface RagNodeConfig {
173
173
  model?: string;
174
174
  /** 融合后总条数,缺省 10 */
175
175
  totalK?: number;
176
- /** ReAct 决策轮硬上限(按决策轮计次,同轮并发多个 tool_call 只计 1),缺省 5 */
176
+ /**
177
+ * 本节点检索轮次上限(同轮并发多个 tool_call 只计 1),缺省 5。
178
+ * 到上限该节点带着已有结果正常结束,后续图节点继续。不是整图停止。
179
+ */
177
180
  maxIterations?: number;
178
181
  /** 近重判定阈值(字符 n-gram Jaccard,M4 启用),缺省 0.8 */
179
182
  nearDupThreshold?: number;