@gt-fe/eap-runtime 0.0.23 → 0.0.25

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 (39) hide show
  1. package/README.md +1 -0
  2. package/dist/api/handlers/invoke.handler.d.ts +4 -0
  3. package/dist/api/handlers/invoke.handler.js +69 -37
  4. package/dist/api/handlers/stream.handler.d.ts +4 -0
  5. package/dist/api/handlers/stream.handler.js +40 -21
  6. package/dist/api/routes/agent.routes.js +6 -5
  7. package/dist/checkpoint/checkpoint-ref-notify.js +1 -1
  8. package/dist/common/types.d.ts +6 -2
  9. package/dist/common/types.js +3 -1
  10. package/dist/graph/adapters/langgraph.adapter.d.ts +6 -4
  11. package/dist/graph/adapters/langgraph.adapter.js +103 -18
  12. package/dist/graph/contracts/executable-graph.d.ts +11 -2
  13. package/dist/graph/execution-trace-context.d.ts +4 -0
  14. package/dist/graph/execution-trace-context.js +3 -1
  15. package/dist/graph/nodes/rag/types.d.ts +4 -1
  16. package/dist/providers/http-llm-provider.js +25 -0
  17. package/dist/registry/graph-loader.js +5 -4
  18. package/dist/runtime/agent-runtime.d.ts +4 -1
  19. package/dist/runtime/agent-runtime.js +110 -20
  20. package/dist/runtime/dev-runtime.d.ts +1 -0
  21. package/dist/runtime/dev-runtime.js +9 -2
  22. package/dist/runtime/types.d.ts +1 -1
  23. package/dist/session/apply-session-compression.d.ts +6 -4
  24. package/dist/session/apply-session-compression.js +24 -11
  25. package/dist/session/index.d.ts +1 -1
  26. package/dist/session/index.js +1 -1
  27. package/dist/session/persist-turn-messages.d.ts +50 -9
  28. package/dist/session/persist-turn-messages.js +133 -15
  29. package/dist/sse/events/response-events.d.ts +2 -2
  30. package/dist/sse/events/response-events.js +1 -1
  31. package/dist/task/contracts/task-manager.contract.d.ts +2 -0
  32. package/dist/task/index.d.ts +1 -0
  33. package/dist/task/index.js +1 -0
  34. package/dist/task/resolve-task-timeout.d.ts +6 -0
  35. package/dist/task/resolve-task-timeout.js +16 -0
  36. package/dist/task/task-timeout.d.ts +1 -1
  37. package/dist/task/task-timeout.js +3 -2
  38. package/dist/task/task.service.js +1 -0
  39. package/package.json +2 -2
@@ -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()
@@ -239,10 +239,19 @@ export interface GraphState {
239
239
  /** 最近一次 createNode 的 span id;Checkpoint 引用挂在此 Span 上 */
240
240
  lastSpanId?: string;
241
241
  /**
242
- * HITL 续跑:入口 messages 以 __set 覆盖 checkpoint 历史,避免 append 重复。
243
- * 仅 stream/invoke 入口使用,不入持久化语义。
242
+ * 入口 messages 以 __set 覆盖 checkpoint 历史,避免 append 重复。
243
+ * HITL 续跑与会话压缩写回最新 Checkpoint 使用。不入持久化语义。
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';
@@ -47,10 +48,12 @@ export interface AgentRuntimeDeps {
47
48
  */
48
49
  traceCollector?: ITraceCollector;
49
50
  recordForkRelation?: RecordForkRelation;
50
- /** 历史压缩;默认关。见 EAP_RUNTIME_COMPRESSION */
51
+ /** 历史压缩;默认开。EAP_RUNTIME_COMPRESSION=0 关闭 */
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 {