@gt-fe/eap-runtime 0.0.17 → 0.0.19
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 +13 -11
- package/dist/checkpoint/checkpoint-ref-notify.js +5 -4
- package/dist/checkpoint/checkpoint-serializer.d.ts +8 -4
- package/dist/checkpoint/checkpoint-serializer.js +58 -19
- package/dist/checkpoint/file-checkpoint-saver.js +2 -1
- package/dist/checkpoint/index.d.ts +1 -1
- package/dist/checkpoint/index.js +1 -1
- package/dist/checkpoint/pg-checkpoint-saver.js +4 -3
- package/dist/checkpoint/redis-checkpoint-saver.js +2 -1
- package/dist/graph/adapters/langgraph.adapter.d.ts +4 -12
- package/dist/graph/adapters/langgraph.adapter.js +73 -63
- package/dist/graph/adapters/subgraph-compiler.js +9 -8
- package/dist/graph/contracts/executable-graph.d.ts +4 -1
- package/dist/graph/execution-trace-context.d.ts +38 -0
- package/dist/graph/execution-trace-context.js +62 -0
- package/dist/graph/hitl/hitl-handler.d.ts +2 -1
- package/dist/graph/hitl/hitl-handler.js +24 -6
- package/dist/graph/nodes/tool-node.js +20 -6
- package/dist/graph/trace-span.js +8 -7
- package/dist/runtime/types.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -277,26 +277,28 @@ EAP_TOOL_EXECUTION_MODE=mcp # 仅 createDevRuntime / eap chat
|
|
|
277
277
|
|
|
278
278
|
---
|
|
279
279
|
|
|
280
|
-
## 11.
|
|
280
|
+
## 11. 发布 npm 包
|
|
281
281
|
|
|
282
|
-
|
|
282
|
+
升版、CHANGELOG、`git commit` / `git push` 不在脚本里,先做完再跑脚本。脚本只做构建、发布、核对 registry 上的版本号。
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
| 脚本 | 包 | registry |
|
|
285
|
+
|------|----|----------|
|
|
286
|
+
| [`scripts/publish-intranet.sh`](./scripts/publish-intranet.sh) | `@eap/runtime` | Nexus `https://maven.gtcloud.cn/repository/npm-local/` |
|
|
287
|
+
| [`scripts/publish-public.sh`](./scripts/publish-public.sh) | `@gt-fe/eap-runtime` | npmjs。临时目录改名;dist 里 `@eap/contracts` 改为 `@gt-fe/eap-contracts`。不改源码 `package.json` |
|
|
285
288
|
|
|
286
289
|
```bash
|
|
290
|
+
# 登录(一次性)
|
|
287
291
|
cp .npmrc.example .npmrc
|
|
288
292
|
npm login --registry https://maven.gtcloud.cn/repository/npm-local/
|
|
293
|
+
npm login --registry https://registry.npmjs.org/
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
pnpm run build && npm publish --registry https://maven.gtcloud.cn/repository/npm-local/
|
|
293
|
-
|
|
294
|
-
# 2) runtime:package.json 中 @eap/contracts 改为版本号(勿再用 file:)
|
|
295
|
-
cd ../runtime && pnpm run build
|
|
296
|
-
npm publish --registry https://maven.gtcloud.cn/repository/npm-local/
|
|
295
|
+
./scripts/publish-intranet.sh
|
|
296
|
+
./scripts/publish-public.sh
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
不要在源码目录对 npmjs 执行 `npm publish`(`publishConfig` 指向 Nexus;公网 dist 必须改写 contracts)。含 `_authToken` 的 `.npmrc` 已 gitignore,勿提交凭证。`@eap:registry=...` 须写入 `.npmrc` 文件,不可在 shell 中当命令执行。
|
|
300
|
+
|
|
301
|
+
UAT Hosting 吃公网包:把 `@eap/runtime` 指到 `npm:@gt-fe/eap-runtime@<本版>` 后重建镜像。
|
|
300
302
|
|
|
301
303
|
---
|
|
302
304
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 无 traceId 或 spanId 则跳过;回调失败不抛。
|
|
4
4
|
* collector 不得写入持久化正文。
|
|
5
5
|
*/
|
|
6
|
+
import { getExecutionTrace } from '../graph/execution-trace-context.js';
|
|
6
7
|
function pickString(...values) {
|
|
7
8
|
for (const value of values) {
|
|
8
9
|
if (typeof value === 'string' && value.trim())
|
|
@@ -34,10 +35,10 @@ export async function notifyCheckpointRef(data, collectorOverride) {
|
|
|
34
35
|
? data.engineSnapshot.channel_values
|
|
35
36
|
: {};
|
|
36
37
|
const state = (data.state ?? {});
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const traceId = pickString(state['traceId'], channels['traceId']);
|
|
38
|
+
const store = getExecutionTrace();
|
|
39
|
+
// collector 是活对象,不从 Checkpoint 正文读(落盘已剥掉)。
|
|
40
|
+
const collector = (collectorOverride ?? store?.collector);
|
|
41
|
+
const traceId = pickString(store?.traceId, state['traceId'], channels['traceId']);
|
|
41
42
|
const spanId = pickString(state['lastSpanId'], channels['lastSpanId']);
|
|
42
43
|
if (!collector?.recordCheckpointRef || !traceId || !spanId)
|
|
43
44
|
return;
|
|
@@ -40,13 +40,17 @@ export interface CreateCheckpointParams {
|
|
|
40
40
|
* 确保所有必要字段的一致性。
|
|
41
41
|
*/
|
|
42
42
|
export declare function createCheckpointData(params: CreateCheckpointParams): CheckpointData;
|
|
43
|
+
/**
|
|
44
|
+
* 落盘用快照:可 JSON 往返的数据留下;定时器 / 函数 / 环丢掉。
|
|
45
|
+
* 环用栈判定,同一对象出现两次但不成环(菱形)仍保留。
|
|
46
|
+
*/
|
|
47
|
+
export declare function toJsonSafe(value: unknown, stack?: Set<object>): unknown;
|
|
48
|
+
/** 所有 Checkpoint Saver 落盘共用,禁止裸 JSON.stringify(state)。 */
|
|
49
|
+
export declare function stringifyJsonSafe(value: unknown): string;
|
|
43
50
|
/**
|
|
44
51
|
* 序列化 CheckpointData 为 JSON 字符串
|
|
45
52
|
*
|
|
46
|
-
*
|
|
47
|
-
* - Date 对象 → ISO 字符串
|
|
48
|
-
* - undefined 值自动剔除(JSON.stringify 默认行为)
|
|
49
|
-
* - 循环引用检测(抛出明确错误而非默认 TypeError)
|
|
53
|
+
* Date → ISO;定时器 / 循环引用省略,不抛 TypeError。
|
|
50
54
|
*/
|
|
51
55
|
export declare function serializeCheckpoint(data: CheckpointData): string;
|
|
52
56
|
/**
|
|
@@ -37,30 +37,69 @@ export function createCheckpointData(params) {
|
|
|
37
37
|
// ─────────────────────────────────────────────
|
|
38
38
|
// 序列化 / 反序列化
|
|
39
39
|
// ─────────────────────────────────────────────
|
|
40
|
+
function isTimerHandle(value) {
|
|
41
|
+
const name = value.constructor?.name;
|
|
42
|
+
if (name === 'Timeout' || name === 'TimersList' || name === 'Immediate')
|
|
43
|
+
return true;
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(value, '_idlePrev')
|
|
45
|
+
&& Object.prototype.hasOwnProperty.call(value, '_idleNext');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 落盘用快照:可 JSON 往返的数据留下;定时器 / 函数 / 环丢掉。
|
|
49
|
+
* 环用栈判定,同一对象出现两次但不成环(菱形)仍保留。
|
|
50
|
+
*/
|
|
51
|
+
export function toJsonSafe(value, stack = new Set()) {
|
|
52
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
53
|
+
return value;
|
|
54
|
+
if (typeof value === 'number')
|
|
55
|
+
return Number.isFinite(value) ? value : null;
|
|
56
|
+
if (typeof value === 'bigint')
|
|
57
|
+
return value.toString();
|
|
58
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (value instanceof Date)
|
|
62
|
+
return value.toISOString();
|
|
63
|
+
if (typeof value !== 'object')
|
|
64
|
+
return undefined;
|
|
65
|
+
if (isTimerHandle(value))
|
|
66
|
+
return undefined;
|
|
67
|
+
if (stack.has(value))
|
|
68
|
+
return undefined;
|
|
69
|
+
const maybeToJSON = value.toJSON;
|
|
70
|
+
if (typeof maybeToJSON === 'function') {
|
|
71
|
+
return toJsonSafe(maybeToJSON.call(value), stack);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
stack.add(value);
|
|
75
|
+
const out = value.map((item) => {
|
|
76
|
+
const next = toJsonSafe(item, stack);
|
|
77
|
+
return next === undefined ? null : next;
|
|
78
|
+
});
|
|
79
|
+
stack.delete(value);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
stack.add(value);
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const [key, child] of Object.entries(value)) {
|
|
85
|
+
const next = toJsonSafe(child, stack);
|
|
86
|
+
if (next !== undefined)
|
|
87
|
+
out[key] = next;
|
|
88
|
+
}
|
|
89
|
+
stack.delete(value);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** 所有 Checkpoint Saver 落盘共用,禁止裸 JSON.stringify(state)。 */
|
|
93
|
+
export function stringifyJsonSafe(value) {
|
|
94
|
+
return JSON.stringify(toJsonSafe(value));
|
|
95
|
+
}
|
|
40
96
|
/**
|
|
41
97
|
* 序列化 CheckpointData 为 JSON 字符串
|
|
42
98
|
*
|
|
43
|
-
*
|
|
44
|
-
* - Date 对象 → ISO 字符串
|
|
45
|
-
* - undefined 值自动剔除(JSON.stringify 默认行为)
|
|
46
|
-
* - 循环引用检测(抛出明确错误而非默认 TypeError)
|
|
99
|
+
* Date → ISO;定时器 / 循环引用省略,不抛 TypeError。
|
|
47
100
|
*/
|
|
48
101
|
export function serializeCheckpoint(data) {
|
|
49
|
-
|
|
50
|
-
return JSON.stringify(data, (_key, value) => {
|
|
51
|
-
// Date → ISO 字符串
|
|
52
|
-
if (value instanceof Date) {
|
|
53
|
-
return value.toISOString();
|
|
54
|
-
}
|
|
55
|
-
// 循环引用检测
|
|
56
|
-
if (typeof value === 'object' && value !== null) {
|
|
57
|
-
if (seen.has(value)) {
|
|
58
|
-
throw new Error(`[CheckpointSerializer] Circular reference detected during serialization`);
|
|
59
|
-
}
|
|
60
|
-
seen.add(value);
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
63
|
-
});
|
|
102
|
+
return stringifyJsonSafe(data);
|
|
64
103
|
}
|
|
65
104
|
/**
|
|
66
105
|
* 反序列化 JSON 字符串为 CheckpointData
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
+
import { stringifyJsonSafe } from './checkpoint-serializer.js';
|
|
7
8
|
export class FileCheckpointSaver {
|
|
8
9
|
baseDir;
|
|
9
10
|
constructor(baseDir) {
|
|
@@ -12,7 +13,7 @@ export class FileCheckpointSaver {
|
|
|
12
13
|
async save(threadId, data) {
|
|
13
14
|
const dir = join(this.baseDir, threadId);
|
|
14
15
|
await mkdir(dir, { recursive: true });
|
|
15
|
-
await writeFile(join(dir, `${data.checkpointId}.json`),
|
|
16
|
+
await writeFile(join(dir, `${data.checkpointId}.json`), stringifyJsonSafe(data));
|
|
16
17
|
// 更新 latest 指针
|
|
17
18
|
await writeFile(join(dir, 'latest.json'), JSON.stringify({ checkpointId: data.checkpointId }));
|
|
18
19
|
}
|
|
@@ -4,7 +4,7 @@ export type { RedisCheckpointSaverOptions } from './redis-checkpoint-saver.js';
|
|
|
4
4
|
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
|
-
export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, } from './checkpoint-serializer.js';
|
|
7
|
+
export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, stringifyJsonSafe, toJsonSafe, } from './checkpoint-serializer.js';
|
|
8
8
|
export type { CreateCheckpointParams } from './checkpoint-serializer.js';
|
|
9
9
|
export { FileCheckpointSaver } from './file-checkpoint-saver.js';
|
|
10
10
|
export { InMemoryCheckpointSaver } from './in-memory-checkpoint-saver.js';
|
package/dist/checkpoint/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { RedisCheckpointSaver } from './redis-checkpoint-saver.js';
|
|
|
4
4
|
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
|
-
export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, } from './checkpoint-serializer.js';
|
|
7
|
+
export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, stringifyJsonSafe, toJsonSafe, } from './checkpoint-serializer.js';
|
|
8
8
|
export { FileCheckpointSaver } from './file-checkpoint-saver.js';
|
|
9
9
|
export { InMemoryCheckpointSaver } from './in-memory-checkpoint-saver.js';
|
|
10
10
|
export { MongoCheckpointSaver } from './mongo-checkpoint-saver.js';
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @see design.md §1.3 checkpoints 数据模型
|
|
8
8
|
* @see checkpoint-saver.contract.ts ICheckpointSaver 接口定义
|
|
9
9
|
*/
|
|
10
|
+
import { stringifyJsonSafe } from './checkpoint-serializer.js';
|
|
10
11
|
// ─────────────────────────────────────────────
|
|
11
12
|
// PostgresCheckpointSaver
|
|
12
13
|
// ─────────────────────────────────────────────
|
|
@@ -38,9 +39,9 @@ export class PostgresCheckpointSaver {
|
|
|
38
39
|
threadId,
|
|
39
40
|
data.checkpointId,
|
|
40
41
|
data.currentNodeId,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
data.engineSnapshot == null ? null :
|
|
42
|
+
stringifyJsonSafe(data.historyPath),
|
|
43
|
+
stringifyJsonSafe(data.state),
|
|
44
|
+
data.engineSnapshot == null ? null : stringifyJsonSafe(data.engineSnapshot),
|
|
44
45
|
data.createdBy || 'system',
|
|
45
46
|
data.createdAt,
|
|
46
47
|
data.updatedBy || 'system',
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @see design.md §1.3 checkpoints 数据模型
|
|
11
11
|
*/
|
|
12
|
+
import { stringifyJsonSafe } from './checkpoint-serializer.js';
|
|
12
13
|
/**
|
|
13
14
|
* RedisCheckpointSaver — ICheckpointSaver 的 Redis 实现
|
|
14
15
|
*
|
|
@@ -52,7 +53,7 @@ export class RedisCheckpointSaver {
|
|
|
52
53
|
const idxKey = this.threadIndexKey(threadId);
|
|
53
54
|
const ltKey = this.latestKey(threadId);
|
|
54
55
|
const revKey = this.reverseIndexKey(data.checkpointId);
|
|
55
|
-
const json =
|
|
56
|
+
const json = stringifyJsonSafe(data);
|
|
56
57
|
const score = new Date(data.createdAt).getTime();
|
|
57
58
|
const pipeline = this.redis.pipeline();
|
|
58
59
|
// 存储 Checkpoint JSON
|
|
@@ -35,13 +35,6 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
|
|
|
35
35
|
private readonly conditionEvaluator;
|
|
36
36
|
private readonly systemPromptBuilder;
|
|
37
37
|
private readonly skillAuditRecorder;
|
|
38
|
-
/** 流式旁路:LLM token / Tool 执行事件在节点执行中实时入队 */
|
|
39
|
-
private streamSink;
|
|
40
|
-
private streamEventContext;
|
|
41
|
-
private streamChunkIndex;
|
|
42
|
-
private streamIteration;
|
|
43
|
-
private reasoningStartedThisTurn;
|
|
44
|
-
private streamedTextThisTurn;
|
|
45
38
|
/**
|
|
46
39
|
* @param checkpointSaver - 可选的 ICheckpointSaver
|
|
47
40
|
* @param llmProvider - LLM 调用提供者,默认 EchoLLMProvider
|
|
@@ -56,7 +49,7 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
|
|
|
56
49
|
registerConditionEvaluator(name: string, fn: ConditionEvaluatorFn): void;
|
|
57
50
|
/**
|
|
58
51
|
* 节点旁路推送 RuntimeEvent(子图 Skill 生命周期、Tool 执行)。
|
|
59
|
-
*
|
|
52
|
+
* 无本次执行流式袋时为空操作(纯 invoke)。
|
|
60
53
|
*/
|
|
61
54
|
emitStreamEvent(event: RuntimeEvent): void;
|
|
62
55
|
/**
|
|
@@ -72,9 +65,8 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
|
|
|
72
65
|
/**
|
|
73
66
|
* 解析 GraphDSL 并构建可执行图
|
|
74
67
|
*
|
|
75
|
-
* **Request Isolation**: 每次调用都创建全新的 StateGraph
|
|
76
|
-
*
|
|
77
|
-
* 不缓存任何编译产物或运行时状态。
|
|
68
|
+
* **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译。
|
|
69
|
+
* 一次执行的 collector / 流式袋跟这次调用走,不挂 Adapter 字段。
|
|
78
70
|
*
|
|
79
71
|
* 流程:
|
|
80
72
|
* 1. 创建 StateGraph 实例(配置 channels)
|
|
@@ -89,7 +81,7 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
|
|
|
89
81
|
* 流式驱动图执行,逐步 yield RuntimeEvent
|
|
90
82
|
*
|
|
91
83
|
* - LangGraph `streamMode: "updates"`:节点完成后产出 response.chunk / stats 等
|
|
92
|
-
* - LLM token / Tool
|
|
84
|
+
* - LLM token / Tool 执行事件:经本次执行流式袋在节点执行中实时旁路入队
|
|
93
85
|
* (当前 @langchain/langgraph@0.2 无 messages mode,故用 LLMNode.onChunk 模拟)
|
|
94
86
|
*/
|
|
95
87
|
stream(graph: ExecutableGraph, state: GraphState): AsyncIterableIterator<RuntimeEvent>;
|
|
@@ -31,6 +31,7 @@ import { pickContextFields } from '../../token/context-window.js';
|
|
|
31
31
|
import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
|
|
32
32
|
import { classifySkillRead } from './skill-read.js';
|
|
33
33
|
import { withGraphSpan } from '../trace-span.js';
|
|
34
|
+
import { executionTraceFromState, getExecutionStream, runWithExecutionTrace, } from '../execution-trace-context.js';
|
|
34
35
|
/** 图未写 `config.maxIterations` 时的 recursionLimit。`agent ⇄ tools` 一圈约 2,50 ≈ 24 次工具往返。 */
|
|
35
36
|
export const DEFAULT_GRAPH_RECURSION_LIMIT = 50;
|
|
36
37
|
/**
|
|
@@ -68,13 +69,6 @@ export class LangGraphAdapter {
|
|
|
68
69
|
conditionEvaluator;
|
|
69
70
|
systemPromptBuilder;
|
|
70
71
|
skillAuditRecorder;
|
|
71
|
-
/** 流式旁路:LLM token / Tool 执行事件在节点执行中实时入队 */
|
|
72
|
-
streamSink = null;
|
|
73
|
-
streamEventContext = {};
|
|
74
|
-
streamChunkIndex = 0;
|
|
75
|
-
streamIteration = 0;
|
|
76
|
-
reasoningStartedThisTurn = false;
|
|
77
|
-
streamedTextThisTurn = false;
|
|
78
72
|
/**
|
|
79
73
|
* @param checkpointSaver - 可选的 ICheckpointSaver
|
|
80
74
|
* @param llmProvider - LLM 调用提供者,默认 EchoLLMProvider
|
|
@@ -140,31 +134,37 @@ export class LangGraphAdapter {
|
|
|
140
134
|
}
|
|
141
135
|
/**
|
|
142
136
|
* 节点旁路推送 RuntimeEvent(子图 Skill 生命周期、Tool 执行)。
|
|
143
|
-
*
|
|
137
|
+
* 无本次执行流式袋时为空操作(纯 invoke)。
|
|
144
138
|
*/
|
|
145
139
|
emitStreamEvent(event) {
|
|
146
|
-
|
|
140
|
+
const stream = getExecutionStream();
|
|
141
|
+
if (!stream)
|
|
147
142
|
return;
|
|
148
|
-
|
|
143
|
+
stream.sink({
|
|
149
144
|
...event,
|
|
150
|
-
taskId: event.taskId ??
|
|
151
|
-
correlationId: event.correlationId ??
|
|
145
|
+
taskId: event.taskId ?? stream.eventContext.taskId,
|
|
146
|
+
correlationId: event.correlationId ?? stream.eventContext.correlationId,
|
|
152
147
|
});
|
|
153
148
|
}
|
|
154
149
|
/**
|
|
155
150
|
* 子图 LLM 可清零流式标志;结束后保留「已经 stream 过」以免父图 convertUpdates 再发一遍正文。
|
|
156
151
|
*/
|
|
157
152
|
async runIsolatedLlmStreamFlags(work) {
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
const stream = getExecutionStream();
|
|
154
|
+
const savedStreamed = stream?.streamedTextThisTurn ?? false;
|
|
155
|
+
const savedReasoning = stream?.reasoningStartedThisTurn ?? false;
|
|
156
|
+
if (stream) {
|
|
157
|
+
stream.reasoningStartedThisTurn = false;
|
|
158
|
+
stream.streamedTextThisTurn = false;
|
|
159
|
+
}
|
|
162
160
|
try {
|
|
163
161
|
return await work();
|
|
164
162
|
}
|
|
165
163
|
finally {
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
if (stream) {
|
|
165
|
+
stream.streamedTextThisTurn = savedStreamed || stream.streamedTextThisTurn;
|
|
166
|
+
stream.reasoningStartedThisTurn = savedReasoning || stream.reasoningStartedThisTurn;
|
|
167
|
+
}
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
@@ -173,10 +173,11 @@ export class LangGraphAdapter {
|
|
|
173
173
|
* 读 SKILL.md / 技能包内文件:成功后再发 skill.loaded / skill.resource_read,同时保留 tool.*。
|
|
174
174
|
*/
|
|
175
175
|
emitToolExecutionEvents(progress) {
|
|
176
|
-
|
|
176
|
+
const stream = getExecutionStream();
|
|
177
|
+
if (!stream)
|
|
177
178
|
return;
|
|
178
179
|
const timestamp = new Date().toISOString();
|
|
179
|
-
const { taskId, correlationId } =
|
|
180
|
+
const { taskId, correlationId } = stream.eventContext;
|
|
180
181
|
const loc = {
|
|
181
182
|
...(progress.node ? { node: progress.node } : {}),
|
|
182
183
|
...(progress.skillId ? { skillId: progress.skillId } : {}),
|
|
@@ -184,7 +185,7 @@ export class LangGraphAdapter {
|
|
|
184
185
|
const skillRead = classifySkillRead(progress.toolName, progress.arguments, progress.skillDirs);
|
|
185
186
|
if (skillRead && progress.status === 'completed') {
|
|
186
187
|
if (skillRead.kind === 'loaded') {
|
|
187
|
-
|
|
188
|
+
stream.sink({
|
|
188
189
|
type: 'skill.loaded',
|
|
189
190
|
taskId,
|
|
190
191
|
correlationId,
|
|
@@ -196,7 +197,7 @@ export class LangGraphAdapter {
|
|
|
196
197
|
});
|
|
197
198
|
}
|
|
198
199
|
else {
|
|
199
|
-
|
|
200
|
+
stream.sink({
|
|
200
201
|
type: 'skill.resource_read',
|
|
201
202
|
taskId,
|
|
202
203
|
correlationId,
|
|
@@ -206,7 +207,7 @@ export class LangGraphAdapter {
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
if (progress.status === 'started') {
|
|
209
|
-
|
|
210
|
+
stream.sink({
|
|
210
211
|
type: 'tool.called',
|
|
211
212
|
taskId,
|
|
212
213
|
correlationId,
|
|
@@ -220,7 +221,7 @@ export class LangGraphAdapter {
|
|
|
220
221
|
},
|
|
221
222
|
});
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
+
stream.sink({
|
|
224
225
|
type: 'tool.progress',
|
|
225
226
|
taskId,
|
|
226
227
|
correlationId,
|
|
@@ -228,7 +229,7 @@ export class LangGraphAdapter {
|
|
|
228
229
|
payload: { ...progress },
|
|
229
230
|
});
|
|
230
231
|
if (progress.status === 'completed') {
|
|
231
|
-
|
|
232
|
+
stream.sink({
|
|
232
233
|
type: 'tool.result',
|
|
233
234
|
taskId,
|
|
234
235
|
correlationId,
|
|
@@ -244,7 +245,7 @@ export class LangGraphAdapter {
|
|
|
244
245
|
});
|
|
245
246
|
}
|
|
246
247
|
else if (progress.status === 'error') {
|
|
247
|
-
|
|
248
|
+
stream.sink({
|
|
248
249
|
type: 'tool.error',
|
|
249
250
|
taskId,
|
|
250
251
|
correlationId,
|
|
@@ -263,9 +264,8 @@ export class LangGraphAdapter {
|
|
|
263
264
|
/**
|
|
264
265
|
* 解析 GraphDSL 并构建可执行图
|
|
265
266
|
*
|
|
266
|
-
* **Request Isolation**: 每次调用都创建全新的 StateGraph
|
|
267
|
-
*
|
|
268
|
-
* 不缓存任何编译产物或运行时状态。
|
|
267
|
+
* **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译。
|
|
268
|
+
* 一次执行的 collector / 流式袋跟这次调用走,不挂 Adapter 字段。
|
|
269
269
|
*
|
|
270
270
|
* 流程:
|
|
271
271
|
* 1. 创建 StateGraph 实例(配置 channels)
|
|
@@ -410,7 +410,7 @@ export class LangGraphAdapter {
|
|
|
410
410
|
* 流式驱动图执行,逐步 yield RuntimeEvent
|
|
411
411
|
*
|
|
412
412
|
* - LangGraph `streamMode: "updates"`:节点完成后产出 response.chunk / stats 等
|
|
413
|
-
* - LLM token / Tool
|
|
413
|
+
* - LLM token / Tool 执行事件:经本次执行流式袋在节点执行中实时旁路入队
|
|
414
414
|
* (当前 @langchain/langgraph@0.2 无 messages mode,故用 LLMNode.onChunk 模拟)
|
|
415
415
|
*/
|
|
416
416
|
async *stream(graph, state) {
|
|
@@ -434,13 +434,17 @@ export class LangGraphAdapter {
|
|
|
434
434
|
});
|
|
435
435
|
signal();
|
|
436
436
|
};
|
|
437
|
-
this
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
437
|
+
// 流式袋跟这次调用走,不挂 this:并行 stream 各写各的 queue。
|
|
438
|
+
const streamBag = {
|
|
439
|
+
sink: enqueue,
|
|
440
|
+
eventContext: { taskId: state.taskId, correlationId: state.correlationId },
|
|
441
|
+
chunkIndex: 0,
|
|
442
|
+
iteration: 0,
|
|
443
|
+
reasoningStartedThisTurn: false,
|
|
444
|
+
streamedTextThisTurn: false,
|
|
445
|
+
};
|
|
446
|
+
const traceCtx = executionTraceFromState(state);
|
|
447
|
+
const producer = runWithExecutionTrace({ ...traceCtx, stream: streamBag }, () => (async () => {
|
|
444
448
|
try {
|
|
445
449
|
const streamIterator = await ref.compiled.stream(input, langGraphRunOptions(ref.dsl, {
|
|
446
450
|
configurable: {
|
|
@@ -453,7 +457,7 @@ export class LangGraphAdapter {
|
|
|
453
457
|
streamMode: 'updates',
|
|
454
458
|
}));
|
|
455
459
|
for await (const update of streamIterator) {
|
|
456
|
-
for (const event of this.convertUpdatesToEvents(update, graph.id, () =>
|
|
460
|
+
for (const event of this.convertUpdatesToEvents(update, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
|
|
457
461
|
enqueue(event);
|
|
458
462
|
}
|
|
459
463
|
}
|
|
@@ -463,11 +467,9 @@ export class LangGraphAdapter {
|
|
|
463
467
|
}
|
|
464
468
|
finally {
|
|
465
469
|
finished = true;
|
|
466
|
-
this.streamSink = null;
|
|
467
|
-
this.streamEventContext = {};
|
|
468
470
|
signal();
|
|
469
471
|
}
|
|
470
|
-
})();
|
|
472
|
+
})());
|
|
471
473
|
while (!finished || queue.length > 0) {
|
|
472
474
|
if (queue.length === 0) {
|
|
473
475
|
await new Promise((resolve) => {
|
|
@@ -488,7 +490,9 @@ export class LangGraphAdapter {
|
|
|
488
490
|
const ref = this.getCompiledRef(graph);
|
|
489
491
|
const input = this.graphStateToLangGraphInput(state);
|
|
490
492
|
const startTime = Date.now();
|
|
491
|
-
const
|
|
493
|
+
const traceCtx = executionTraceFromState(state);
|
|
494
|
+
// 只补 collector/traceId。子图套娃时外层 stream 袋经 ?? 保留。
|
|
495
|
+
const result = await runWithExecutionTrace(traceCtx, () => ref.compiled.invoke(input, langGraphRunOptions(ref.dsl, {
|
|
492
496
|
configurable: {
|
|
493
497
|
thread_id: resolveCheckpointThreadId({
|
|
494
498
|
sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
|
|
@@ -496,7 +500,7 @@ export class LangGraphAdapter {
|
|
|
496
500
|
graphId: graph.id,
|
|
497
501
|
}),
|
|
498
502
|
},
|
|
499
|
-
}));
|
|
503
|
+
})));
|
|
500
504
|
const duration = Date.now() - startTime;
|
|
501
505
|
const messages = Array.isArray(result['messages']) ? result['messages'] : [];
|
|
502
506
|
const lastMessage = messages[messages.length - 1];
|
|
@@ -661,15 +665,16 @@ export class LangGraphAdapter {
|
|
|
661
665
|
},
|
|
662
666
|
});
|
|
663
667
|
}
|
|
664
|
-
|
|
668
|
+
const stream = getExecutionStream();
|
|
669
|
+
if (patch.skillRoute === 'clarify' && stream) {
|
|
665
670
|
const question = patch.messages.at(-1)?.content;
|
|
666
671
|
if (typeof question === 'string' && question.trim()) {
|
|
667
|
-
|
|
672
|
+
stream.sink({
|
|
668
673
|
type: 'response.chunk',
|
|
669
674
|
taskId: undefined,
|
|
670
675
|
correlationId: undefined,
|
|
671
676
|
timestamp: new Date().toISOString(),
|
|
672
|
-
payload: { content: question, index:
|
|
677
|
+
payload: { content: question, index: stream.chunkIndex++, node: node.id },
|
|
673
678
|
});
|
|
674
679
|
}
|
|
675
680
|
}
|
|
@@ -892,38 +897,42 @@ export class LangGraphAdapter {
|
|
|
892
897
|
tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
|
|
893
898
|
toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
|
|
894
899
|
};
|
|
895
|
-
//
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
900
|
+
// 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
|
|
901
|
+
const stream = getExecutionStream();
|
|
902
|
+
const enableStream = Boolean(stream) && !silent;
|
|
903
|
+
if (stream) {
|
|
904
|
+
stream.reasoningStartedThisTurn = false;
|
|
905
|
+
stream.streamedTextThisTurn = false;
|
|
906
|
+
}
|
|
899
907
|
const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
|
|
900
908
|
? (text) => {
|
|
901
|
-
|
|
909
|
+
const live = getExecutionStream();
|
|
910
|
+
if (!live || !text)
|
|
902
911
|
return;
|
|
903
912
|
const timestamp = new Date().toISOString();
|
|
904
|
-
if (!
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
913
|
+
if (!live.reasoningStartedThisTurn) {
|
|
914
|
+
live.reasoningStartedThisTurn = true;
|
|
915
|
+
live.iteration += 1;
|
|
916
|
+
live.sink({
|
|
908
917
|
type: 'reasoning.started',
|
|
909
918
|
taskId: undefined,
|
|
910
919
|
correlationId: undefined,
|
|
911
920
|
timestamp,
|
|
912
921
|
payload: {
|
|
913
|
-
iterationCount:
|
|
922
|
+
iterationCount: live.iteration,
|
|
914
923
|
model: finalConfig.model,
|
|
915
924
|
},
|
|
916
925
|
});
|
|
917
926
|
}
|
|
918
|
-
|
|
919
|
-
|
|
927
|
+
live.streamedTextThisTurn = true;
|
|
928
|
+
live.sink({
|
|
920
929
|
type: 'response.chunk',
|
|
921
930
|
taskId: undefined,
|
|
922
931
|
correlationId: undefined,
|
|
923
932
|
timestamp,
|
|
924
933
|
payload: {
|
|
925
934
|
content: text,
|
|
926
|
-
index:
|
|
935
|
+
index: live.chunkIndex++,
|
|
927
936
|
node: node.id,
|
|
928
937
|
},
|
|
929
938
|
});
|
|
@@ -1229,6 +1238,7 @@ export class LangGraphAdapter {
|
|
|
1229
1238
|
*/
|
|
1230
1239
|
*convertUpdatesToEvents(update, graphId, nextChunkIndex, silentNodeIds = new Set(), subgraphNodeIds = new Set()) {
|
|
1231
1240
|
const timestamp = new Date().toISOString();
|
|
1241
|
+
const streamedTextThisTurn = Boolean(getExecutionStream()?.streamedTextThisTurn);
|
|
1232
1242
|
const entries = this.normalizeStreamUpdates(update);
|
|
1233
1243
|
for (const { nodeId, data } of entries) {
|
|
1234
1244
|
// LLM 节点 token 明细(写入 __llmTokenUsage)→ stats.node_token_usage
|
|
@@ -1266,8 +1276,8 @@ export class LangGraphAdapter {
|
|
|
1266
1276
|
}
|
|
1267
1277
|
const hasText = typeof msg.content === 'string' && msg.content.trim().length > 0;
|
|
1268
1278
|
if ((hasText || msg.reasoningContent) &&
|
|
1269
|
-
(!
|
|
1270
|
-
if (
|
|
1279
|
+
(!streamedTextThisTurn || msg.reasoningContent)) {
|
|
1280
|
+
if (streamedTextThisTurn && !msg.reasoningContent)
|
|
1271
1281
|
continue;
|
|
1272
1282
|
yield {
|
|
1273
1283
|
type: 'response.chunk',
|
|
@@ -1275,7 +1285,7 @@ export class LangGraphAdapter {
|
|
|
1275
1285
|
correlationId: undefined,
|
|
1276
1286
|
timestamp,
|
|
1277
1287
|
payload: {
|
|
1278
|
-
content:
|
|
1288
|
+
content: streamedTextThisTurn ? '' : (msg.content ?? ''),
|
|
1279
1289
|
index: nextChunkIndex(),
|
|
1280
1290
|
node: nodeId,
|
|
1281
1291
|
graphId,
|
|
@@ -1291,7 +1301,7 @@ export class LangGraphAdapter {
|
|
|
1291
1301
|
if (silentNodeIds.has(nodeId) || subgraphNodeIds.has(nodeId))
|
|
1292
1302
|
continue;
|
|
1293
1303
|
// 已通过 onChunk 推送过 token,避免整段重复
|
|
1294
|
-
if (
|
|
1304
|
+
if (streamedTextThisTurn) {
|
|
1295
1305
|
// token 已流过:仍补发一条带 reasoning 的标记 chunk(空 content),供落库回传
|
|
1296
1306
|
if (msg.reasoningContent) {
|
|
1297
1307
|
yield {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { assertSkillPermission } from '../../governance/permission-guard.js';
|
|
11
11
|
import { LangGraphAdapter } from './langgraph.adapter.js';
|
|
12
|
-
import {
|
|
12
|
+
import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
|
|
13
13
|
import { filterLoadedToolsForSkill, } from '../../tool/system-prompt-builder.js';
|
|
14
14
|
import { unwrapHitlPaused } from '../hitl/hitl-handler.js';
|
|
15
15
|
import { pickContextFields } from '../../token/context-window.js';
|
|
@@ -60,8 +60,8 @@ export class SubGraphCompiler {
|
|
|
60
60
|
taskId: state['taskId'],
|
|
61
61
|
correlationId: state['correlationId'],
|
|
62
62
|
});
|
|
63
|
-
|
|
64
|
-
const traceId =
|
|
63
|
+
// 用父图这次调用上的 collector;内层 invoke 只补 collector/traceId,沿用外层流式袋。
|
|
64
|
+
const { collector: traceCollector, traceId } = resolveExecutionTrace(state);
|
|
65
65
|
const skillName = dsl.name || resolvedSkillId;
|
|
66
66
|
if (!resuming) {
|
|
67
67
|
adapter.emitStreamEvent({
|
|
@@ -86,7 +86,8 @@ export class SubGraphCompiler {
|
|
|
86
86
|
metadata: { skillId: resolvedSkillId, taskId: state['taskId'], correlationId: state['correlationId'] },
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
catch {
|
|
89
|
+
catch (err) {
|
|
90
|
+
warnTraceSpanFailure('createNode', { type: 'skill', name: resolvedSkillId || dsl.name || 'skill', traceId }, err);
|
|
90
91
|
skillSpanId = null;
|
|
91
92
|
}
|
|
92
93
|
}
|
|
@@ -160,8 +161,8 @@ export class SubGraphCompiler {
|
|
|
160
161
|
totalTokens: result.tokenUsage?.total,
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
164
|
+
catch (err) {
|
|
165
|
+
warnTraceSpanFailure('completeNode', { type: 'skill', name: skillName, traceId, spanId: skillSpanId }, err);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
// 4. 只回写子图本步新增消息(含内部 tool 回合),禁止把入口历史再 append
|
|
@@ -224,8 +225,8 @@ export class SubGraphCompiler {
|
|
|
224
225
|
error: err instanceof Error ? err.message : String(err),
|
|
225
226
|
});
|
|
226
227
|
}
|
|
227
|
-
catch {
|
|
228
|
-
|
|
228
|
+
catch (completeErr) {
|
|
229
|
+
warnTraceSpanFailure('completeNode', { type: 'skill', name: skillName, traceId, spanId: skillSpanId }, completeErr);
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
adapter.emitStreamEvent({
|
|
@@ -207,7 +207,10 @@ export interface GraphState {
|
|
|
207
207
|
skillInput?: Record<string, unknown>;
|
|
208
208
|
/** Hosting 已建 Trace id(与 Foundation traces.id 同值) */
|
|
209
209
|
traceId?: string;
|
|
210
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* 入口把 collector 绑到本次执行上下文;节点不从此字段读取。
|
|
212
|
+
* 不入 Checkpoint。
|
|
213
|
+
*/
|
|
211
214
|
traceCollector?: import('../../contracts/trace-collector.js').ITraceCollector;
|
|
212
215
|
/** 当前 Skill 子图 createNode 返回的 span id;子图内 Tool span 的 parentNodeId */
|
|
213
216
|
skillSpanId?: string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ITraceCollector } from '../contracts/trace-collector.js';
|
|
2
|
+
import type { GraphState, RuntimeEvent } from './contracts/executable-graph.js';
|
|
3
|
+
/** 同一次 stream 共用;套娃 invoke 共享引用,并行任务各一份。 */
|
|
4
|
+
export interface ExecutionStreamBag {
|
|
5
|
+
sink: (event: RuntimeEvent) => void;
|
|
6
|
+
eventContext: {
|
|
7
|
+
taskId?: string;
|
|
8
|
+
correlationId?: string;
|
|
9
|
+
};
|
|
10
|
+
chunkIndex: number;
|
|
11
|
+
iteration: number;
|
|
12
|
+
reasoningStartedThisTurn: boolean;
|
|
13
|
+
streamedTextThisTurn: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ExecutionRunContext {
|
|
16
|
+
collector?: ITraceCollector;
|
|
17
|
+
traceId?: string;
|
|
18
|
+
stream?: ExecutionStreamBag;
|
|
19
|
+
}
|
|
20
|
+
/** 入口从本次入参取出可绑定的 collector+traceId;缺一则不算。 */
|
|
21
|
+
export declare function executionTraceFromState(state: GraphState | Record<string, unknown> | undefined): {
|
|
22
|
+
collector: ITraceCollector;
|
|
23
|
+
traceId: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* 绑到这次异步调用链。入参缺省时不新开一层。
|
|
27
|
+
* 套娃:只补 collector/traceId,stream 袋沿用外层引用。
|
|
28
|
+
* 并行:各 run 各一份 store。
|
|
29
|
+
*/
|
|
30
|
+
export declare function runWithExecutionTrace<T>(patch: ExecutionRunContext | undefined, fn: () => T): T;
|
|
31
|
+
export declare function getExecutionTrace(): ExecutionRunContext | undefined;
|
|
32
|
+
export declare function getExecutionStream(): ExecutionStreamBag | undefined;
|
|
33
|
+
/** 节点只认这次调用链上的 collector。traceId 可回退 state(字符串,可落盘)。 */
|
|
34
|
+
export declare function resolveExecutionTrace(state?: Record<string, unknown>): {
|
|
35
|
+
collector?: ITraceCollector;
|
|
36
|
+
traceId?: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function warnTraceSpanFailure(op: 'createNode' | 'completeNode', extra: Record<string, unknown>, err: unknown): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图内 span 与流式旁路:绑在这次调用链上,不挂 Adapter 实例、不写库。
|
|
3
|
+
*
|
|
4
|
+
* 约束:套娃 invoke 必须共用外层流式袋(引用),不能 finally 清空;
|
|
5
|
+
* 并行 invoke/stream 必须各有一份 store,否则 span 与 tool.called 会串台。
|
|
6
|
+
* collector 不入 Checkpoint;节点不从 Graph state 读 collector。
|
|
7
|
+
*/
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
9
|
+
import { emitRuntimeLog } from '../common/logger.js';
|
|
10
|
+
import { resolveTraceId } from '../common/trace-id.js';
|
|
11
|
+
const storage = new AsyncLocalStorage();
|
|
12
|
+
/** 入口从本次入参取出可绑定的 collector+traceId;缺一则不算。 */
|
|
13
|
+
export function executionTraceFromState(state) {
|
|
14
|
+
if (!state)
|
|
15
|
+
return undefined;
|
|
16
|
+
const collector = state.traceCollector;
|
|
17
|
+
const traceId = resolveTraceId(state);
|
|
18
|
+
if (!collector || !traceId)
|
|
19
|
+
return undefined;
|
|
20
|
+
return { collector, traceId };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 绑到这次异步调用链。入参缺省时不新开一层。
|
|
24
|
+
* 套娃:只补 collector/traceId,stream 袋沿用外层引用。
|
|
25
|
+
* 并行:各 run 各一份 store。
|
|
26
|
+
*/
|
|
27
|
+
export function runWithExecutionTrace(patch, fn) {
|
|
28
|
+
if (!patch)
|
|
29
|
+
return fn();
|
|
30
|
+
const outer = storage.getStore();
|
|
31
|
+
// stream 用 ?? 保留外层引用:子图 adapter.invoke 不得换袋、不得清空父 stream。
|
|
32
|
+
const next = {
|
|
33
|
+
collector: patch.collector ?? outer?.collector,
|
|
34
|
+
traceId: patch.traceId ?? outer?.traceId,
|
|
35
|
+
stream: patch.stream ?? outer?.stream,
|
|
36
|
+
};
|
|
37
|
+
if (!next.collector && !next.traceId && !next.stream)
|
|
38
|
+
return fn();
|
|
39
|
+
return storage.run(next, fn);
|
|
40
|
+
}
|
|
41
|
+
export function getExecutionTrace() {
|
|
42
|
+
return storage.getStore();
|
|
43
|
+
}
|
|
44
|
+
export function getExecutionStream() {
|
|
45
|
+
return storage.getStore()?.stream;
|
|
46
|
+
}
|
|
47
|
+
/** 节点只认这次调用链上的 collector。traceId 可回退 state(字符串,可落盘)。 */
|
|
48
|
+
export function resolveExecutionTrace(state) {
|
|
49
|
+
const store = storage.getStore();
|
|
50
|
+
return {
|
|
51
|
+
collector: store?.collector,
|
|
52
|
+
traceId: store?.traceId ?? (state ? resolveTraceId(state) : undefined),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function warnTraceSpanFailure(op, extra, err) {
|
|
56
|
+
emitRuntimeLog('warn', 'runtime.trace', `trace ${op} failed`, {
|
|
57
|
+
event: 'runtime.trace.span_failed',
|
|
58
|
+
op,
|
|
59
|
+
error: err instanceof Error ? err.message : String(err),
|
|
60
|
+
...extra,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -169,7 +169,7 @@ export declare class HitlHandler {
|
|
|
169
169
|
/** 同一 taskId 上串行化 resume 与 timeout,避免双边占用 */
|
|
170
170
|
private taskLocks;
|
|
171
171
|
private traceCollector?;
|
|
172
|
-
/**
|
|
172
|
+
/** interrupt 当时的 collector。timeout/resume 已离开图的调用链,不能从 state 恢复。 */
|
|
173
173
|
private readonly waitingCollectors;
|
|
174
174
|
constructor(checkpointSaver: ICheckpointSaver, sseEmitter: ISSEEmitter, timeoutListener?: HitlTimeoutListener);
|
|
175
175
|
setTimeoutListener(listener: HitlTimeoutListener | undefined): void;
|
|
@@ -182,6 +182,7 @@ export declare class HitlHandler {
|
|
|
182
182
|
* resume 时用 GraphState 从父图入口重入(skillSpanId / hitlChoice 防重复 span)。
|
|
183
183
|
*/
|
|
184
184
|
interrupt(taskId: string, threadId: string, state: GraphState, config: HitlInterruptConfig, extras?: HitlInterruptExtras): Promise<void>;
|
|
185
|
+
private interruptCore;
|
|
185
186
|
/**
|
|
186
187
|
* 用户响应后恢复执行
|
|
187
188
|
*
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { notifyCheckpointRef, stripCheckpointCollectors, } from '../../checkpoint/checkpoint-ref-notify.js';
|
|
14
14
|
import { startGraphSpan, completeGraphSpan } from '../trace-span.js';
|
|
15
|
+
import { resolveTraceId } from '../../common/trace-id.js';
|
|
16
|
+
import { executionTraceFromState, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
|
|
15
17
|
// ─────────────────────────────────────────────
|
|
16
18
|
// Types
|
|
17
19
|
// ─────────────────────────────────────────────
|
|
@@ -225,7 +227,7 @@ export class HitlHandler {
|
|
|
225
227
|
/** 同一 taskId 上串行化 resume 与 timeout,避免双边占用 */
|
|
226
228
|
taskLocks = new Map();
|
|
227
229
|
traceCollector;
|
|
228
|
-
/**
|
|
230
|
+
/** interrupt 当时的 collector。timeout/resume 已离开图的调用链,不能从 state 恢复。 */
|
|
229
231
|
waitingCollectors = new Map();
|
|
230
232
|
constructor(checkpointSaver, sseEmitter, timeoutListener) {
|
|
231
233
|
this.checkpointSaver = checkpointSaver;
|
|
@@ -246,6 +248,12 @@ export class HitlHandler {
|
|
|
246
248
|
* resume 时用 GraphState 从父图入口重入(skillSpanId / hitlChoice 防重复 span)。
|
|
247
249
|
*/
|
|
248
250
|
async interrupt(taskId, threadId, state, config, extras = {}) {
|
|
251
|
+
const work = () => this.interruptCore(taskId, threadId, state, config, extras);
|
|
252
|
+
// 图内 interrupt 已在这次调用链上;单测直接 interrupt 时从入参绑一次。
|
|
253
|
+
const ctx = getExecutionTrace()?.collector ? undefined : executionTraceFromState(state);
|
|
254
|
+
return ctx ? runWithExecutionTrace(ctx, work) : work();
|
|
255
|
+
}
|
|
256
|
+
async interruptCore(taskId, threadId, state, config, extras) {
|
|
249
257
|
const now = new Date().toISOString();
|
|
250
258
|
const timeoutMs = toHitlTimeoutMs(config.timeout);
|
|
251
259
|
const deadlineAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
@@ -292,7 +300,7 @@ export class HitlHandler {
|
|
|
292
300
|
};
|
|
293
301
|
const persistedCheckpoint = stripCheckpointCollectors(checkpointData);
|
|
294
302
|
await this.checkpointSaver.save(threadId, persistedCheckpoint);
|
|
295
|
-
const liveCollector =
|
|
303
|
+
const liveCollector = getExecutionTrace()?.collector;
|
|
296
304
|
if (liveCollector) {
|
|
297
305
|
this.waitingCollectors.set(taskId, liveCollector);
|
|
298
306
|
}
|
|
@@ -386,10 +394,15 @@ export class HitlHandler {
|
|
|
386
394
|
updatedAt: now,
|
|
387
395
|
}));
|
|
388
396
|
const hitlSpanId = waiting.hitlSpanId ?? persisted?.hitlSpanId;
|
|
389
|
-
|
|
397
|
+
const resumeTraceId = resolveTraceId(resumedState) ?? resolveTraceId(checkpoint.state);
|
|
398
|
+
const resumeCtx = liveCollector && resumeTraceId
|
|
399
|
+
? { collector: liveCollector, traceId: resumeTraceId }
|
|
400
|
+
: undefined;
|
|
401
|
+
// 用户确认不在图执行链上,必须重新绑 collector。
|
|
402
|
+
await runWithExecutionTrace(resumeCtx, () => completeGraphSpan(resumedState, hitlSpanId, {
|
|
390
403
|
status: 'completed',
|
|
391
404
|
output: { choice, userId },
|
|
392
|
-
}, liveCollector);
|
|
405
|
+
}, liveCollector));
|
|
393
406
|
const event = {
|
|
394
407
|
type: 'hitl.resumed',
|
|
395
408
|
data: { choice, userId },
|
|
@@ -445,10 +458,15 @@ export class HitlHandler {
|
|
|
445
458
|
updatedAt: new Date().toISOString(),
|
|
446
459
|
}));
|
|
447
460
|
const hitlSpanId = waiting.hitlSpanId ?? persisted.hitlSpanId;
|
|
448
|
-
|
|
461
|
+
const timeoutTraceId = resolveTraceId(checkpoint.state);
|
|
462
|
+
const timeoutCtx = liveCollector && timeoutTraceId
|
|
463
|
+
? { collector: liveCollector, traceId: timeoutTraceId }
|
|
464
|
+
: undefined;
|
|
465
|
+
// timer 回调不在图的这次调用链上。
|
|
466
|
+
await runWithExecutionTrace(timeoutCtx, () => completeGraphSpan(checkpoint.state, hitlSpanId, {
|
|
449
467
|
status: 'failed',
|
|
450
468
|
error: HITL_TIMEOUT_MESSAGE,
|
|
451
|
-
}, liveCollector);
|
|
469
|
+
}, liveCollector));
|
|
452
470
|
}
|
|
453
471
|
return timeoutEvent;
|
|
454
472
|
});
|
|
@@ -14,8 +14,8 @@ import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
|
|
|
14
14
|
import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
|
|
15
15
|
import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
|
|
16
16
|
import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
|
|
17
|
-
import { resolveTraceId } from '../../common/trace-id.js';
|
|
18
17
|
import { toolsetCodeFromLoaded } from '../../tool/mcp-list.js';
|
|
18
|
+
import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
|
|
19
19
|
function skillDirsFromState(state) {
|
|
20
20
|
const defs = state['skillDefinitions'];
|
|
21
21
|
return Object.values(defs ?? {})
|
|
@@ -102,8 +102,8 @@ export class ToolNode {
|
|
|
102
102
|
};
|
|
103
103
|
});
|
|
104
104
|
const executionContext = state['executionContext'];
|
|
105
|
-
|
|
106
|
-
const traceId =
|
|
105
|
+
// 不读 state.traceCollector:存档会剥掉,后续节点会空。
|
|
106
|
+
const { collector: traceCollector, traceId } = resolveExecutionTrace(state);
|
|
107
107
|
const agentCode = String(state['agentCode'] ?? '');
|
|
108
108
|
const nodeId = this.nodeId ?? 'tool-node';
|
|
109
109
|
const skillId = typeof state['activatedSkill'] === 'string' && state['activatedSkill']
|
|
@@ -142,7 +142,8 @@ export class ToolNode {
|
|
|
142
142
|
},
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
-
catch {
|
|
145
|
+
catch (err) {
|
|
146
|
+
warnTraceSpanFailure('createNode', { type: 'tool', name: req.toolName, traceId, toolCallId: req.toolCallId }, err);
|
|
146
147
|
return null;
|
|
147
148
|
}
|
|
148
149
|
}));
|
|
@@ -227,8 +228,8 @@ export class ToolNode {
|
|
|
227
228
|
error: result.error,
|
|
228
229
|
});
|
|
229
230
|
}
|
|
230
|
-
catch {
|
|
231
|
-
|
|
231
|
+
catch (err) {
|
|
232
|
+
warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: nodeId }, err);
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
return result;
|
|
@@ -237,6 +238,7 @@ export class ToolNode {
|
|
|
237
238
|
// HITL 暂停必须向上抛出,供 invoke/stream 返回 waiting_hitl(不可降级为 tool failure)
|
|
238
239
|
const hitlPaused = unwrapHitlPaused(err);
|
|
239
240
|
if (hitlPaused) {
|
|
241
|
+
// 暂停不 completeNode:span 保持 running,id 随错误交给 resume/timeout。
|
|
240
242
|
hitlPaused.toolSpanId = hitlPaused.toolSpanId ?? spanIds[i] ?? undefined;
|
|
241
243
|
hitlPaused.skillSpanId =
|
|
242
244
|
hitlPaused.skillSpanId ??
|
|
@@ -254,6 +256,18 @@ export class ToolNode {
|
|
|
254
256
|
error: message,
|
|
255
257
|
executionTime,
|
|
256
258
|
});
|
|
259
|
+
const failedSpanId = spanIds[i];
|
|
260
|
+
if (traceCollector && failedSpanId) {
|
|
261
|
+
try {
|
|
262
|
+
await traceCollector.completeNode(failedSpanId, {
|
|
263
|
+
status: 'failed',
|
|
264
|
+
error: message,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (completeErr) {
|
|
268
|
+
warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: failedSpanId }, completeErr);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
257
271
|
return {
|
|
258
272
|
toolCallId: req.toolCallId,
|
|
259
273
|
toolName: req.toolName,
|
package/dist/graph/trace-span.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Graph 节点 Span 辅助:createNode / completeNode 失败不挡主路径。
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { resolveExecutionTrace, warnTraceSpanFailure, } from './execution-trace-context.js';
|
|
5
5
|
export async function startGraphSpan(state, input) {
|
|
6
|
-
|
|
7
|
-
const traceId =
|
|
6
|
+
// collector 只从这次调用链取;state 只用来拼 parent/task 元数据。
|
|
7
|
+
const { collector, traceId } = resolveExecutionTrace(state);
|
|
8
8
|
if (!collector || !traceId)
|
|
9
9
|
return undefined;
|
|
10
10
|
const parentNodeId = input.parentSpanId ??
|
|
@@ -27,19 +27,20 @@ export async function startGraphSpan(state, input) {
|
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
catch {
|
|
30
|
+
catch (err) {
|
|
31
|
+
warnTraceSpanFailure('createNode', { type: input.type, name: input.name, traceId }, err);
|
|
31
32
|
return undefined;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
export async function completeGraphSpan(state, spanId, params, collectorOverride) {
|
|
35
|
-
const collector = collectorOverride ?? state
|
|
36
|
+
const collector = collectorOverride ?? resolveExecutionTrace(state).collector;
|
|
36
37
|
if (!collector?.completeNode || !spanId)
|
|
37
38
|
return;
|
|
38
39
|
try {
|
|
39
40
|
await collector.completeNode(spanId, params);
|
|
40
41
|
}
|
|
41
|
-
catch {
|
|
42
|
-
|
|
42
|
+
catch (err) {
|
|
43
|
+
warnTraceSpanFailure('completeNode', { spanId, traceId: resolveExecutionTrace(state).traceId }, err);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
export async function withGraphSpan(state, spec, run) {
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface ExecuteParams {
|
|
|
24
24
|
agentVersion?: string;
|
|
25
25
|
/**
|
|
26
26
|
* Hosting 已创建的 Trace id(Foundation `POST /traces` / TraceLinker.createTrace 的返回值)。
|
|
27
|
-
*
|
|
27
|
+
* 与装配期 `traceCollector` 同时存在时,invoke/stream 入口绑定到本次执行上下文供图内 span。
|
|
28
28
|
*/
|
|
29
29
|
traceId?: string;
|
|
30
30
|
config?: InvokeConfig;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gt-fe/eap-runtime",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Enterprise Agent Platform
|
|
3
|
+
"version": "0.0.19",
|
|
4
|
+
"description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|