@gt-fe/eap-runtime 0.0.23 → 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.
@@ -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;
@@ -9,6 +9,7 @@
9
9
  import { resolveLlmModelName } from './resolve-llm-model.js';
10
10
  import { createFileLlmTurnLogFromEnv, maskApiKey } from '../debug/llm-turn-log.js';
11
11
  import { buildContextUsage, turnSectionChars } from '../token/context-window.js';
12
+ import { getExecutionTrace } from '../graph/execution-trace-context.js';
12
13
  export const DEFAULT_LLM_HTTP_TIMEOUT_MS = 180_000;
13
14
  export class HttpLLMProvider {
14
15
  apiKey;
@@ -101,6 +102,7 @@ export class HttpLLMProvider {
101
102
  }
102
103
  const controller = new AbortController();
103
104
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
105
+ const unlinkGraphAbort = linkGraphAbort(controller);
104
106
  const url = `${this.baseUrl}/chat/completions`;
105
107
  const startedAt = Date.now();
106
108
  try {
@@ -117,6 +119,7 @@ export class HttpLLMProvider {
117
119
  });
118
120
  }
119
121
  catch (err) {
122
+ throwIfGraphTimedOut();
120
123
  throw new Error(this.formatNetworkError(url, err));
121
124
  }
122
125
  if (!response.ok) {
@@ -184,6 +187,7 @@ export class HttpLLMProvider {
184
187
  }
185
188
  finally {
186
189
  clearTimeout(timeout);
190
+ unlinkGraphAbort();
187
191
  }
188
192
  }
189
193
  async *stream(messages, config) {
@@ -217,6 +221,7 @@ export class HttpLLMProvider {
217
221
  }
218
222
  const controller = new AbortController();
219
223
  const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
224
+ const unlinkGraphAbort = linkGraphAbort(controller);
220
225
  const url = `${this.baseUrl}/chat/completions`;
221
226
  const startedAt = Date.now();
222
227
  const contentChunks = [];
@@ -239,6 +244,7 @@ export class HttpLLMProvider {
239
244
  });
240
245
  }
241
246
  catch (err) {
247
+ throwIfGraphTimedOut();
242
248
  throw new Error(this.formatNetworkError(url, err));
243
249
  }
244
250
  if (!response.ok) {
@@ -412,6 +418,7 @@ export class HttpLLMProvider {
412
418
  }
413
419
  finally {
414
420
  clearTimeout(timeout);
421
+ unlinkGraphAbort();
415
422
  }
416
423
  }
417
424
  // ─────────────────────────────────────────────
@@ -509,6 +516,24 @@ export class HttpLLMProvider {
509
516
  return `LLM network error calling ${url}: ${detail}`;
510
517
  }
511
518
  }
519
+ function linkGraphAbort(controller) {
520
+ const graphSignal = getExecutionTrace()?.abortSignal;
521
+ if (!graphSignal)
522
+ return () => undefined;
523
+ if (graphSignal.aborted) {
524
+ controller.abort(graphSignal.reason);
525
+ return () => undefined;
526
+ }
527
+ const onAbort = () => controller.abort(graphSignal.reason);
528
+ graphSignal.addEventListener('abort', onAbort, { once: true });
529
+ return () => graphSignal.removeEventListener('abort', onAbort);
530
+ }
531
+ function throwIfGraphTimedOut() {
532
+ const reason = getExecutionTrace()?.abortSignal?.reason;
533
+ if (reason instanceof Error && /timed out/i.test(reason.message)) {
534
+ throw reason;
535
+ }
536
+ }
512
537
  function formatNetworkDetail(error, depth = 0) {
513
538
  if (depth > 5)
514
539
  return '...';
@@ -4,6 +4,7 @@
4
4
  * 生产(server + EAP_PREINSTALL_ROOT):以预装为准,缺失则失败(04 Req 8.1)。
5
5
  * 开发态:Registry + 可选 ./tools 本地 manifest。
6
6
  */
7
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../common/types.js';
7
8
  import { readdir, readFile } from 'node:fs/promises';
8
9
  import path from 'node:path';
9
10
  import { resolveAgentModelAllowlist, resolveAgentDefaultModel } from './agent-models.js';
@@ -142,7 +143,7 @@ const DEFAULT_GRAPH_DSL = {
142
143
  ],
143
144
  entryPoint: 'llm',
144
145
  finishPoints: ['__end__'],
145
- config: { timeout: 60000 },
146
+ config: { timeout: DEFAULT_GRAPH_TIMEOUT_MS },
146
147
  };
