@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 CHANGED
@@ -277,26 +277,28 @@ EAP_TOOL_EXECUTION_MODE=mcp # 仅 createDevRuntime / eap chat
277
277
 
278
278
  ---
279
279
 
280
- ## 11. 发布到公司私有 npm(可选)
280
+ ## 11. 发布 npm 包
281
281
 
282
- 私服:`https://maven.gtcloud.cn/repository/npm-local/`(模板 [`.npmrc.example`](./.npmrc.example))。
282
+ 升版、CHANGELOG、`git commit` / `git push` 不在脚本里,先做完再跑脚本。脚本只做构建、发布、核对 registry 上的版本号。
283
283
 
284
- > `@eap:registry=...` 须写入 `.npmrc` 文件,不可在 shell 中作为命令直接执行。
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
- # 1) 先发 @eap/contracts(version 改为目标版,如 0.0.1)
291
- cd ../eap-contracts && cp .npmrc.example .npmrc
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
- 含 `_authToken` 的 `.npmrc` 已 gitignore,勿提交凭证。
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 collector = (collectorOverride ??
38
- channels['traceCollector'] ??
39
- state['traceCollector']);
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
- const seen = new WeakSet();
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`), JSON.stringify(data));
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';
@@ -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
- JSON.stringify(data.historyPath),
42
- JSON.stringify(data.state),
43
- data.engineSnapshot == null ? null : JSON.stringify(data.engineSnapshot),
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 = JSON.stringify(data);
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
- * 无 streamSink 时为空操作(纯 invoke)。
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
- * 确保并发请求之间无共享可变状态。Adapter 自身仅持有 readonly 配置(checkpointer、subGraphCompiler),
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 执行事件:经 streamSink 在节点执行中实时旁路入队
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
- * 无 streamSink 时为空操作(纯 invoke)。
137
+ * 无本次执行流式袋时为空操作(纯 invoke)。
144
138
  */
145
139
  emitStreamEvent(event) {
146
- if (!this.streamSink)
140
+ const stream = getExecutionStream();
141
+ if (!stream)
147
142
  return;
148
- this.streamSink({
143
+ stream.sink({
149
144
  ...event,
150
- taskId: event.taskId ?? this.streamEventContext.taskId,
151
- correlationId: event.correlationId ?? this.streamEventContext.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 savedStreamed = this.streamedTextThisTurn;
159
- const savedReasoning = this.reasoningStartedThisTurn;
160
- this.reasoningStartedThisTurn = false;
161
- this.streamedTextThisTurn = false;
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
- this.streamedTextThisTurn = savedStreamed || this.streamedTextThisTurn;
167
- this.reasoningStartedThisTurn = savedReasoning || this.reasoningStartedThisTurn;
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
- if (!this.streamSink)
176
+ const stream = getExecutionStream();
177
+ if (!stream)
177
178
  return;
178
179
  const timestamp = new Date().toISOString();
179
- const { taskId, correlationId } = this.streamEventContext;
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
- this.streamSink({
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
- this.streamSink({
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
- this.streamSink({
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
- this.streamSink({
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
- this.streamSink({
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
- this.streamSink({
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
- * 确保并发请求之间无共享可变状态。Adapter 自身仅持有 readonly 配置(checkpointer、subGraphCompiler),
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 执行事件:经 streamSink 在节点执行中实时旁路入队
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.streamSink = enqueue;
438
- this.streamEventContext = { taskId: state.taskId, correlationId: state.correlationId };
439
- this.streamChunkIndex = 0;
440
- this.streamIteration = 0;
441
- this.reasoningStartedThisTurn = false;
442
- this.streamedTextThisTurn = false;
443
- const producer = (async () => {
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, () => this.streamChunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
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 result = await ref.compiled.invoke(input, langGraphRunOptions(ref.dsl, {
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
- if (patch.skillRoute === 'clarify' && this.streamSink) {
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
- this.streamSink({
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: this.streamChunkIndex++, node: node.id },
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
- // 流式旁路:有 streamSink 时启用 LLM token 推送;silent 节点不推给用户
896
- const enableStream = Boolean(this.streamSink) && !silent;
897
- this.reasoningStartedThisTurn = false;
898
- this.streamedTextThisTurn = false;
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
- if (!this.streamSink || !text)
909
+ const live = getExecutionStream();
910
+ if (!live || !text)
902
911
  return;
903
912
  const timestamp = new Date().toISOString();
904
- if (!this.reasoningStartedThisTurn) {
905
- this.reasoningStartedThisTurn = true;
906
- this.streamIteration += 1;
907
- this.streamSink({
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: this.streamIteration,
922
+ iterationCount: live.iteration,
914
923
  model: finalConfig.model,
915
924
  },
916
925
  });
917
926
  }
918
- this.streamedTextThisTurn = true;
919
- this.streamSink({
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: this.streamChunkIndex++,
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
- (!this.streamedTextThisTurn || msg.reasoningContent)) {
1270
- if (this.streamedTextThisTurn && !msg.reasoningContent)
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: this.streamedTextThisTurn ? '' : (msg.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 (this.streamedTextThisTurn) {
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 { resolveTraceId } from '../../common/trace-id.js';
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
- const traceCollector = state['traceCollector'];
64
- const traceId = resolveTraceId(state);
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
- // ignore
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
- // ignore
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
- /** Trace 采集器(RT-7;不入 Checkpoint 序列化语义) */
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
- /** 进程内 interrupt 的 collector,不进 Checkpoint */
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
- /** 进程内 interrupt 的 collector,不进 Checkpoint */
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 = state.traceCollector;
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
- await completeGraphSpan(resumedState, hitlSpanId, {
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
- await completeGraphSpan(checkpoint.state, hitlSpanId, {
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
- const traceCollector = state['traceCollector'];
106
- const traceId = resolveTraceId(state);
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
- // Trace 失败不影响主路径
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,
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Graph 节点 Span 辅助:createNode / completeNode 失败不挡主路径。
3
3
  */
4
- import { resolveTraceId } from '../common/trace-id.js';
4
+ import { resolveExecutionTrace, warnTraceSpanFailure, } from './execution-trace-context.js';
5
5
  export async function startGraphSpan(state, input) {
6
- const collector = state['traceCollector'];
7
- const traceId = resolveTraceId(state);
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['traceCollector'];
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
- // Trace 失败不影响主路径
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) {
@@ -24,7 +24,7 @@ export interface ExecuteParams {
24
24
  agentVersion?: string;
25
25
  /**
26
26
  * Hosting 已创建的 Trace id(Foundation `POST /traces` / TraceLinker.createTrace 的返回值)。
27
- * 写入 GraphState 供 Skill/Tool span。
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.17",
4
- "description": "Enterprise Agent Platform \u2014 Runtime\u6838\u5fc3\uff1aGraph\u6267\u884c\u5f15\u64ce\u3001Task\u751f\u547d\u5468\u671f\u3001Session\u7ba1\u7406\u3001Checkpoint\u6301\u4e45\u5316",
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",