@gt-fe/eap-runtime 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/api/handlers/invoke.handler.js +10 -16
  2. package/dist/api/handlers/stream.handler.js +3 -0
  3. package/dist/checkpoint/checkpoint-ref-notify.js +5 -4
  4. package/dist/contracts/index.d.ts +1 -1
  5. package/dist/contracts/trace-collector.d.ts +16 -8
  6. package/dist/graph/adapters/langgraph.adapter.d.ts +5 -12
  7. package/dist/graph/adapters/langgraph.adapter.js +109 -64
  8. package/dist/graph/adapters/skill-entry-span.d.ts +37 -0
  9. package/dist/graph/adapters/skill-entry-span.js +78 -0
  10. package/dist/graph/adapters/skill-read.d.ts +10 -0
  11. package/dist/graph/adapters/skill-read.js +33 -0
  12. package/dist/graph/adapters/subgraph-compiler.js +22 -11
  13. package/dist/graph/contracts/executable-graph.d.ts +4 -1
  14. package/dist/graph/execution-trace-context.d.ts +38 -0
  15. package/dist/graph/execution-trace-context.js +62 -0
  16. package/dist/graph/hitl/hitl-handler.d.ts +2 -1
  17. package/dist/graph/hitl/hitl-handler.js +24 -6
  18. package/dist/graph/nodes/tool-node.d.ts +6 -0
  19. package/dist/graph/nodes/tool-node.js +51 -8
  20. package/dist/graph/skill-audit-recorder.d.ts +4 -4
  21. package/dist/graph/skill-audit-recorder.js +3 -3
  22. package/dist/graph/trace-span.d.ts +2 -9
  23. package/dist/graph/trace-span.js +9 -7
  24. package/dist/index.d.ts +1 -1
  25. package/dist/runtime/agent-runtime.js +22 -0
  26. package/dist/runtime/types.d.ts +1 -1
  27. package/dist/sse/trace-collector.d.ts +2 -9
  28. package/dist/token/index.d.ts +2 -0
  29. package/dist/token/index.js +1 -0
  30. package/dist/token/record-task-token-usage.d.ts +21 -0
  31. package/dist/token/record-task-token-usage.js +52 -0
  32. package/dist/token/token-stats.service.d.ts +2 -0
  33. package/dist/token/token-stats.service.js +4 -0
  34. package/dist/tool/index.d.ts +1 -1
  35. package/dist/tool/index.js +1 -1
  36. package/dist/tool/mcp-list.d.ts +12 -0
  37. package/dist/tool/mcp-list.js +15 -0
  38. package/package.json +1 -1
@@ -18,6 +18,7 @@ import { randomUUID } from 'node:crypto';
18
18
  import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, } from '../../session/persist-turn-messages.js';
19
19
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
20
20
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
21
+ import { recordTaskTokenUsages } from '../../token/record-task-token-usage.js';
21
22
  import { resolveExecutionContext } from '../../governance/governance-client.js';
22
23
  import { resolveSkillInputFromInvokeInput } from '../../registry/skill-io-guard.js';
23
24
  import { resolveAgentSystemPromptFromLoader } from '../../registry/graph-loader.js';
@@ -310,24 +311,17 @@ export class InvokeOrchestrator {
310
311
  };
311
312
  // 将 Graph 层面的 token 消耗记录到 Task(按节点明细逐个记录,便于审计)
312
313
  if (graphUsage.perNode && graphUsage.perNode.length > 0) {
313
- for (const u of graphUsage.perNode) {
314
- await this.taskService.recordTokenUsage(task.id, {
315
- nodeId: u.nodeId,
316
- model: u.model,
317
- inputTokens: u.inputTokens,
318
- outputTokens: u.outputTokens,
319
- costEstimate: 0,
320
- });
321
- }
314
+ await recordTaskTokenUsages(this.taskService, task.id, graphUsage.perNode);
322
315
  }
323
316
  else if (graphUsage.total > 0) {
324
- await this.taskService.recordTokenUsage(task.id, {
325
- nodeId: 'graph',
326
- model: 'unknown',
327
- inputTokens: graphUsage.input,
328
- outputTokens: graphUsage.output,
329
- costEstimate: 0,
330
- });
317
+ await recordTaskTokenUsages(this.taskService, task.id, [
318
+ {
319
+ nodeId: 'graph',
320
+ model: 'unknown',
321
+ inputTokens: graphUsage.input,
322
+ outputTokens: graphUsage.output,
323
+ },
324
+ ]);
331
325
  }