147
148
  /** 源图有非空字符串则保留;缺省或空数组为 `['__end__']`。预装 graph.json 与清单内嵌图同一收口。 */
148
149
  function finishPointsFromGraphDocument(graph) {
@@ -155,8 +156,8 @@ function finishPointsFromGraphDocument(graph) {
155
156
  .filter((id) => id.length > 0);
156
157
  return ids.length > 0 ? ids : ['__end__'];
157
158
  }
158
- const DEFAULT_LOADED_GRAPH_CONFIG = { timeout: 60000 };
159
- /** 默认 timeout 60000;源图 `config` 覆盖。缺 `maxIterations` 时由引擎用 50。 */
159
+ const DEFAULT_LOADED_GRAPH_CONFIG = { timeout: DEFAULT_GRAPH_TIMEOUT_MS };
160
+ /** 默认 timeout {@link DEFAULT_GRAPH_TIMEOUT_MS};源图 `config` 覆盖。`maxIterations` 可留在 DSL 上,引擎忽略。 */
160
161
  function configFromGraphDocument(graph) {
161
162
  const raw = graph.config;
162
163
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
@@ -758,7 +759,7 @@ export class GraphLoader {
758
759
  },
759
760
  ],
760
761
  config: {
761
- timeout: manifest.config?.timeout ?? 60000,
762
+ timeout: manifest.config?.timeout ?? DEFAULT_GRAPH_TIMEOUT_MS,
762
763
  maxRetries: manifest.config?.retryCount,
763
764
  },
764
765
  };
@@ -10,6 +10,7 @@ import type { InvokeOrchestrator } from '../api/handlers/invoke.handler.js';
10
10
  import type { ICheckpointSaver } from '../checkpoint/contracts/checkpoint-saver.contract.js';
11
11
  import type { ITraceCollector } from '../contracts/trace-collector.js';
12
12
  import type { IStateGraphEngine } from '../graph/contracts/state-graph.contract.js';
13
+ import type { TaskTimeoutManager } from '../task/task-timeout.js';
13
14
  import { HitlHandler } from '../graph/hitl/hitl-handler.js';
14
15
  import { type GraphLoader } from '../registry/graph-loader.js';
15
16
  import type { SessionContextService } from '../session/session-context.service.js';
@@ -51,6 +52,8 @@ export interface AgentRuntimeDeps {
51
52
  sessionCompression?: SessionCompressionOptions;
52
53
  /** 长期记忆注入;默认关。见 EAP_RUNTIME_MEMORY */
53
54
  sessionMemory?: import('../session/apply-session-memory.js').SessionMemoryOptions;
55
+ /** 未注入时不启动整图超时(测试桩) */
56
+ timeoutManager?: TaskTimeoutManager;
54
57
  }
55
58
  /** 一次对话详情:消息 + 每步 LLM 入参/响应。File / 内存 / Postgres 同一套结构。 */
