@gt-fe/eap-runtime 0.0.21 → 0.0.22

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/api/handlers/invoke.handler.js +6 -3
  3. package/dist/api/handlers/stream.handler.js +6 -2
  4. package/dist/checkpoint/file-checkpoint-saver.d.ts +4 -0
  5. package/dist/checkpoint/file-checkpoint-saver.js +51 -22
  6. package/dist/checkpoint/index.d.ts +1 -1
  7. package/dist/checkpoint/index.js +1 -1
  8. package/dist/graph/adapters/langgraph-checkpointer.d.ts +11 -0
  9. package/dist/graph/adapters/langgraph-checkpointer.js +48 -9
  10. package/dist/graph/adapters/langgraph.adapter.d.ts +8 -0
  11. package/dist/graph/adapters/langgraph.adapter.js +58 -16
  12. package/dist/graph/adapters/skill-isolate.d.ts +1 -5
  13. package/dist/graph/adapters/skill-isolate.js +1 -12
  14. package/dist/graph/adapters/subgraph-compiler.js +12 -15
  15. package/dist/graph/contracts/executable-graph.d.ts +6 -0
  16. package/dist/graph/hitl/hitl-handler.d.ts +13 -4
  17. package/dist/graph/hitl/hitl-handler.js +78 -7
  18. package/dist/graph/nodes/tool-node.js +1 -5
  19. package/dist/providers/index.d.ts +1 -1
  20. package/dist/providers/index.js +1 -1
  21. package/dist/providers/local-llm-provider.d.ts +6 -0
  22. package/dist/providers/local-llm-provider.js +29 -5
  23. package/dist/providers/routing-llm-provider.d.ts +2 -2
  24. package/dist/providers/routing-llm-provider.js +5 -15
  25. package/dist/registry/deployment-snapshot.js +18 -4
  26. package/dist/registry/graph-loader.d.ts +2 -0
  27. package/dist/registry/graph-loader.js +39 -2
  28. package/dist/registry/preload-flags.d.ts +1 -1
  29. package/dist/registry/preload-flags.js +1 -1
  30. package/dist/runtime/agent-runtime.js +27 -10
  31. package/dist/runtime/dev-runtime.d.ts +2 -1
  32. package/dist/runtime/dev-runtime.js +9 -3
  33. package/dist/session/apply-session-compression.d.ts +4 -0
  34. package/dist/session/apply-session-compression.js +22 -3
  35. package/dist/session/index.d.ts +1 -1
  36. package/dist/session/index.js +1 -1
  37. package/dist/session/persist-turn-messages.d.ts +6 -1
  38. package/dist/session/persist-turn-messages.js +23 -5
  39. package/dist/token/record-task-token-usage.d.ts +23 -0
  40. package/dist/token/record-task-token-usage.js +20 -0
  41. package/dist/tool/tool-confirmation.js +8 -5
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -164,7 +164,7 @@ DSL / 契约细节见 `.kiro/specs/04-runtime-core/design.md` §1.4 与 `src/gra
164
164
  |------|------|------|
165
165
  | `EAP_PREINSTALL_ROOT` | — | 预装根;也可用 `GraphLoader({ preinstallRoot })` |
166
166
  | `EAP_PRELOAD_REQUIRE_READY` | 关 | 缺 `.ready` → `runtime.preload.missing` |
167
- | `EAP_PRELOAD_REQUIRE_CHECKSUM` | 关 | snapshot 须含 sha256 或 artifacts。加载期比对文件摘要,不匹配则 warn、不中断 |
167
+ | `EAP_PRELOAD_REQUIRE_CHECKSUM` | 关 | snapshot 须含 sha256 或 artifacts。开启时摘要不匹配抛 `RUNTIME_PRELOAD_PACKAGE_CORRUPT`;未开仍 warn、不中断 |
168
168
  | `EAP_PRELOAD_USER_SKILL_OVERRIDE` | 关 | `users` 优先于 `shared` |
169
169
  | `EAP_PRELOAD_VALIDATE_SKILL_IO` | 关 | 校验 Skill `inputDef.required`(`state.skillInput`) |
170
170
  | `EAP_PRELOAD_DEFINITION_CACHE` | 有预装时开 | 定义缓存;`0`/`false` 关闭,`1`/`true` 强制开 |
@@ -28,7 +28,7 @@ import { assembleLoadedTools } from '../../tool/harness-primitives.js';
28
28
  import { resolveAgentCode } from '../../common/agent-code.js';
29
29
  import { unwrapHitlPaused } from '../../graph/hitl/hitl-handler.js';
30
30
  import { resolveTraceId } from '../../common/trace-id.js';
31
- import { applySessionCompression, } from '../../session/apply-session-compression.js';
31
+ import { applySessionCompression, withCompressionSummaryModel, } from '../../session/apply-session-compression.js';
32
32
  import { applySessionMemory, } from '../../session/apply-session-memory.js';
33
33
  // ─────────────────────────────────────────────
34
34
  // Default Graph DSL fallback
@@ -199,7 +199,9 @@ export class InvokeOrchestrator {
199
199
  sessionId,
200
200
  userId: effectiveUserId,
201
201
  taskId: task.id,
202
- options: this.sessionCompression,
202
+ options: await withCompressionSummaryModel(this.sessionCompression, () => this.graphLoader
203
+ ? this.graphLoader.resolveDefaultModel(agentCode, req.agentVersion)
204
+ : Promise.resolve(undefined)),
203
205
  }),
204
206
  userId: effectiveUserId,
205
207
  agentCode,
@@ -356,6 +358,7 @@ export class InvokeOrchestrator {
356
358
  ? graphResult.finalState.messages
357
359
  : [];
358
360
  const newMessages = sessionVisibleGraphMessages(allMessages.slice(initialMessagesCount), finishPoints);
361
+ const duration = Date.now() - startTime;
359
362
  await persistTurnTranscript(this.messageService, {
360
363
  sessionId,
361
364
  taskId: task.id,
@@ -363,9 +366,9 @@ export class InvokeOrchestrator {
363
366
  messages: newMessages,
364
367
  fallbackAssistantContent: graphResult.output ?? '',
365
368
  assistantTokenCount: tokenUsage.output,
369
+ assistantDurationMs: duration,
366
370
  });
367
371
  // ─── Step 8: 返回 InvokeResponse ───
368
- const duration = Date.now() - startTime;
369
372
  // 从本轮 Graph 执行结果中提取 tool 事件明细(供 CLI / UI 渲染 ReAct 循环)
370
373
  const toolEvents = extractToolEvents(newMessages);
371
374
  return this.buildResponse(traceId, {
@@ -23,7 +23,7 @@ import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscript
23
23
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
24
24
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
25
25
  import { recordTaskTokenUsages } from '../../token/record-task-token-usage.js';
26
- import { iterSessionCompression, } from '../../session/apply-session-compression.js';
26
+ import { iterSessionCompression, withCompressionSummaryModel, } from '../../session/apply-session-compression.js';
27
27
  import { applySessionMemory, } from '../../session/apply-session-memory.js';
28
28
  import { resolveTokenDetail, shouldEmitTokenSse } from '../../token/token-detail.js';
29
29
  import { SSEEmitter } from '../../sse/sse-emitter.js';
@@ -215,12 +215,15 @@ export class StreamOrchestrator {
215
215
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.graphLoader, agentCode, req.agentVersion);
216
216
  // 组装 GraphState:有 Checkpoint 历史则只追加本轮 user,避免双重 append
217
217
  let history = messageRecordsToGraphMessages(sessionContext.messages);
218
+ const compressionOptions = await withCompressionSummaryModel(this.sessionCompression, () => this.graphLoader
219
+ ? this.graphLoader.resolveDefaultModel(agentCode, req.agentVersion)
220
+ : Promise.resolve(undefined));
218
221
  for await (const step of iterSessionCompression({
219
222
  history,
220
223
  sessionId,
221
224
  userId,
222
225
  taskId,
223
- options: this.sessionCompression,
226
+ options: compressionOptions,
224
227
  })) {
225
228
  if (step.kind === 'history') {
226
229
  history = step.messages;
@@ -405,6 +408,7 @@ export class StreamOrchestrator {
405
408
  assistantReasoningContent,
406
409
  }),
407
410
  assistantTokenCount: tokenStats.total.output ?? 0,
411
+ assistantDurationMs: duration,
408
412
  });
409
413
  // 关闭 SSE 连接
410
414
  emitter.close();
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * FileCheckpointSaver — 本地 JSON 文件存储
3
3
  * 适用场景:本地开发、单机部署
4
+ * 目录名对 threadId 做 encodeURIComponent:子图槽 `sessionId#父节点id` 不会变成带 `#` 的目录。
4
5
  */
