@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
@@ -16,7 +16,7 @@ import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
16
16
  import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
17
17
  import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
18
18
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
19
- import { toolTraceIdentity } from '../../tool/mcp-list.js';
19
+ import { mcpToolsetCode, toolTraceIdentity } from '../../tool/mcp-list.js';
20
20
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
21
21
  import { emitRuntimeLog } from '../../common/logger.js';
22
22
  import { decideBeforeTool, } from '../../execution/interceptors.js';
@@ -92,6 +92,7 @@ export class ToolNode {
92
92
  // 4. 转换为 ToolCallRequest[](附带 manifest 路由字段)
93
93
  const requests = lastAssistantMessage.toolCalls.map((tc) => {
94
94
  const loaded = loadedTools?.[tc.name];
95
+ const toolsetCode = mcpToolsetCode(loaded);
95
96
  const correlationId = String(state['correlationId'] ?? tc.id);
96
97
  const taskId = String(state['taskId'] ?? state['correlationId'] ?? tc.id);
97
98
  const command = typeof tc.arguments?.command === 'string' ? tc.arguments.command : undefined;
@@ -114,6 +115,7 @@ export class ToolNode {
114
115
  // dev 上下文 DevMcpExecutor 直连所需;server 上下文会忽略
115
116
  execution: loaded?.manifest.execution,
116
117
  ...(loaded?.manifest.version ? { toolVersion: loaded.manifest.version } : {}),
118
+ ...(toolsetCode ? { toolsetCode } : {}),
117
119
  correlationId,
118
120
  taskId,
119
121
  ...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
@@ -506,10 +508,17 @@ export class ToolNode {
506
508
  const delegationToken = String(executionContext?.delegationToken ??
507
509
  state['delegationToken'] ??
508
510
  this.defaultAuthContext.delegationToken);
511
+ const fromState = typeof state['agentVersion'] === 'string' ? state['agentVersion'].trim() : '';
512
+ const agentDir = typeof state['agentDir'] === 'string' ? state['agentDir'].trim() : '';
513
+ const fromDir = agentDir
514
+ ? agentDir.split(/[/\\]/).filter(Boolean).pop()
515
+ : undefined;
516
+ const agentVersion = fromState || fromDir;
509
517
  return {
510
518
  userId,
511
519
  delegationToken,
512
520
  agentCode,
521
+ ...(agentVersion ? { agentVersion } : {}),
513
522
  };
514
523
  }
515
524
  shouldForceServer(state, toolName) {
@@ -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,7 +4,8 @@
4
4
  * 生产(server + EAP_PREINSTALL_ROOT):以预装为准,缺失则失败(04 Req 8.1)。
5
5
  * 开发态:Registry + 可选 ./tools 本地 manifest。
6
6
  */
7
- import { readFile } from 'node:fs/promises';
7
+ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../common/types.js';
8
+ import { readdir, readFile } from 'node:fs/promises';
8
9
  import path from 'node:path';
9
10
  import { resolveAgentModelAllowlist, resolveAgentDefaultModel } from './agent-models.js';
10
11
  import { DEFAULT_HTTP_TOOL_TIMEOUT_SEC, DEFAULT_TOOL_TIMEOUT_SEC } from '../tool/timeout.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)) {
@@ -463,16 +464,71 @@ function fromInlineToolDeclaration(tool) {
463
464
  inputSchema: tool.input,
464
465
  };
465
466
  }
467
+ const DEV_LOCAL_MANIFEST_FILES = [
468
+ 'toolset.manifest.json',
469
+ 'tool.manifest.json',
470
+ 'manifest.json',
471
+ ];
472
+ async function readJsonObject(filePath) {
473
+ try {
474
+ const raw = await readFile(filePath, 'utf-8');
475
+ const parsed = JSON.parse(raw);
476
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
477
+ return null;
478
+ return parsed;
479
+ }
480
+ catch {
481
+ return null;
482
+ }
483
+ }
484
+ function loadedToolFromDevDir(dir, toolName, doc, toolsetOnly) {
485
+ const tools = doc.tools;
486
+ if (Array.isArray(tools) && tools.length > 0 && typeof tools[0] === 'object') {
487
+ const pre = {
488
+ toolId: String(doc.code ?? path.basename(dir)),
489
+ name: String(doc.name ?? path.basename(dir)),
490
+ version: String(doc.version ?? '0.0.0'),
491
+ manifest: doc,
492
+ bundlePath: dir,
493
+ };
494
+ const expanded = expandPreinstallToolset(pre);
495
+ return expanded.find((d) => d.name === toolName) ?? null;
496
+ }
497
+ if (toolsetOnly)
498
+ return null;
499
+ return fromLocalManifestFile(toolName, doc);
500
+ }
501
+ async function loadDevLocalManifestFromDir(dir, toolName, toolsetOnly) {
502
+ const files = toolsetOnly ? ['toolset.manifest.json'] : DEV_LOCAL_MANIFEST_FILES;
503
+ for (const file of files) {
504
+ const doc = await readJsonObject(path.join(dir, file));
505
+ if (!doc)
506
+ continue;
507
+ const hit = loadedToolFromDevDir(dir, toolName, doc, toolsetOnly);
508
+ if (hit)
509
+ return hit;
510
+ }
511
+ return null;
512
+ }
466
513
  async function loadDevLocalManifest(toolName, toolsDir) {
467
- const manifestPath = path.join(toolsDir, toolName, 'tool.manifest.json');
514
+ const direct = await loadDevLocalManifestFromDir(path.join(toolsDir, toolName), toolName, false);
515
+ if (direct)
516
+ return direct;
517
+ let entries;
468
518
  try {
469
- const raw = await readFile(manifestPath, 'utf-8');
470
- const manifest = JSON.parse(raw);
471
- return fromLocalManifestFile(toolName, manifest);
519
+ entries = await readdir(toolsDir, { withFileTypes: true });
472
520
  }
473
521
  catch {
474
522
  return null;
475
523
  }
524
+ for (const ent of entries) {
525
+ if (!ent.isDirectory() || ent.name === toolName)
526
+ continue;
527
+ const hit = await loadDevLocalManifestFromDir(path.join(toolsDir, ent.name), toolName, true);
528
+ if (hit)
529
+ return hit;
530
+ }
531
+ return null;
476
532
  }
477
533
  function fromLocalInstall(install) {
478
534
  return {
@@ -703,7 +759,7 @@ export class GraphLoader {
703
759
  },
704
760
  ],
705
761
  config: {
706
- timeout: manifest.config?.timeout ?? 60000,
762
+ timeout: manifest.config?.timeout ?? DEFAULT_GRAPH_TIMEOUT_MS,
707
763
  maxRetries: manifest.config?.retryCount,
708
764
  },
709
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
  /**