@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.
- package/README.md +1 -1
- package/dist/api/handlers/invoke.handler.js +6 -3
- package/dist/api/handlers/stream.handler.js +6 -2
- package/dist/checkpoint/file-checkpoint-saver.d.ts +4 -0
- package/dist/checkpoint/file-checkpoint-saver.js +51 -22
- package/dist/checkpoint/index.d.ts +1 -1
- package/dist/checkpoint/index.js +1 -1
- package/dist/graph/adapters/langgraph-checkpointer.d.ts +11 -0
- package/dist/graph/adapters/langgraph-checkpointer.js +48 -9
- package/dist/graph/adapters/langgraph.adapter.d.ts +8 -0
- package/dist/graph/adapters/langgraph.adapter.js +58 -16
- package/dist/graph/adapters/skill-isolate.d.ts +1 -5
- package/dist/graph/adapters/skill-isolate.js +1 -12
- package/dist/graph/adapters/subgraph-compiler.js +12 -15
- package/dist/graph/contracts/executable-graph.d.ts +6 -0
- package/dist/graph/hitl/hitl-handler.d.ts +13 -4
- package/dist/graph/hitl/hitl-handler.js +78 -7
- package/dist/graph/nodes/tool-node.js +1 -5
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/local-llm-provider.d.ts +6 -0
- package/dist/providers/local-llm-provider.js +29 -5
- package/dist/providers/routing-llm-provider.d.ts +2 -2
- package/dist/providers/routing-llm-provider.js +5 -15
- package/dist/registry/deployment-snapshot.js +18 -4
- package/dist/registry/graph-loader.d.ts +2 -0
- package/dist/registry/graph-loader.js +39 -2
- package/dist/registry/preload-flags.d.ts +1 -1
- package/dist/registry/preload-flags.js +1 -1
- package/dist/runtime/agent-runtime.js +27 -10
- package/dist/runtime/dev-runtime.d.ts +2 -1
- package/dist/runtime/dev-runtime.js +9 -3
- package/dist/session/apply-session-compression.d.ts +4 -0
- package/dist/session/apply-session-compression.js +22 -3
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/persist-turn-messages.d.ts +6 -1
- package/dist/session/persist-turn-messages.js +23 -5
- package/dist/token/record-task-token-usage.d.ts +23 -0
- package/dist/token/record-task-token-usage.js +20 -0
- package/dist/tool/tool-confirmation.js +8 -5
- 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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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';
|
package/dist/checkpoint/index.js
CHANGED
|
@@ -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
|
|
46
|
-
await this.saver.
|
|
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,
|
|
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
|
|
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 (
|
|
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)
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
}
|