56
59
  export interface ConversationDetail {
@@ -7,6 +7,9 @@
7
7
  * @see 04-runtime-core/design.md §1.5 / §1.7
8
8
  */
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../common/types.js';
11
+ import { resolveTaskTimeoutMs } from '../task/resolve-task-timeout.js';
12
+ import { runWithExecutionTrace } from '../graph/execution-trace-context.js';
10
13
  import { HITL_TIMEOUT_ERROR_CODE, HITL_TIMEOUT_MESSAGE, HitlAlreadyTimedOutError, HitlNotWaitingError, buildHitlWaitedPayload, isHitlDeadlinePassed, loadHitlCheckpoint, readPersistedHitlWaiting, unwrapHitlPaused, } from '../graph/hitl/hitl-handler.js';
11
14
  import { resolveAgentSystemPromptFromLoader } from '../registry/graph-loader.js';
12
15
  import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
@@ -37,7 +40,7 @@ const DEFAULT_GRAPH_DSL = {
37
40
  { from: '__start__', to: 'llm' },
38
41
  { from: 'llm', to: '__end__' },
39
42
  ],
40
- config: { timeout: 60000 },
43
+ config: { timeout: DEFAULT_GRAPH_TIMEOUT_MS },
41
44
  };
42
45
  /**
43
46
  * AgentRuntime — 管理 Graph 执行的 SDK 门面
@@ -214,6 +217,14 @@ export class AgentRuntime {
214
217
  ragScope: params.ragScope,
215
218
  ragSources: params.ragSources,
216
219
  }, userId);
220
+ if (response.code !== 'ok') {
221
+ const output = typeof response.data?.output === 'string' && response.data.output
222
+ ? response.data.output
223
+ : response.code;
224
+ throw new AppError(response.code, output, {
225
+ exposeMessage: true,
226
+ });
227
+ }
217
228
  return response.data;
218
229
  }
219
230
  /**
@@ -241,6 +252,11 @@ export class AgentRuntime {
241
252
  userId,
242
253
  });
243
254
  }
255
+ const loadOpts = { agentVersion: params.agentVersion };
256
+ const graphDsl = this.deps.graphLoader
257
+ ? await this.deps.graphLoader.loadGraph(agentCode, loadOpts)
258
+ : DEFAULT_GRAPH_DSL;
259
+ const timeout = resolveTaskTimeoutMs(params.config?.timeout, graphDsl.config?.timeout);
244
260
  const task = await this.deps.taskService.create({
245
261
  id: params.taskId,
246
262
  agentId: agentCode,
@@ -248,9 +264,12 @@ export class AgentRuntime {
248
264
  userId,
249
265
  correlationId,
250
266
  input: params.input,
251
- timeout: params.config?.timeout ?? 60_000,
267
+ timeout,
252
268
  });
253
269
  await this.deps.taskService.updateStatus(task.id, 'running');
270
+ const abortSignal = this.deps.timeoutManager?.startTimer(task.id, timeout, async () => {
271
+ await this.deps.taskService.tryUpdateStatus(task.id, 'running', 'timeout', 'Task execution timed out');
272
+ });
254
273
  await persistUserTurnMessage(this.deps.messageService, {
255
274
  sessionId,
256
275
  taskId: task.id,
@@ -258,10 +277,6 @@ export class AgentRuntime {
258
277
  content: params.input,
259
278
  });
260
279
  yield this.toSSE('task.started', correlationId, task.id, { taskId: task.id, sessionId });
261
- const loadOpts = { agentVersion: params.agentVersion };
262
- const graphDsl = this.deps.graphLoader
263
- ? await this.deps.graphLoader.loadGraph(agentCode, loadOpts)
264
- : DEFAULT_GRAPH_DSL;
265
280
  const graph = await this.deps.graphEngine.buildGraph(graphDsl);
266
281
  const loadedToolsBase = await this.resolveLoadedTools(agentCode, params.agentVersion);
267
282
  const loadedSkills = this.deps.graphLoader
@@ -321,7 +336,7 @@ export class AgentRuntime {
321
336
  const toolResults = [];
322
337
  const nodeTokenUsages = [];
323
338
  try {
324
- for await (const runtimeEvent of this.deps.graphEngine.stream(graph, {
339
+ for await (const runtimeEvent of runWithExecutionTrace({ abortSignal }, () => this.deps.graphEngine.stream(graph, {
325
340
  messages,
326
341
  userId,
327
342
  delegationToken: ctx.delegationToken,
@@ -343,7 +358,9 @@ export class AgentRuntime {
343
358
  skillInput: resolveSkillInputFromInvokeInput(params.input),
344
359
  traceId: resolveTraceId(params),
345
360
  traceCollector: this.deps.traceCollector,
346
- })) {
361
+ abortSignal,
362
+ abortDeadlineMs: Date.now() + timeout,
363
+ }))) {
347
364
  if (runtimeEvent.type === 'tool.called') {
348
365
  const p = runtimeEvent.payload;
349
366
  if (p.toolCallId && p.toolName) {
@@ -415,6 +432,17 @@ export class AgentRuntime {
415
432
  yield this.toSSE('hitl.waited', correlationId, task.id, buildHitlWaitedPayload(hitlPaused));
416
433
  return;
417
434
  }
435
+ const timedOut = err instanceof Error && /timed out/i.test(err.message);
436
+ if (timedOut) {
437
+ await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
438
+ await this.deps.taskService.tryUpdateStatus(task.id, 'running', 'timeout');
439
+ yield this.toSSE('task.error', correlationId, task.id, {
440
+ code: ErrorCodes.TASK_TIMEOUT,
441
+ message: 'Task execution timed out',
442
+ recoverable: false,
443
+ });
444
+ return;
445
+ }
418
446
  const message = err instanceof Error ? err.message : 'Unknown error';
419
447
  await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
420
448
  await this.deps.taskService.updateStatus(task.id, 'failed', message);
@@ -425,6 +453,9 @@ export class AgentRuntime {
425
453
  });
426
454
  throw err;
427
455
  }
456
+ finally {
457
+ this.deps.timeoutManager?.clearTimer(task.id);
458
+ }
428
459
  }
429
460
  /**
430
461
  * HITL 用户响应后继续执行(同步结果)。
@@ -483,6 +514,9 @@ export class AgentRuntime {
483
514
  if (!claimed) {
484
515
  throw new AppError(ErrorCodes.TASK_HITL_INVALID_STATE, `Task ${taskId} is no longer waiting for HITL`, { exposeMessage: true });
485
516
  }
517
+ const abortSignal = this.deps.timeoutManager?.startTimer(taskId, resolveTaskTimeoutMs(task.timeout), async () => {
518
+ await this.deps.taskService.tryUpdateStatus(taskId, 'running', 'timeout', 'Task execution timed out');
519
+ });
486
520
  const tokenDetail = resolveTokenDetail(response.tokenDetail);
487
521
  yield this.toSSE('hitl.resumed', correlationId, taskId, {
488
522
  choice: response.choice,
@@ -507,6 +541,8 @@ export class AgentRuntime {
507
541
  : await this.resolveLoadedSkills(agentCode, response.userId, agentVersion);
508
542
  const continueState = {
509
543
  ...resumedState,
544
+ abortSignal,
545
+ abortDeadlineMs: Date.now() + resolveTaskTimeoutMs(task.timeout),
510
546
  messages: resumedState.messages ?? [],
511
547
  replaceCheckpointMessages: true,
512
548
  sessionId: resumedState.sessionId ?? sessionId,
@@ -542,7 +578,7 @@ export class AgentRuntime {
542
578
  const toolResults = [];
543
579
  const nodeTokenUsages = [];
544
580
  try {
545
- for await (const runtimeEvent of this.deps.graphEngine.stream(graph, continueState)) {
581
+ for await (const runtimeEvent of runWithExecutionTrace({ abortSignal, abortDeadlineMs: continueState.abortDeadlineMs }, () => this.deps.graphEngine.stream(graph, continueState))) {
546
582
  if (runtimeEvent.type === 'tool.called') {
547
583
  const p = runtimeEvent.payload;
548
584
  if (p.toolCallId && p.toolName) {
@@ -617,6 +653,17 @@ export class AgentRuntime {
617
653
  yield this.toSSE('hitl.waited', correlationId, taskId, buildHitlWaitedPayload(hitlPaused));
618
654
  return;
619
655
  }
656
+ const timedOut = err instanceof Error && /timed out/i.test(err.message);
657
+ if (timedOut) {
658
+ await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
659
+ await this.deps.taskService.tryUpdateStatus(taskId, 'running', 'timeout');
660
+ yield this.toSSE('task.error', correlationId, taskId, {
661
+ code: ErrorCodes.TASK_TIMEOUT,
662
+ message: 'Task execution timed out',
663
+ recoverable: false,
664
+ });
665
+ return;
666
+ }
620
667
  const appErr = err instanceof AppError ? err : AppError.fromUnknown(err);
621
668
  const failMsg = appErr.exposeMessage ? appErr.message : '系统内部错误';
622
669
  await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
@@ -628,6 +675,9 @@ export class AgentRuntime {
628
675
  });
629
676
  throw err instanceof AppError ? err : appErr;
630
677
  }
678
+ finally {
679
+ this.deps.timeoutManager?.clearTimer(taskId);
680
+ }
631
681
  }
632
682
  /**
633
683
  * 从 Checkpoint Fork 并真实重新执行。
@@ -658,6 +708,12 @@ export class AgentRuntime {
658
708
  const newThreadId = randomUUID();
659
709
  const forkedCheckpoint = await this.deps.checkpointSaver.fork(checkpointId, newThreadId, overrides);
660
710
  const correlationId = overrides.executionContext?.correlationId ?? randomUUID();
711
+ const agentVersion = overrides.agentVersion ??
712
+ (typeof source.state.agentVersion === 'string' ? source.state.agentVersion : undefined);
713
+ const graphDsl = this.deps.graphLoader && typeof this.deps.graphLoader.loadGraph === 'function'
714
+ ? await this.deps.graphLoader.loadGraph(agentCode, { agentVersion })
715
+ : DEFAULT_GRAPH_DSL;
716
+ const timeout = resolveTaskTimeoutMs(undefined, graphDsl.config?.timeout);
661
717
  const task = await this.deps.taskService.create({
662
718
  id: taskId,
663
719
  agentId: agentCode,
@@ -665,12 +721,13 @@ export class AgentRuntime {
665
721
  userId,
666
722
  correlationId,
667
723
  input: { forkFrom: checkpointId, overrides },
668
- timeout: 600_000,
724
+ timeout,
669
725
  });
670
726
  await this.deps.taskService.updateStatus(task.id, 'running');
671
- const agentVersion = overrides.agentVersion ??
672
- (typeof source.state.agentVersion === 'string' ? source.state.agentVersion : undefined);
673
- const graph = await this.rebuildExecutableGraph(agentCode, agentVersion);
727
+ const abortSignal = this.deps.timeoutManager?.startTimer(task.id, timeout, async () => {
728
+ await this.deps.taskService.tryUpdateStatus(task.id, 'running', 'timeout', 'Task execution timed out');
729
+ });
730
+ const graph = await this.deps.graphEngine.buildGraph(graphDsl);
674
731
  const loadedToolsBase = await this.resolveLoadedTools(agentCode, agentVersion);
675
732
  const skillBundle = await this.resolveLoadedSkills(agentCode, userId, agentVersion);
676
733
  const loadedTools = assembleLoadedTools(loadedToolsBase);
@@ -681,11 +738,12 @@ export class AgentRuntime {
681
738
  const agentSystemPrompt = (await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, agentVersion)) ?? (typeof forkedCheckpoint.state.agentSystemPrompt === 'string'
682
739
  ? forkedCheckpoint.state.agentSystemPrompt
683
740
  : undefined);
741
+ const abortDeadlineMs = Date.now() + timeout;
684
742
  try {
685
743
  const forkState = { ...forkedCheckpoint.state };
686
744
  delete forkState.toolSpanId;
687
745
  delete forkState.hitlToolCallId;
688
- const result = await this.deps.graphEngine.invoke(graph, {
746
+ const result = await runWithExecutionTrace({ abortSignal, abortDeadlineMs }, () => this.deps.graphEngine.invoke(graph, {
689
747
  messages: Array.isArray(forkState.messages)
690
748
  ? forkState.messages
691
749
  : [],
@@ -701,7 +759,9 @@ export class AgentRuntime {
701
759
  taskId: task.id,
702
760
  traceId,
703
761
  traceCollector: this.deps.traceCollector,
704
- });
762
+ abortSignal,
763
+ abortDeadlineMs,
764
+ }));
705
765
  if (result.tokenUsage.perNode && result.tokenUsage.perNode.length > 0) {
706
766
  await recordTaskTokenUsages(this.deps.taskService, task.id, result.tokenUsage.perNode);
707
767
  }
@@ -760,6 +820,23 @@ export class AgentRuntime {
760
820
  await recordTaskTokenUsages(this.deps.taskService, task.id, usages);
761
821
  const input_tokens = usages.reduce((sum, row) => sum + row.inputTokens, 0);
762
822
  const output_tokens = usages.reduce((sum, row) => sum + row.outputTokens, 0);
823
+ const timedOut = err instanceof Error && /timed out/i.test(err.message);
824
+ if (timedOut) {
825
+ await this.deps.taskService.tryUpdateStatus(task.id, 'running', 'timeout');
826
+ return {
827
+ newTraceId: traceId,
828
+ originalTraceId,
829
+ newTaskId: task.id,
830
+ checkpointId,
831
+ status: 'timeout',
832
+ output: message,
833
+ tokenUsage: {
834
+ input_tokens,
835
+ output_tokens,
836
+ total_tokens: input_tokens + output_tokens,
837
+ },
838
+ };
839
+ }
763
840
  await this.deps.taskService.updateStatus(task.id, 'failed', message);
764
841
  return {
765
842
  newTraceId: traceId,
@@ -775,6 +852,9 @@ export class AgentRuntime {
775
852
  },
776
853
  };
777
854
  }
855
+ finally {
856
+ this.deps.timeoutManager?.clearTimer(task.id);
857
+ }
778
858
  }
779
859
  async prepareHitlResume(taskId, response) {
780
860
  const task = await this.deps.taskService.getTask(taskId);
@@ -31,7 +31,7 @@ import { createToolExecutor } from '../tool/create-tool-executor.js';
31
31
  import { DevToolExecutor } from '../tool/dev-tool-executor.js';
32
32
  import { captureOverlayJailOptions, withHarnessPrimitives } from '../tool/harness-primitives.js';
33
33
  import { AllowAllGovernanceClient, HttpGovernanceClient, } from '../governance/index.js';
34
- import { InvokeOrchestrator, StreamOrchestrator, SessionService, MessageService, SessionContextService, TaskService, LangGraphAdapter, HitlHandler, TokenBudgetService, TokenStatsService, TokenBudgetGuard, SessionBudgetChecker, createAgentRegistryClient, GraphLoader, } from '../index.js';
34
+ import { InvokeOrchestrator, StreamOrchestrator, SessionService, MessageService, SessionContextService, TaskService, TaskTimeoutManager, LangGraphAdapter, HitlHandler, TokenBudgetService, TokenStatsService, TokenBudgetGuard, SessionBudgetChecker, createAgentRegistryClient, GraphLoader, } from '../index.js';
35
35
  import { createAgentRuntime } from './agent-runtime.js';
36
36
  import { assembleRuntimeStorage } from './storage-profile.js';
37
37
  import { createDevRagNodeDeps } from './dev-rag-deps.js';
@@ -139,6 +139,7 @@ export function createDevRuntime(options) {
139
139
  const sessionContextService = new SessionContextService(sessionService, messageService);
140
140
  // ── Task / Token ──
141
141
  const taskService = new TaskService();
142
+ const timeoutManager = new TaskTimeoutManager();
142
143
  const budgetService = new TokenBudgetService(storage.tokenBudgetRepository);
143
144
  const tokenStatsService = new TokenStatsService();
144
145
  const tokenBudgetGuard = new TokenBudgetGuard(budgetService);
@@ -202,6 +203,7 @@ export function createDevRuntime(options) {
202
203
  checkpointSaver: storage.checkpointSaver,
203
204
  sessionCompression,
204
205
  sessionMemory,
206
+ timeoutManager,
205
207
  });
206
208
  const streamOrchestrator = new StreamOrchestrator({
207
209
  sessionContextService,
@@ -215,6 +217,7 @@ export function createDevRuntime(options) {
215
217
  checkpointSaver: storage.checkpointSaver,
216
218
  sessionCompression,
217
219
  sessionMemory,
220
+ timeoutManager,
218
221
  });
219
222
  const agentRuntime = createAgentRuntime({
220
223
  invokeOrchestrator,
@@ -229,6 +232,7 @@ export function createDevRuntime(options) {
229
232
  turnLog,
230
233
  sessionCompression,
231
234
  sessionMemory,
235
+ timeoutManager,
232
236
  });
233
237
  return {
234
238
  invokeOrchestrator,
@@ -243,7 +247,10 @@ export function createDevRuntime(options) {
243
247
  taskService,
244
248
  tokenStatsService,
245
249
  graphLoader,
246
- dispose: stopSkillWatch,
250
+ dispose: () => {
251
+ stopSkillWatch();
252
+ timeoutManager.dispose();
253
+ },
247
254
  diagnostics: {
248
255
  packageVersion: RUNTIME_PACKAGE_VERSION,
249
256
  deploymentContext,
@@ -78,7 +78,7 @@ export interface ForkResult {
78
78
  originalTraceId: string;
79
79
  newTaskId: string;
80
80
  checkpointId: string;
81
- status: 'completed' | 'failed';
81
+ status: 'completed' | 'failed' | 'timeout';
82
82
  output: unknown;
83
83
  tokenUsage: {
84
84
  input_tokens: number;
@@ -5,7 +5,7 @@
5
5
  * - response.final
6
6
  *
7
7
  * 可选事件:
8
- * - response.max_iterations_reached
8
+ * - response.max_iterations_reached(契约保留;调度步上限走 task.error / runtime.graph.max_iterations,不发本事件)
9
9
  *
10
10
  * @see design.md §1.6 SSE事件协议定义
11
11
  */