5
6
  import type { ICheckpointSaver } from './contracts/checkpoint-saver.contract.js';
6
7
  import type { CheckpointData } from '../graph/contracts/executable-graph.js';
8
+ /** 存储键可含 `#`(子图槽);目录名百分号编码,避免 URL/Markdown 把 `#` 当片段。 */
9
+ export declare function checkpointFileThreadDir(baseDir: string, threadId: string): string;
7
10
  export declare class FileCheckpointSaver implements ICheckpointSaver {
8
11
  private readonly baseDir;
9
12
  constructor(baseDir: string);
@@ -11,6 +14,7 @@ export declare class FileCheckpointSaver implements ICheckpointSaver {
11
14
  load(threadId: string): Promise<CheckpointData | null>;
12
15
  loadByCheckpointId(checkpointId: string, threadId?: string): Promise<CheckpointData | null>;
13
16
  list(threadId: string): Promise<CheckpointData[]>;
17
+ private readLatestId;
14
18
  private readCheckpoint;
15
19
  /**
16
20
  * Fork: 从指定 Checkpoint 创建新的执行线程
@@ -1,36 +1,51 @@
1
1
  /**
2
2
  * FileCheckpointSaver — 本地 JSON 文件存储
3
3
  * 适用场景:本地开发、单机部署
4
+ * 目录名对 threadId 做 encodeURIComponent:子图槽 `sessionId#父节点id` 不会变成带 `#` 的目录。
4
5
  */
5
6
  import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
6
7
  import { join } from 'node:path';
7
8
  import { stringifyJsonSafe } from './checkpoint-serializer.js';
9
+ /** 存储键可含 `#`(子图槽);目录名百分号编码,避免 URL/Markdown 把 `#` 当片段。 */
10
+ export function checkpointFileThreadDir(baseDir, threadId) {
11
+ return join(baseDir, encodeURIComponent(threadId));
12
+ }
13
+ function threadDirCandidates(baseDir, threadId) {
14
+ const encoded = checkpointFileThreadDir(baseDir, threadId);
15
+ const raw = join(baseDir, threadId);
16
+ return encoded === raw ? [encoded] : [encoded, raw];
17
+ }
8
18
  export class FileCheckpointSaver {
9
19
  baseDir;
10
20
  constructor(baseDir) {
11
21
  this.baseDir = baseDir;
12
22
  }
13
23
  async save(threadId, data) {
14
- const dir = join(this.baseDir, threadId);
24
+ const dir = checkpointFileThreadDir(this.baseDir, threadId);
15
25
  await mkdir(dir, { recursive: true });
16
26
  await writeFile(join(dir, `${data.checkpointId}.json`), stringifyJsonSafe(data));
17
27
  // 更新 latest 指针
18
28
  await writeFile(join(dir, 'latest.json'), JSON.stringify({ checkpointId: data.checkpointId }));
19
29
  }
20
30
  async load(threadId) {
21
- try {
22
- const latestRaw = await readFile(join(this.baseDir, threadId, 'latest.json'), 'utf-8');
23
- const { checkpointId } = JSON.parse(latestRaw);
24
- return this.loadByCheckpointId(checkpointId, threadId);
25
- }
26
- catch {
27
- return null;
31
+ for (const dir of threadDirCandidates(this.baseDir, threadId)) {
32
+ const checkpointId = await this.readLatestId(dir);
33
+ if (!checkpointId)
34
+ continue;
35
+ const data = await this.readCheckpoint(join(dir, `${checkpointId}.json`));
36
+ if (data)
37
+ return data;
28
38
  }
39
+ return null;
29
40
  }
30
41
  async loadByCheckpointId(checkpointId, threadId) {
31
- // If threadId provided, look directly; otherwise scan all thread dirs
32
42
  if (threadId) {
33
- return this.readCheckpoint(join(this.baseDir, threadId, `${checkpointId}.json`));
43
+ for (const dir of threadDirCandidates(this.baseDir, threadId)) {
44
+ const data = await this.readCheckpoint(join(dir, `${checkpointId}.json`));
45
+ if (data)
46
+ return data;
47
+ }
48
+ return null;
34
49
  }
35
50
  // ponytail: brute scan — upgrade to index file if perf matters
36
51
  try {
@@ -45,21 +60,35 @@ export class FileCheckpointSaver {
45
60
  return null;
46
61
  }
47
62
  async list(threadId) {
48
- const dir = join(this.baseDir, threadId);
49
- try {
50
- const files = await readdir(dir);
51
- const results = [];
52
- for (const f of files) {
53
- if (f === 'latest.json' || !f.endsWith('.json'))
54
- continue;
55
- const data = await this.readCheckpoint(join(dir, f));
56
- if (data)
57
- results.push(data);
63
+ for (const dir of threadDirCandidates(this.baseDir, threadId)) {
64
+ try {
65
+ const files = await readdir(dir);
66
+ const results = [];
67
+ for (const f of files) {
68
+ if (f === 'latest.json' || !f.endsWith('.json'))
69
+ continue;
70
+ const data = await this.readCheckpoint(join(dir, f));
71
+ if (data)
72
+ results.push(data);
73
+ }
74
+ if (results.length > 0) {
75
+ return results.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
76
+ }
77
+ }
78
+ catch {
79
+ // try next candidate
58
80
  }
59
- return results.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
81
+ }
82
+ return [];
83
+ }
84
+ async readLatestId(dir) {
85
+ try {
86
+ const latestRaw = await readFile(join(dir, 'latest.json'), 'utf-8');
87
+ const { checkpointId } = JSON.parse(latestRaw);
88
+ return typeof checkpointId === 'string' && checkpointId.trim() ? checkpointId : null;
60
89
  }
61
90
  catch {
62
- return [];
91
+ return null;
63
92
  }
64
93
  }
65
94
  async readCheckpoint(filePath) {
@@ -6,7 +6,7 @@ export { restoreCheckpoint } from './restore-checkpoint.js';
6
6
  export { notifyCheckpointRef, stripCheckpointCollectors } from './checkpoint-ref-notify.js';
7
7
  export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, stringifyJsonSafe, toJsonSafe, } from './checkpoint-serializer.js';
8
8
  export type { CreateCheckpointParams } from './checkpoint-serializer.js';
9
- export { FileCheckpointSaver } from './file-checkpoint-saver.js';
9
+ export { FileCheckpointSaver, checkpointFileThreadDir } from './file-checkpoint-saver.js';
10
10
  export { InMemoryCheckpointSaver } from './in-memory-checkpoint-saver.js';
11
11
  export { MongoCheckpointSaver } from './mongo-checkpoint-saver.js';
12
12
  export type { IMongoCollection, CheckpointDocument } from './mongo-checkpoint-saver.js';
@@ -5,7 +5,7 @@ export { CompositeCheckpointSaver } from './composite-checkpoint-saver.js';
5
5
  export { restoreCheckpoint } from './restore-checkpoint.js';
6
6
  export { notifyCheckpointRef, stripCheckpointCollectors } from './checkpoint-ref-notify.js';
7
7
  export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, stringifyJsonSafe, toJsonSafe, } from './checkpoint-serializer.js';
8
- export { FileCheckpointSaver } from './file-checkpoint-saver.js';
8
+ export { FileCheckpointSaver, checkpointFileThreadDir } from './file-checkpoint-saver.js';
9
9
  export { InMemoryCheckpointSaver } from './in-memory-checkpoint-saver.js';
10
10
  export { MongoCheckpointSaver } from './mongo-checkpoint-saver.js';
11
11
  export { PostgresCheckpointSaver } from './pg-checkpoint-saver.js';
@@ -29,6 +29,7 @@ export declare class LangGraphCheckpointer extends BaseCheckpointSaver {
29
29
  putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
30
30
  list(config: RunnableConfig, options?: ListOptions): AsyncIterableIterator<CheckpointTuple>;
31
31
  private extractThreadId;
32
+ private extractCheckpointNs;
32
33
  private checkpointDataToTuple;
33
34
  /**
34
35
  * 优先还原 put() 时写入的 engineSnapshot;旧数据兜底补齐 pending_sends=[]。
@@ -37,4 +38,14 @@ export declare class LangGraphCheckpointer extends BaseCheckpointSaver {
37
38
  private checkpointToData;
38
39
  private extractExtraState;
39
40
  }
41
+ /**
42
+ * ICheckpointSaver 仍按一个 threadId 分槽。
43
+ * 父图 ns 为空,槽位=threadId(HITL/多轮仍等于 sessionId)。
44
+ * 子图 ns 非空,槽位=threadId#ns,不覆盖父 latest。
45
+ */
46
+ export declare function checkpointStorageThreadId(threadId: string, checkpointNs?: string): string;
47
+ /** 在上一份路径后追加当前节点 id;空 id 或与末项相同则不追加。 */
48
+ export declare function nextHistoryPath(previous: string[] | undefined, currentNodeId: string): string[];
49
+ /** 优先用图状态 channel;旧 checkpoint 没有该 channel 时退回按 currentNode 追加。 */
50
+ export declare function historyPathFromChannel(channel: unknown, previousHistoryPath: string[] | undefined, currentNodeId: string): string[];
40
51
  export {};
@@ -33,7 +33,7 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
33
33
  data = await this.saver.loadByCheckpointId(checkpointId);
34
34
  }
35
35
  else {
36
- data = await this.saver.load(threadId);
36
+ data = await this.saver.load(checkpointStorageThreadId(threadId, this.extractCheckpointNs(config)));
37
37
  }
38
38
  if (!data) {
39
39
  return undefined;
@@ -42,8 +42,10 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
42
42
  }
43
43
  async put(config, checkpoint, metadata, _newVersions) {
44
44
  const threadId = this.extractThreadId(config) ?? `thread-${Date.now()}`;
45
- const data = this.checkpointToData(threadId, checkpoint, metadata);
46
- await this.saver.save(threadId, stripCheckpointCollectors(data));
45
+ const storageThreadId = checkpointStorageThreadId(threadId, this.extractCheckpointNs(config));
46
+ const previous = await this.saver.load(storageThreadId);
47
+ const data = this.checkpointToData(storageThreadId, checkpoint, previous?.historyPath);
48
+ await this.saver.save(storageThreadId, stripCheckpointCollectors(data));
47
49
  await notifyCheckpointRef(data);
48
50
  return {
49
51
  ...config,
@@ -80,7 +82,7 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
80
82
  if (!threadId) {
81
83
  return;
82
84
  }
83
- let checkpoints = await this.saver.list(threadId);
85
+ let checkpoints = await this.saver.list(checkpointStorageThreadId(threadId, this.extractCheckpointNs(config)));
84
86
  checkpoints.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
85
87
  if (options?.before) {
86
88
  const beforeId = options.before.configurable?.['checkpoint_id'];
@@ -101,6 +103,9 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
101
103
  extractThreadId(config) {
102
104
  return config.configurable?.['thread_id'];
103
105
  }
106
+ extractCheckpointNs(config) {
107
+ return String(config.configurable?.['checkpoint_ns'] ?? '').trim();
108
+ }
104
109
  checkpointDataToTuple(data, config) {
105
110
  const checkpoint = this.restoreLangGraphCheckpoint(data);
106
111
  const metadata = {
@@ -112,7 +117,7 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
112
117
  ...config,
113
118
  configurable: {
114
119
  ...config.configurable,
115
- thread_id: data.threadId,
120
+ thread_id: this.extractThreadId(config) ?? data.threadId,
116
121
  checkpoint_id: data.checkpointId,
117
122
  },
118
123
  };
@@ -162,20 +167,20 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
162
167
  channel_values: {
163
168
  messages: data.state.messages ?? [],
164
169
  currentNode: data.currentNodeId,
170
+ historyPath: data.historyPath ?? [],
165
171
  ...this.extractExtraState(data),
166
172
  },
167
- channel_versions: { messages: 1, currentNode: 1 },
173
+ channel_versions: { messages: 1, currentNode: 1, historyPath: 1 },
168
174
  versions_seen: {},
169
175
  pending_sends: [],
170
176
  };
171
177
  }
172
- checkpointToData(threadId, checkpoint, metadata) {
178
+ checkpointToData(threadId, checkpoint, previousHistoryPath) {
173
179
  const messages = Array.isArray(checkpoint.channel_values['messages'])
174
180
  ? checkpoint.channel_values['messages']
175
181
  : [];
176
182
  const currentNodeId = checkpoint.channel_values['currentNode'] ?? '';
177
- const step = Number.isFinite(metadata.step) ? Math.max(0, Math.min(metadata.step, 1000)) : 0;
178
- const historyPath = Array.from({ length: step }, () => 'node');
183
+ const historyPath = historyPathFromChannel(checkpoint.channel_values['historyPath'], previousHistoryPath, currentNodeId);
179
184
  const raw = checkpoint;
180
185
  return {
181
186
  checkpointId: checkpoint.id,
@@ -211,3 +216,37 @@ export class LangGraphCheckpointer extends BaseCheckpointSaver {
211
216
  return rest;
212
217
  }
213
218
  }
219
+ /**
220
+ * ICheckpointSaver 仍按一个 threadId 分槽。
221
+ * 父图 ns 为空,槽位=threadId(HITL/多轮仍等于 sessionId)。
222
+ * 子图 ns 非空,槽位=threadId#ns,不覆盖父 latest。
223
+ */
224
+ export function checkpointStorageThreadId(threadId, checkpointNs) {
225
+ const ns = (checkpointNs ?? '').trim().replace(/[/\\]/g, '_');
226
+ if (!ns)
227
+ return threadId;
228
+ return `${threadId}#${ns}`;
229
+ }
230
+ /** 在上一份路径后追加当前节点 id;空 id 或与末项相同则不追加。 */
231
+ export function nextHistoryPath(previous, currentNodeId) {
232
+ const path = Array.isArray(previous) ? [...previous] : [];
233
+ const id = currentNodeId.trim();
234
+ if (!id)
235
+ return path;
236
+ if (path[path.length - 1] === id)
237
+ return path;
238
+ path.push(id);
239
+ if (path.length > 1000)
240
+ return path.slice(path.length - 1000);
241
+ return path;
242
+ }
243
+ /** 优先用图状态 channel;旧 checkpoint 没有该 channel 时退回按 currentNode 追加。 */
244
+ export function historyPathFromChannel(channel, previousHistoryPath, currentNodeId) {
245
+ if (Array.isArray(channel) && channel.length > 0) {
246
+ const path = channel.filter((id) => typeof id === 'string' && id.trim().length > 0);
247
+ if (path.length > 0) {
248
+ return path.length > 1000 ? path.slice(path.length - 1000) : path;
249
+ }
250
+ }
251
+ return nextHistoryPath(previousHistoryPath, currentNodeId);
252
+ }
@@ -19,6 +19,12 @@ import { type ConditionEvaluatorFn } from '../condition-evaluator.js';
19
19
  import type { ExecutionInterceptors } from '../../execution/interceptors.js';
20
20
  /** 图未写 `config.maxIterations` 时的 recursionLimit。`agent ⇄ tools` 一圈约 2,50 ≈ 24 次工具往返。 */
21
21
  export declare const DEFAULT_GRAPH_RECURSION_LIMIT = 50;
22
+ /**
23
+ * LangGraph `recursionLimit` 计 superstep(每进一个节点 +1),不是 ReAct 圈数。
24
+ * 图上 `config.maxIterations` 优先;缺省 {@link DEFAULT_GRAPH_RECURSION_LIMIT}。
25
+ */
26
+ /** 并行节点各自返回 [id] 时合并进同一路径;末项相同不重复。 */
27
+ export declare function mergeHistoryPath(current: unknown, update: unknown): string[];
22
28
  /**
23
29
  * LangGraphAdapter — 基于 @langchain/langgraph 的 IStateGraphEngine 实现
24
30
  *
@@ -112,10 +118,12 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
112
118
  * 本方法无 DSL,无法编译出可用 `_engineInstance`。
113
119
  */
114
120
  restore(checkpointData: CheckpointData): Promise<ExecutableGraph>;
121
+ private checkpointConfigurable;
115
122
  /**
116
123
  * 根据节点类型创建对应的执行函数
117
124
  */
118
125
  private createNodeFunction;
126
+ private createNodeFunctionBody;
119
127
  private createSkillBindNodeFunction;
120
128
  private buildSkillCatalog;
121
129
  private createToolNodeFunction;
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { StateGraph, START, END, } from '@langchain/langgraph';
11
11
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
12
+ import { checkpointStorageThreadId } from './langgraph-checkpointer.js';
12
13
  import { LangGraphCheckpointer } from './langgraph-checkpointer.js';
13
14
  import { SubGraphCompiler } from './subgraph-compiler.js';
14
15
  import { assertEmbeddedSubgraphsAreFlat } from './graph-nesting.js';
@@ -44,6 +45,24 @@ export const DEFAULT_GRAPH_RECURSION_LIMIT = 50;
44
45
  * LangGraph `recursionLimit` 计 superstep(每进一个节点 +1),不是 ReAct 圈数。
45
46
  * 图上 `config.maxIterations` 优先;缺省 {@link DEFAULT_GRAPH_RECURSION_LIMIT}。
46
47
  */
48
+ /** 并行节点各自返回 [id] 时合并进同一路径;末项相同不重复。 */
49
+ export function mergeHistoryPath(current, update) {
50
+ const path = Array.isArray(current)
51
+ ? current.filter((id) => typeof id === 'string' && id.trim().length > 0)
52
+ : [];
53
+ const add = Array.isArray(update) ? update : [];
54
+ for (const raw of add) {
55
+ if (typeof raw !== 'string')
56
+ continue;
57
+ const id = raw.trim();
58
+ if (!id)
59
+ continue;
60
+ if (path[path.length - 1] === id)
61
+ continue;
62
+ path.push(id);
63
+ }
64
+ return path.length > 1000 ? path.slice(path.length - 1000) : path;
65
+ }
47
66
  function langGraphRunOptions(dsl, extra) {
48
67
  const raw = dsl.config?.maxIterations;
49
68
  const recursionLimit = typeof raw === 'number' && Number.isFinite(raw) && raw > 0
@@ -365,6 +384,10 @@ export class LangGraphAdapter {
365
384
  value: (_current, update) => update,
366
385
  default: () => '',
367
386
  },
387
+ historyPath: {
388
+ value: (current, update) => mergeHistoryPath(current, update),
389
+ default: () => [],
390
+ },
368
391
  // ── 运行期上下文(ephemeral):由 invoke/stream 入口注入,节点只读 ──
369
392
  userId: ephemeral(''),
370
393
  // agentId 已废弃:channel 保留仅为旧 Checkpoint 反序列化兼容,新代码只写 agentCode
@@ -375,6 +398,7 @@ export class LangGraphAdapter {
375
398
  correlationId: ephemeral(''),
376
399
  taskId: ephemeral(''),
377
400
  sessionId: ephemeral(''),
401
+ checkpointNs: ephemeral(undefined),
378
402
  skillIsolateContext: ephemeral(undefined),
379
403
  loadedTools: ephemeral(undefined),
380
404
  loadedSkills: ephemeral(undefined),
@@ -511,13 +535,7 @@ export class LangGraphAdapter {
511
535
  const producer = runWithExecutionTrace({ ...traceCtx, stream: streamBag }, () => (async () => {
512
536
  try {
513
537
  const streamIterator = await ref.compiled.stream(input, langGraphRunOptions(ref.dsl, {
514
- configurable: {
515
- thread_id: resolveCheckpointThreadId({
516
- sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
517
- taskId: state.taskId,
518
- graphId: graph.id,
519
- }),
520
- },
538
+ configurable: this.checkpointConfigurable(state, graph),
521
539
  streamMode: 'updates',
522
540
  }));
523
541
  for await (const update of streamIterator) {
@@ -557,13 +575,7 @@ export class LangGraphAdapter {
557
575
  const traceCtx = executionTraceFromState(state);
558
576
  // 只补 collector/traceId。子图套娃时外层 stream 袋经 ?? 保留。
559
577
  const result = await runWithExecutionTrace(traceCtx, () => ref.compiled.invoke(input, langGraphRunOptions(ref.dsl, {
560
- configurable: {
561
- thread_id: resolveCheckpointThreadId({
562
- sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
563
- taskId: state.taskId,
564
- graphId: graph.id,
565
- }),
566
- },
578
+ configurable: this.checkpointConfigurable(state, graph),
567
579
  })));
568
580
  const duration = Date.now() - startTime;
569
581
  const messages = Array.isArray(result['messages']) ? result['messages'] : [];
@@ -656,10 +668,35 @@ export class LangGraphAdapter {
656
668
  // ─────────────────────────────────────────────
657
669
  // Private helpers
658
670
  // ─────────────────────────────────────────────
671
+ checkpointConfigurable(state, graph) {
672
+ const thread_id = resolveCheckpointThreadId({
673
+ sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
674
+ taskId: state.taskId,
675
+ graphId: graph.id,
676
+ });
677
+ const ns = typeof state.checkpointNs === 'string' ? state.checkpointNs.trim() : '';
678
+ // 子图是独立 compile 再 invoke,LangGraph put 不一定带回 checkpoint_ns;槽位编进 thread_id。
679
+ return { thread_id: checkpointStorageThreadId(thread_id, ns) };
680
+ }
659
681
  /**
660
682
  * 根据节点类型创建对应的执行函数
661
683
  */
662
684
  createNodeFunction(node) {
685
+ const impl = this.createNodeFunctionBody(node);
686
+ return async (state) => {
687
+ const update = await impl(state);
688
+ const patch = update && typeof update === 'object' ? { ...update } : {};
689
+ const currentNode = typeof patch['currentNode'] === 'string' && String(patch['currentNode']).trim()
690
+ ? String(patch['currentNode']).trim()
691
+ : node.id;
692
+ if (!Array.isArray(patch['historyPath'])) {
693
+ patch['historyPath'] = [currentNode];
694
+ }
695
+ patch['currentNode'] = currentNode;
696
+ return patch;
697
+ };
698
+ }
699
+ createNodeFunctionBody(node) {
663
700
  switch (node.type) {
664
701
  case 'llm':
665
702
  return this.createLLMNodeFunction(node);
@@ -1366,7 +1403,10 @@ export class LangGraphAdapter {
1366
1403
  if (msg.role === 'assistant' && Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0) {
1367
1404
  // tool.called 由 Tool 节点在真正执行时发出,此处不再把 LLM function-call 当执行开始
1368
1405
  // 子图节点的 updates 是内层已 stream 过的回写,不要再发空 reasoning chunk
1369
- if (subgraphNodeIds.has(nodeId) || silentNodeIds.has(nodeId)) {
1406
+ if (silentNodeIds.has(nodeId)) {
1407
+ continue;
1408
+ }
1409
+ if (subgraphNodeIds.has(nodeId) && streamedTextThisTurn) {
1370
1410
  continue;
1371
1411
  }
1372
1412
  const hasText = typeof msg.content === 'string' && msg.content.trim().length > 0;
@@ -1393,7 +1433,9 @@ export class LangGraphAdapter {
1393
1433
  continue;
1394
1434
  }
1395
1435
  if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.length > 0) {
1396
- if (silentNodeIds.has(nodeId) || subgraphNodeIds.has(nodeId))
1436
+ if (silentNodeIds.has(nodeId))
1437
+ continue;
1438
+ if (subgraphNodeIds.has(nodeId) && streamedTextThisTurn)
1397
1439
  continue;
1398
1440
  // 已通过 onChunk 推送过 token,避免整段重复
1399
1441
  if (streamedTextThisTurn) {
@@ -1,17 +1,13 @@
1
1
  /**
2
2
  * Skill 子图可选隔离(实验口)。仅节点 `isolateContext: true`。
3
3
  * 只把本轮输入给子图,回写最后一条带正文的 assistant;没有则不回写。
4
- * 隔离时不写父 session Checkpoint。
5
- * 子图内 HITL 未按隔离线程设计:skillIsolateContext 为 true 时禁止 interrupt。
4
+ * 子图 Checkpoint / HITL 走 `sessionId#父节点id`,不写父 latest。
6
5
  */
7
6
  import type { GraphMessage } from '../contracts/executable-graph.js';
8
7
  import type { GraphNode } from '../contracts/executable-graph.js';
9
- export declare const SKILL_ISOLATE_HITL_MESSAGE = "Skill isolateContext \u4E0D\u80FD\u4E0E\u5B50\u56FE\u5185\u4EBA\u5DE5\u786E\u8BA4\u4E00\u8D77\u4F7F\u7528\u3002\u5173\u6389 isolate\uFF0C\u6216\u4E0D\u8981\u5728\u8BE5 Skill \u91CC\u4F7F\u7528 require_confirmation / beforeTool ask\u3002";
10
8
  /** 只认节点配置。进程环境变量不会给所有 subgraph 开隔离。 */
11
9
  export declare function isSkillContextIsolated(node: GraphNode | undefined): boolean;
12
10
  export declare function isolatedSkillMessages(parentMessages: GraphMessage[], skillInput: unknown): GraphMessage[];
13
11
  /** 只回写最后一条带正文的 assistant;没有则空数组,避免把 tool 行丢给父图。 */
14
12
  export declare function isolatedSkillOutput(added: GraphMessage[]): GraphMessage[];
15
13
  export declare function isIsolatedSkillState(state: Record<string, unknown> | undefined): boolean;
16
- /** 隔离子图即将 HITL 时拒绝,避免写错父线程 Checkpoint。 */
17
- export declare function assertIsolatedSkillAllowsHitl(state: Record<string, unknown> | undefined): void;
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Skill 子图可选隔离(实验口)。仅节点 `isolateContext: true`。
3
3
  * 只把本轮输入给子图,回写最后一条带正文的 assistant;没有则不回写。
4
- * 隔离时不写父 session Checkpoint。
5
- * 子图内 HITL 未按隔离线程设计:skillIsolateContext 为 true 时禁止 interrupt。
4
+ * 子图 Checkpoint / HITL 走 `sessionId#父节点id`,不写父 latest。
6
5
  */
7
- import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
8
- export const SKILL_ISOLATE_HITL_MESSAGE = 'Skill isolateContext 不能与子图内人工确认一起使用。关掉 isolate,或不要在该 Skill 里使用 require_confirmation / beforeTool ask。';
9
6
  /** 只认节点配置。进程环境变量不会给所有 subgraph 开隔离。 */
10
7
  export function isSkillContextIsolated(node) {
11
8
  return node?.config?.['isolateContext'] === true;
@@ -40,11 +37,3 @@ export function isolatedSkillOutput(added) {
40
37
  export function isIsolatedSkillState(state) {
41
38
  return state?.['skillIsolateContext'] === true;
42
39
  }
43
- /** 隔离子图即将 HITL 时拒绝,避免写错父线程 Checkpoint。 */
44
- export function assertIsolatedSkillAllowsHitl(state) {
45
- if (!isIsolatedSkillState(state))
46
- return;
47
- throw new AppError(ErrorCodes.GRAPH_EXECUTION_ERROR, SKILL_ISOLATE_HITL_MESSAGE, {
48
- exposeMessage: true,
49
- });
50
- }