332
326
  // ─── Step 7: 完成 Task (running → success) ───
333
327
  await this.taskService.updateStatus(task.id, 'success', graphResult.output);
@@ -22,6 +22,7 @@
22
22
  import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscriptFromStreamEvents, extractStreamReasoning, extractStreamTextDelta, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, } from '../../session/persist-turn-messages.js';
23
23
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
24
24
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
25
+ import { recordTaskTokenUsages } from '../../token/record-task-token-usage.js';
25
26
  import { SSEEmitter } from '../../sse/sse-emitter.js';
26
27
  import { resolveExecutionContext } from '../../governance/governance-client.js';
27
28
  import { resolveSkillInputFromInvokeInput } from '../../registry/skill-io-guard.js';
@@ -303,6 +304,7 @@ export class StreamOrchestrator {
303
304
  catch (error) {
304
305
  const hitlPaused = unwrapHitlPaused(error);
305
306
  if (hitlPaused) {
307
+ await recordTaskTokenUsages(this.taskService, taskId, this.tokenStatsService.list(taskId));
306
308
  await this.taskService.updateStatus(taskId, 'waiting_hitl', buildHitlWaitingTaskOutput({
307
309
  threadId: hitlPaused.threadId,
308
310
  question: hitlPaused.question,
@@ -370,6 +372,7 @@ export class StreamOrchestrator {
370
372
  taskId,
371
373
  duration,
372
374
  }));
375
+ await recordTaskTokenUsages(this.taskService, taskId, this.tokenStatsService.list(taskId));
373
376
  // Task 状态流转 → success
374
377
  await this.taskService.updateStatus(taskId, 'success');
375
378
  // ─── Step 8b: 持久化 transcript(与 invoke 同序)───
@@ -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;
@@ -7,5 +7,5 @@
7
7
  * - LLM 调用:由模型路由模块实现
8
8
  * - 存储接口:由各存储后端实现
9
9
  */
10
- export type { ITraceCollector, TraceEvent, TraceSession, } from './trace-collector.js';
10
+ export type { ITraceCollector, CompleteNodeParams, TraceEvent, TraceSession, } from './trace-collector.js';
11
11
  export type { IPermissionChecker } from './permission-checker.js';
@@ -85,6 +85,8 @@ export interface ExecutionNodeEvent {
85
85
  type: 'start' | 'llm' | 'tool' | 'skill' | 'condition' | 'hitl' | 'end';
86
86
  /** 工具运行环境(type=tool) */
87
87
  toolRuntime?: 'local' | 'server' | 'both';
88
+ /** skill/tool 包版本;与 TraceClient RecordSpanParams.version 同义 */
89
+ version?: string;
88
90
  /** Agent 对外主键 */
89
91
  agentCode?: string;
90
92
  /** 输入数据 */
@@ -92,6 +94,19 @@ export interface ExecutionNodeEvent {
92
94
  /** 扩展元数据(如 skillId) */
93
95
  metadata?: Record<string, unknown>;
94
96
  }
97
+ /**
98
+ * 完成执行节点参数。llm Span 的 model / token 与 stats.node_token_usage 同源。
99
+ */
100
+ export interface CompleteNodeParams {
101
+ status: 'completed' | 'failed';
102
+ output?: unknown;
103
+ error?: string;
104
+ inputTokens?: number;
105
+ outputTokens?: number;
106
+ totalTokens?: number;
107
+ /** llm Span:实际模型标识;与 NodeTokenUsage.model 相同 */
108
+ model?: string;
109
+ }
95
110
  /**
96
111
  * 链路事件
97
112
  *
@@ -176,14 +191,7 @@ export interface ITraceCollector {
176
191
  * @param nodeId - 节点 ID
177
192
  * @param params - 完成参数
178
193
  */
179
- completeNode(nodeId: string, params: {
180
- status: 'completed' | 'failed';
181
- output?: unknown;
182
- error?: string;
183
- inputTokens?: number;
184
- outputTokens?: number;
185
- totalTokens?: number;
186
- }): Promise<void>;
194
+ completeNode(nodeId: string, params: CompleteNodeParams): Promise<void>;
187
195
  /**
188
196
  * Checkpoint 已落盘后的引用回调。Hosting 适配 TraceClient.recordCheckpointRef。
189
197
  * Runtime 仅在同时有 traceId 与 spanId 时调用;不传 graph state 正文。
@@ -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
  /**
@@ -67,14 +60,14 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
67
60
  * Tool 节点执行旁路:started → tool.called + tool.progress;
68
61
  * completed/error → 对应终态 + tool.progress。同一 toolCallId。
69
62
  * 读 SKILL.md / 技能包内文件:成功后再发 skill.loaded / skill.resource_read,同时保留 tool.*。
63
+ * 读 SKILL.md 另发 skill.started / skill.completed(与 Trace type=skill Span 对齐);子图内不重复。
70
64
  */
71
65
  private emitToolExecutionEvents;
72
66
  /**
73
67
  * 解析 GraphDSL 并构建可执行图
74
68
  *
75
- * **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译,
76
- * 确保并发请求之间无共享可变状态。Adapter 自身仅持有 readonly 配置(checkpointer、subGraphCompiler),
77
- * 不缓存任何编译产物或运行时状态。
69
+ * **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译。
70
+ * 一次执行的 collector / 流式袋跟这次调用走,不挂 Adapter 字段。
78
71
  *
79
72
  * 流程:
80
73
  * 1. 创建 StateGraph 实例(配置 channels)
@@ -89,7 +82,7 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
89
82
  * 流式驱动图执行,逐步 yield RuntimeEvent
90
83
  *
91
84
  * - LangGraph `streamMode: "updates"`:节点完成后产出 response.chunk / stats 等
92
- * - LLM token / Tool 执行事件:经 streamSink 在节点执行中实时旁路入队
85
+ * - LLM token / Tool 执行事件:经本次执行流式袋在节点执行中实时旁路入队
93
86
  * (当前 @langchain/langgraph@0.2 无 messages mode,故用 LLMNode.onChunk 模拟)
94
87
  */
95
88
  stream(graph: ExecutableGraph, state: GraphState): AsyncIterableIterator<RuntimeEvent>;
@@ -30,7 +30,9 @@ import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
30
30
  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
+ import { skillEntryInvocationFromTool } from './skill-entry-span.js';
33
34
  import { withGraphSpan } from '../trace-span.js';
35
+ import { executionTraceFromState, getExecutionStream, runWithExecutionTrace, } from '../execution-trace-context.js';
34
36
  /** 图未写 `config.maxIterations` 时的 recursionLimit。`agent ⇄ tools` 一圈约 2,50 ≈ 24 次工具往返。 */
35
37
  export const DEFAULT_GRAPH_RECURSION_LIMIT = 50;
36
38
  /**
@@ -68,13 +70,6 @@ export class LangGraphAdapter {
68
70
  conditionEvaluator;
69
71
  systemPromptBuilder;
70
72
  skillAuditRecorder;
71
- /** 流式旁路:LLM token / Tool 执行事件在节点执行中实时入队 */
72
- streamSink = null;
73
- streamEventContext = {};
74
- streamChunkIndex = 0;
75
- streamIteration = 0;
76
- reasoningStartedThisTurn = false;
77
- streamedTextThisTurn = false;
78
73
  /**
79
74
  * @param checkpointSaver - 可选的 ICheckpointSaver
80
75
  * @param llmProvider - LLM 调用提供者,默认 EchoLLMProvider
@@ -140,43 +135,51 @@ export class LangGraphAdapter {
140
135
  }
141
136
  /**
142
137
  * 节点旁路推送 RuntimeEvent(子图 Skill 生命周期、Tool 执行)。
143
- * 无 streamSink 时为空操作(纯 invoke)。
138
+ * 无本次执行流式袋时为空操作(纯 invoke)。
144
139
  */
145
140
  emitStreamEvent(event) {
146
- if (!this.streamSink)
141
+ const stream = getExecutionStream();
142
+ if (!stream)
147
143
  return;
148
- this.streamSink({
144
+ stream.sink({
149
145
  ...event,
150
- taskId: event.taskId ?? this.streamEventContext.taskId,
151
- correlationId: event.correlationId ?? this.streamEventContext.correlationId,
146
+ taskId: event.taskId ?? stream.eventContext.taskId,
147
+ correlationId: event.correlationId ?? stream.eventContext.correlationId,
152
148
  });
153
149
  }
154
150
  /**
155
151
  * 子图 LLM 可清零流式标志;结束后保留「已经 stream 过」以免父图 convertUpdates 再发一遍正文。
156
152
  */
157
153
  async runIsolatedLlmStreamFlags(work) {
158
- const savedStreamed = this.streamedTextThisTurn;
159
- const savedReasoning = this.reasoningStartedThisTurn;
160
- this.reasoningStartedThisTurn = false;
161
- this.streamedTextThisTurn = false;
154
+ const stream = getExecutionStream();
155
+ const savedStreamed = stream?.streamedTextThisTurn ?? false;
156
+ const savedReasoning = stream?.reasoningStartedThisTurn ?? false;
157
+ if (stream) {
158
+ stream.reasoningStartedThisTurn = false;
159
+ stream.streamedTextThisTurn = false;
160
+ }
162
161
  try {
163
162
  return await work();
164
163
  }
165
164
  finally {
166
- this.streamedTextThisTurn = savedStreamed || this.streamedTextThisTurn;
167
- this.reasoningStartedThisTurn = savedReasoning || this.reasoningStartedThisTurn;
165
+ if (stream) {
166
+ stream.streamedTextThisTurn = savedStreamed || stream.streamedTextThisTurn;
167
+ stream.reasoningStartedThisTurn = savedReasoning || stream.reasoningStartedThisTurn;
168
+ }
168
169
  }
169
170
  }
170
171
  /**
171
172
  * Tool 节点执行旁路:started → tool.called + tool.progress;
172
173
  * completed/error → 对应终态 + tool.progress。同一 toolCallId。
173
174
  * 读 SKILL.md / 技能包内文件:成功后再发 skill.loaded / skill.resource_read,同时保留 tool.*。
175
+ * 读 SKILL.md 另发 skill.started / skill.completed(与 Trace type=skill Span 对齐);子图内不重复。
174
176
  */
175
177
  emitToolExecutionEvents(progress) {
176
- if (!this.streamSink)
178
+ const stream = getExecutionStream();
179
+ if (!stream)
177
180
  return;
178
181
  const timestamp = new Date().toISOString();
179
- const { taskId, correlationId } = this.streamEventContext;
182
+ const { taskId, correlationId } = stream.eventContext;
180
183
  const loc = {
181
184
  ...(progress.node ? { node: progress.node } : {}),
182
185
  ...(progress.skillId ? { skillId: progress.skillId } : {}),
@@ -184,19 +187,51 @@ export class LangGraphAdapter {
184
187
  const skillRead = classifySkillRead(progress.toolName, progress.arguments, progress.skillDirs);
185
188
  if (skillRead && progress.status === 'completed') {
186
189
  if (skillRead.kind === 'loaded') {
187
- this.streamSink({
190
+ const skillId = skillRead.skillId || loc.skillId || skillRead.path;
191
+ stream.sink({
188
192
  type: 'skill.loaded',
189
193
  taskId,
190
194
  correlationId,
191
195
  timestamp,
192
196
  payload: {
193
- skillId: skillRead.skillId || loc.skillId || skillRead.path,
197
+ skillId,
194
198
  path: skillRead.path,
195
199
  },
196
200
  });
201
+ const catalogSkill = skillEntryInvocationFromTool({
202
+ toolName: progress.toolName,
203
+ arguments: progress.arguments,
204
+ skillDirs: progress.skillDirs,
205
+ alreadyInSkillSpan: progress.inSkillSubgraph,
206
+ });
207
+ if (catalogSkill) {
208
+ stream.sink({
209
+ type: 'skill.started',
210
+ taskId,
211
+ correlationId,
212
+ timestamp,
213
+ payload: {
214
+ skillId: catalogSkill.skillId,
215
+ skillName: progress.skillName || catalogSkill.skillId,
216
+ path: catalogSkill.path,
217
+ ...(progress.skillSpace ? { skillSpace: progress.skillSpace } : {}),
218
+ ...(progress.skillVersion ? { skillVersion: progress.skillVersion } : {}),
219
+ },
220
+ });
221
+ stream.sink({
222
+ type: 'skill.completed',
223
+ taskId,
224
+ correlationId,
225
+ timestamp,
226
+ payload: {
227
+ skillId: catalogSkill.skillId,
228
+ skillName: catalogSkill.skillId,
229
+ },
230
+ });
231
+ }
197
232
  }
198
233
  else {
199
- this.streamSink({
234
+ stream.sink({
200
235
  type: 'skill.resource_read',
201
236
  taskId,
202
237
  correlationId,
@@ -206,7 +241,7 @@ export class LangGraphAdapter {
206
241
  }
207
242
  }
208
243
  if (progress.status === 'started') {
209
- this.streamSink({
244
+ stream.sink({
210
245
  type: 'tool.called',
211
246
  taskId,
212
247
  correlationId,
@@ -215,12 +250,13 @@ export class LangGraphAdapter {
215
250
  toolCallId: progress.toolCallId,
216
251
  toolName: progress.toolName,
217
252
  ...(progress.toolset ? { toolset: progress.toolset } : {}),
253
+ ...(progress.toolVersion ? { toolVersion: progress.toolVersion } : {}),
218
254
  arguments: progress.arguments ?? {},
219
255
  ...loc,
220
256
  },
221
257
  });
222
258
  }
223
- this.streamSink({
259
+ stream.sink({
224
260
  type: 'tool.progress',
225
261
  taskId,
226
262
  correlationId,
@@ -228,7 +264,7 @@ export class LangGraphAdapter {
228
264
  payload: { ...progress },
229
265
  });
230
266
  if (progress.status === 'completed') {
231
- this.streamSink({
267
+ stream.sink({
232
268
  type: 'tool.result',
233
269
  taskId,
234
270
  correlationId,
@@ -244,7 +280,7 @@ export class LangGraphAdapter {
244
280
  });
245
281
  }
246
282
  else if (progress.status === 'error') {
247
- this.streamSink({
283
+ stream.sink({
248
284
  type: 'tool.error',
249
285
  taskId,
250
286
  correlationId,
@@ -263,9 +299,8 @@ export class LangGraphAdapter {
263
299
  /**
264
300
  * 解析 GraphDSL 并构建可执行图
265
301
  *
266
- * **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译,
267
- * 确保并发请求之间无共享可变状态。Adapter 自身仅持有 readonly 配置(checkpointer、subGraphCompiler),
268
- * 不缓存任何编译产物或运行时状态。
302
+ * **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译。
303
+ * 一次执行的 collector / 流式袋跟这次调用走,不挂 Adapter 字段。
269
304
  *
270
305
  * 流程:
271
306
  * 1. 创建 StateGraph 实例(配置 channels)
@@ -410,7 +445,7 @@ export class LangGraphAdapter {
410
445
  * 流式驱动图执行,逐步 yield RuntimeEvent
411
446
  *
412
447
  * - LangGraph `streamMode: "updates"`:节点完成后产出 response.chunk / stats 等
413
- * - LLM token / Tool 执行事件:经 streamSink 在节点执行中实时旁路入队
448
+ * - LLM token / Tool 执行事件:经本次执行流式袋在节点执行中实时旁路入队
414
449
  * (当前 @langchain/langgraph@0.2 无 messages mode,故用 LLMNode.onChunk 模拟)
415
450
  */
416
451
  async *stream(graph, state) {
@@ -434,13 +469,17 @@ export class LangGraphAdapter {
434
469
  });
435
470
  signal();
436
471
  };
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 () => {
472
+ // 流式袋跟这次调用走,不挂 this:并行 stream 各写各的 queue。
473
+ const streamBag = {
474
+ sink: enqueue,
475
+ eventContext: { taskId: state.taskId, correlationId: state.correlationId },
476
+ chunkIndex: 0,
477
+ iteration: 0,
478
+ reasoningStartedThisTurn: false,
479
+ streamedTextThisTurn: false,
480
+ };
481
+ const traceCtx = executionTraceFromState(state);
482
+ const producer = runWithExecutionTrace({ ...traceCtx, stream: streamBag }, () => (async () => {
444
483
  try {
445
484
  const streamIterator = await ref.compiled.stream(input, langGraphRunOptions(ref.dsl, {
446
485
  configurable: {
@@ -453,7 +492,7 @@ export class LangGraphAdapter {
453
492
  streamMode: 'updates',
454
493
  }));
455
494
  for await (const update of streamIterator) {
456
- for (const event of this.convertUpdatesToEvents(update, graph.id, () => this.streamChunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
495
+ for (const event of this.convertUpdatesToEvents(update, graph.id, () => streamBag.chunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
457
496
  enqueue(event);
458
497
  }
459
498
  }
@@ -463,11 +502,9 @@ export class LangGraphAdapter {
463
502
  }
464
503
  finally {
465
504
  finished = true;
466
- this.streamSink = null;
467
- this.streamEventContext = {};
468
505
  signal();
469
506
  }
470
- })();
507
+ })());
471
508
  while (!finished || queue.length > 0) {
472
509
  if (queue.length === 0) {
473
510
  await new Promise((resolve) => {
@@ -488,7 +525,9 @@ export class LangGraphAdapter {
488
525
  const ref = this.getCompiledRef(graph);
489
526
  const input = this.graphStateToLangGraphInput(state);
490
527
  const startTime = Date.now();
491
- const result = await ref.compiled.invoke(input, langGraphRunOptions(ref.dsl, {
528
+ const traceCtx = executionTraceFromState(state);
529
+ // 只补 collector/traceId。子图套娃时外层 stream 袋经 ?? 保留。
530
+ const result = await runWithExecutionTrace(traceCtx, () => ref.compiled.invoke(input, langGraphRunOptions(ref.dsl, {
492
531
  configurable: {
493
532
  thread_id: resolveCheckpointThreadId({
494
533
  sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
@@ -496,7 +535,7 @@ export class LangGraphAdapter {
496
535
  graphId: graph.id,
497
536
  }),
498
537
  },
499
- }));
538
+ })));
500
539
  const duration = Date.now() - startTime;
501
540
  const messages = Array.isArray(result['messages']) ? result['messages'] : [];
502
541
  const lastMessage = messages[messages.length - 1];
@@ -661,15 +700,16 @@ export class LangGraphAdapter {
661
700
  },
662
701
  });
663
702
  }
664
- if (patch.skillRoute === 'clarify' && this.streamSink) {
703
+ const stream = getExecutionStream();
704
+ if (patch.skillRoute === 'clarify' && stream) {
665
705
  const question = patch.messages.at(-1)?.content;
666
706
  if (typeof question === 'string' && question.trim()) {
667
- this.streamSink({
707
+ stream.sink({
668
708
  type: 'response.chunk',
669
709
  taskId: undefined,
670
710
  correlationId: undefined,
671
711
  timestamp: new Date().toISOString(),
672
- payload: { content: question, index: this.streamChunkIndex++, node: node.id },
712
+ payload: { content: question, index: stream.chunkIndex++, node: node.id },
673
713
  });
674
714
  }
675
715
  }
@@ -892,38 +932,42 @@ export class LangGraphAdapter {
892
932
  tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
893
933
  toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
894
934
  };
895
- // 流式旁路:有 streamSink 时启用 LLM token 推送;silent 节点不推给用户
896
- const enableStream = Boolean(this.streamSink) && !silent;
897
- this.reasoningStartedThisTurn = false;
898
- this.streamedTextThisTurn = false;
935
+ // 流式旁路:有本次 stream 袋时启用 LLM token 推送;silent 节点不推给用户
936
+ const stream = getExecutionStream();
937
+ const enableStream = Boolean(stream) && !silent;
938
+ if (stream) {
939
+ stream.reasoningStartedThisTurn = false;
940
+ stream.streamedTextThisTurn = false;
941
+ }
899
942
  const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
900
943
  ? (text) => {
901
- if (!this.streamSink || !text)
944
+ const live = getExecutionStream();
945
+ if (!live || !text)
902
946
  return;
903
947
  const timestamp = new Date().toISOString();
904
- if (!this.reasoningStartedThisTurn) {
905
- this.reasoningStartedThisTurn = true;
906
- this.streamIteration += 1;
907
- this.streamSink({
948
+ if (!live.reasoningStartedThisTurn) {
949
+ live.reasoningStartedThisTurn = true;
950
+ live.iteration += 1;
951
+ live.sink({
908
952
  type: 'reasoning.started',
909
953
  taskId: undefined,
910
954
  correlationId: undefined,
911
955
  timestamp,
912
956
  payload: {
913
- iterationCount: this.streamIteration,
957
+ iterationCount: live.iteration,
914
958
  model: finalConfig.model,
915
959
  },
916
960
  });
917
961
  }
918
- this.streamedTextThisTurn = true;
919
- this.streamSink({
962
+ live.streamedTextThisTurn = true;
963
+ live.sink({
920
964
  type: 'response.chunk',
921
965
  taskId: undefined,
922
966
  correlationId: undefined,
923
967
  timestamp,
924
968
  payload: {
925
969
  content: text,
926
- index: this.streamChunkIndex++,
970
+ index: live.chunkIndex++,
927
971
  node: node.id,
928
972
  },
929
973
  });
@@ -1229,6 +1273,7 @@ export class LangGraphAdapter {
1229
1273
  */
1230
1274
  *convertUpdatesToEvents(update, graphId, nextChunkIndex, silentNodeIds = new Set(), subgraphNodeIds = new Set()) {
1231
1275
  const timestamp = new Date().toISOString();
1276
+ const streamedTextThisTurn = Boolean(getExecutionStream()?.streamedTextThisTurn);
1232
1277
  const entries = this.normalizeStreamUpdates(update);
1233
1278
  for (const { nodeId, data } of entries) {
1234
1279
  // LLM 节点 token 明细(写入 __llmTokenUsage)→ stats.node_token_usage
@@ -1266,8 +1311,8 @@ export class LangGraphAdapter {
1266
1311
  }
1267
1312
  const hasText = typeof msg.content === 'string' && msg.content.trim().length > 0;
1268
1313
  if ((hasText || msg.reasoningContent) &&
1269
- (!this.streamedTextThisTurn || msg.reasoningContent)) {
1270
- if (this.streamedTextThisTurn && !msg.reasoningContent)
1314
+ (!streamedTextThisTurn || msg.reasoningContent)) {
1315
+ if (streamedTextThisTurn && !msg.reasoningContent)
1271
1316
  continue;
1272
1317
  yield {
1273
1318
  type: 'response.chunk',
@@ -1275,7 +1320,7 @@ export class LangGraphAdapter {
1275
1320
  correlationId: undefined,
1276
1321
  timestamp,
1277
1322
  payload: {
1278
- content: this.streamedTextThisTurn ? '' : (msg.content ?? ''),
1323
+ content: streamedTextThisTurn ? '' : (msg.content ?? ''),
1279
1324
  index: nextChunkIndex(),
1280
1325
  node: nodeId,
1281
1326
  graphId,
@@ -1291,7 +1336,7 @@ export class LangGraphAdapter {
1291
1336
  if (silentNodeIds.has(nodeId) || subgraphNodeIds.has(nodeId))
1292
1337
  continue;
1293
1338
  // 已通过 onChunk 推送过 token,避免整段重复
1294
- if (this.streamedTextThisTurn) {
1339
+ if (streamedTextThisTurn) {
1295
1340
  // token 已流过:仍补发一条带 reasoning 的标记 chunk(空 content),供落库回传
1296
1341
  if (msg.reasoningContent) {
1297
1342
  yield {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 成功读 SKILL.md 时写 Trace `type=skill` Span。
3
+ *
4
+ * 假设:Skill 被调用 = 一次成功读取技能包入口(SKILL.md),
5
+ * 与子图激活走同一 collector.createNode({ type: 'skill' })。
6
+ * 声明版本在 createNode.version;skillSpace 只在 input。
7
+ * 子图已有 skillSpanId 时不记第二条。write_file / 包内附属读 / 失败读不是调用。
8
+ */
9
+ import { type SkillDirRef, type SkillSpace } from './skill-read.js';
10
+ export interface SkillTraceIdentity {
11
+ skillId: string;
12
+ skillName?: string;
13
+ skillVersion?: string;
14
+ skillSpace?: SkillSpace;
15
+ }
16
+ export declare function skillTraceIdentity(state: Record<string, unknown>, skillId: string, fallbackPath?: string): SkillTraceIdentity;
17
+ export declare function skillSpanFields(identity: SkillTraceIdentity, extra?: {
18
+ path?: string;
19
+ source?: string;
20
+ }): {
21
+ version?: string;
22
+ input: Record<string, unknown>;
23
+ };
24
+ export declare function skillEntryInvocationFromTool(input: {
25
+ toolName: string;
26
+ arguments?: Record<string, unknown>;
27
+ skillDirs?: SkillDirRef[];
28
+ alreadyInSkillSpan?: boolean;
29
+ }): {
30
+ skillId: string;
31
+ path: string;
32
+ } | null;
33
+ export declare function recordSkillEntrySpan(state: Record<string, unknown>, input: {
34
+ toolName: string;
35
+ arguments?: Record<string, unknown>;
36
+ skillDirs?: SkillDirRef[];
37
+ }): Promise<void>;