@@ -16,7 +16,7 @@ export interface ResponseFinalPayload {
16
16
  /** 回复格式(如 text, markdown, json) */
17
17
  format: string;
18
18
  }
19
- /** 达到最大迭代次数 */
19
+ /** 契约保留。整图调度步上限不发此事件。 */
20
20
  export interface ResponseMaxIterationsReachedPayload {
21
21
  /** 最大迭代次数 */
22
22
  maxIterations: number;
@@ -5,7 +5,7 @@
5
5
  * - response.final
6
6
  *
7
7
  * 可选事件:
8
- * - response.max_iterations_reached
8
+ * - response.max_iterations_reached(契约保留;调度步上限走 task.error / runtime.graph.max_iterations,不发本事件)
9
9
  *
10
10
  * @see design.md §1.6 SSE事件协议定义
11
11
  */
@@ -46,6 +46,8 @@ export interface TaskRecord {
46
46
  correlationId: string;
47
47
  /** 用户输入 */
48
48
  input: unknown;
49
+ /** 整图超时(毫秒)。未写入旧记录时 resume 用平台缺省 */
50
+ timeout?: number;
49
51
  /** 执行输出 */
50
52
  output: unknown | null;
51
53
  /** 当前状态 */
@@ -3,5 +3,6 @@ export { TaskService, InMemoryTaskRepository } from './task.service.js';
3
3
  export type { ITaskRepository } from './task.service.js';
4
4
  export { TaskTimeoutManager } from './task-timeout.js';
5
5
  export type { TaskTimeoutManagerOptions } from './task-timeout.js';
6
+ export { resolveTaskTimeoutMs } from './resolve-task-timeout.js';
6
7
  export { TaskProgressTracker } from './task-progress-tracker.js';
7
8
  export type { TaskProgressInfo } from './task-progress-tracker.js';
@@ -2,4 +2,5 @@
2
2
  export * from './contracts/index.js';
3
3
  export { TaskService, InMemoryTaskRepository } from './task.service.js';
4
4
  export { TaskTimeoutManager } from './task-timeout.js';
5
+ export { resolveTaskTimeoutMs } from './resolve-task-timeout.js';
5
6
  export { TaskProgressTracker } from './task-progress-tracker.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 整图超时:请求 config.timeout,否则图 config.timeout,否则平台缺省 1200000 毫秒(20 分钟)。
3
+ * 官方模板图上不写 timeout,用平台缺省。LLM HTTP 超时、HITL 等待另算。
4
+ */
5
+ /** 请求显式值优先,否则图配置,否则 {@link DEFAULT_GRAPH_TIMEOUT_MS}。 */
6
+ export declare function resolveTaskTimeoutMs(requestTimeout?: number, graphTimeout?: number): number;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 整图超时:请求 config.timeout,否则图 config.timeout,否则平台缺省 1200000 毫秒(20 分钟)。
3
+ * 官方模板图上不写 timeout,用平台缺省。LLM HTTP 超时、HITL 等待另算。
4
+ */
5
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../common/types.js';
6
+ function positiveMs(value) {
7
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
8
+ return undefined;
9
+ return Math.trunc(value);
10
+ }
11
+ /** 请求显式值优先,否则图配置,否则 {@link DEFAULT_GRAPH_TIMEOUT_MS}。 */
12
+ export function resolveTaskTimeoutMs(requestTimeout, graphTimeout) {
13
+ return (positiveMs(requestTimeout) ??
14
+ positiveMs(graphTimeout) ??
15
+ DEFAULT_GRAPH_TIMEOUT_MS);
16
+ }
@@ -13,7 +13,7 @@
13
13
  * TaskTimeoutManager 配置选项
14
14
  */
15
15
  export interface TaskTimeoutManagerOptions {
16
- /** 默认超时时间(毫秒),默认 60000 (60s) */
16
+ /** 默认超时时间(毫秒),默认 {@link DEFAULT_GRAPH_TIMEOUT_MS} */
17
17
  defaultTimeoutMs?: number;
18
18
  }
19
19
  /**
@@ -9,11 +9,12 @@
9
9
  * @see design.md §1.1 任务管理 — Task级超时
10
10
  * @see requirements.md Requirement 7 AC4
11
11
  */
12
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../common/types.js';
12
13
  // ─────────────────────────────────────────────
13
14
  // Default Constants
14
15
  // ─────────────────────────────────────────────
15
- /** 默认超时:60 秒 */
16
- const DEFAULT_TIMEOUT_MS = 60_000;
16
+ /** 默认超时:与图/请求缺省同一常数 */
17
+ const DEFAULT_TIMEOUT_MS = DEFAULT_GRAPH_TIMEOUT_MS;
17
18
  /** 最大允许超时:3600 秒(1 小时) */
18
19
  const MAX_TIMEOUT_MS = 3_600_000;
19
20
  // ─────────────────────────────────────────────
@@ -84,6 +84,7 @@ export class TaskService {
84
84
  userId: params.userId,
85
85
  correlationId: params.correlationId,
86
86
  input: params.input,
87
+ timeout: params.timeout,
87
88
  output: null,
88
89
  status: 'pending',
89
90
  traceId: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gt-fe/eap-runtime",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "ioredis": "^5.4.0",
21
21
  "pg": "^8.13.0",
22
22
  "yaml": "^2.6.1",
23
- "@gt-fe/eap-contracts": "0.0.13"
23
+ "@gt-fe/eap-contracts": "0.0.14"
24
24
  },
25
25
  "publishConfig": {
26
26
  "registry": "https://registry.npmjs.org/",