@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
@@ -0,0 +1,78 @@
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 { classifySkillRead, classifySkillSpace } from './skill-read.js';
10
+ import { completeGraphSpan, startGraphSpan } from '../trace-span.js';
11
+ function declaredSkillVersion(skillId, version) {
12
+ const raw = version?.trim();
13
+ if (!raw || raw === skillId)
14
+ return undefined;
15
+ return raw;
16
+ }
17
+ export function skillTraceIdentity(state, skillId, fallbackPath) {
18
+ const defs = state['skillDefinitions'];
19
+ const info = defs?.[skillId];
20
+ const exec = state['executionContext'];
21
+ const userId = (typeof exec?.userId === 'string' && exec.userId.trim()) ||
22
+ (typeof state['userId'] === 'string' && state['userId'].trim()) ||
23
+ undefined;
24
+ const agentDir = typeof state['agentDir'] === 'string' ? state['agentDir'] : undefined;
25
+ const bundle = info?.bundlePath || fallbackPath;
26
+ const skillSpace = bundle ? classifySkillSpace(bundle, { agentDir, userId }) : undefined;
27
+ const skillVersion = declaredSkillVersion(skillId, info?.version);
28
+ return {
29
+ skillId,
30
+ ...(info?.name ? { skillName: info.name } : {}),
31
+ ...(skillVersion ? { skillVersion } : {}),
32
+ ...(skillSpace ? { skillSpace } : {}),
33
+ };
34
+ }
35
+ export function skillSpanFields(identity, extra) {
36
+ return {
37
+ ...(identity.skillVersion ? { version: identity.skillVersion } : {}),
38
+ input: {
39
+ skillId: identity.skillId,
40
+ ...(identity.skillSpace ? { skillSpace: identity.skillSpace } : {}),
41
+ ...(extra?.path ? { path: extra.path } : {}),
42
+ ...(extra?.source ? { source: extra.source } : {}),
43
+ },
44
+ };
45
+ }
46
+ export function skillEntryInvocationFromTool(input) {
47
+ if (input.alreadyInSkillSpan)
48
+ return null;
49
+ const hit = classifySkillRead(input.toolName, input.arguments, input.skillDirs);
50
+ if (!hit || hit.kind !== 'loaded' || !hit.skillId)
51
+ return null;
52
+ return { skillId: hit.skillId, path: hit.path };
53
+ }
54
+ export async function recordSkillEntrySpan(state, input) {
55
+ const alreadyInSkillSpan = typeof state['skillSpanId'] === 'string' && Boolean(state['skillSpanId']);
56
+ const hit = skillEntryInvocationFromTool({
57
+ toolName: input.toolName,
58
+ arguments: input.arguments,
59
+ skillDirs: input.skillDirs,
60
+ alreadyInSkillSpan,
61
+ });
62
+ if (!hit)
63
+ return;
64
+ const identity = skillTraceIdentity(state, hit.skillId, hit.path);
65
+ const fields = skillSpanFields(identity, { path: hit.path, source: 'skill_md' });
66
+ const spanId = await startGraphSpan(state, {
67
+ name: hit.skillId,
68
+ type: 'skill',
69
+ extra: {
70
+ ...(fields.version ? { version: fields.version } : {}),
71
+ input: fields.input,
72
+ },
73
+ });
74
+ await completeGraphSpan(state, spanId, {
75
+ status: 'completed',
76
+ output: { skillId: hit.skillId, path: hit.path },
77
+ });
78
+ }
@@ -2,6 +2,16 @@ export interface SkillDirRef {
2
2
  skillId: string;
3
3
  bundlePath: string;
4
4
  }
5
+ /** 技能包落点:Agent 预装 / 用户装配 / 开发态 overlay。 */
6
+ export type SkillSpace = 'agent' | 'user' | 'local';
7
+ /**
8
+ * 按实际 bundle 路径判定空间。有 agentDir 时相对预装根与 users/ 判定;
9
+ * 否则看路径段 `preinstall/skills` / `users/<id>/skills`。
10
+ */
11
+ export declare function classifySkillSpace(bundleOrFilePath: string, opts?: {
12
+ agentDir?: string;
13
+ userId?: string;
14
+ }): SkillSpace | undefined;
5
15
  /**
6
16
  * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
7
17
  */
@@ -33,6 +33,39 @@ function skillIdFromSkillsSegment(norm) {
33
33
  const match = norm.match(/(?:^|\/)(?:preinstall\/)?skills\/([^/]+)(?:\/|$)/i);
34
34
  return match?.[1] ?? '';
35
35
  }
36
+ /**
37
+ * 按实际 bundle 路径判定空间。有 agentDir 时相对预装根与 users/ 判定;
38
+ * 否则看路径段 `preinstall/skills` / `users/<id>/skills`。
39
+ */
40
+ export function classifySkillSpace(bundleOrFilePath, opts) {
41
+ if (!bundleOrFilePath)
42
+ return undefined;
43
+ const target = normalizeForMatch(bundleOrFilePath);
44
+ const agentDir = opts?.agentDir?.trim();
45
+ if (agentDir) {
46
+ const root = normalizeForMatch(agentDir);
47
+ const pre = `${root}/preinstall/skills`;
48
+ if (target === pre || target.startsWith(`${pre}/`))
49
+ return 'agent';
50
+ const uid = opts?.userId?.trim();
51
+ if (uid) {
52
+ const user = `${root}/users/${uid}/skills`;
53
+ if (target === user || target.startsWith(`${user}/`))
54
+ return 'user';
55
+ }
56
+ if (new RegExp(`^${escapeRegExp(root)}/users/[^/]+/skills(/|$)`).test(target))
57
+ return 'user';
58
+ return 'local';
59
+ }
60
+ if (/(^|\/)preinstall\/skills(\/|$)/i.test(target))
61
+ return 'agent';
62
+ if (/(^|\/)users\/[^/]+\/skills(\/|$)/i.test(target))
63
+ return 'user';
64
+ return undefined;
65
+ }
66
+ function escapeRegExp(value) {
67
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
68
+ }
36
69
  /**
37
70
  * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
38
71
  */
@@ -9,7 +9,8 @@
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
+ import { skillSpanFields, skillTraceIdentity } from './skill-entry-span.js';
13
14
  import { filterLoadedToolsForSkill, } from '../../tool/system-prompt-builder.js';
14
15
  import { unwrapHitlPaused } from '../hitl/hitl-handler.js';
15
16
  import { pickContextFields } from '../../token/context-window.js';
@@ -60,9 +61,10 @@ export class SubGraphCompiler {
60
61
  taskId: state['taskId'],
61
62
  correlationId: state['correlationId'],
62
63
  });
63
- const traceCollector = state['traceCollector'];
64
- const traceId = resolveTraceId(state);
64
+ // 用父图这次调用上的 collector;内层 invoke 只补 collector/traceId,沿用外层流式袋。
65
+ const { collector: traceCollector, traceId } = resolveExecutionTrace(state);
65
66
  const skillName = dsl.name || resolvedSkillId;
67
+ const identity = skillTraceIdentity(state, resolvedSkillId);
66
68
  if (!resuming) {
67
69
  adapter.emitStreamEvent({
68
70
  type: 'skill.started',
@@ -71,22 +73,31 @@ export class SubGraphCompiler {
71
73
  timestamp: new Date().toISOString(),
72
74
  payload: {
73
75
  skillId: resolvedSkillId,
74
- skillName,
76
+ skillName: identity.skillName || skillName,
77
+ ...(identity.skillSpace ? { skillSpace: identity.skillSpace } : {}),
78
+ ...(identity.skillVersion ? { skillVersion: identity.skillVersion } : {}),
75
79
  },
76
80
  });
77
81
  }
78
82
  let skillSpanId = existingSpanId || null;
79
83
  if (!resuming && traceCollector && traceId) {
80
84
  try {
85
+ const fields = skillSpanFields(identity, { source: 'subgraph' });
81
86
  skillSpanId = await traceCollector.createNode({
82
87
  traceId,
83
88
  name: resolvedSkillId || dsl.name || 'skill',
84
89
  type: 'skill',
85
90
  agentCode: state['agentCode'] ?? state['agentId'],
86
- metadata: { skillId: resolvedSkillId, taskId: state['taskId'], correlationId: state['correlationId'] },
91
+ ...(fields.version ? { version: fields.version } : {}),
92
+ input: fields.input,
93
+ metadata: {
94
+ taskId: state['taskId'],
95
+ correlationId: state['correlationId'],
96
+ },
87
97
  });
88
98
  }
89
- catch {
99
+ catch (err) {
100
+ warnTraceSpanFailure('createNode', { type: 'skill', name: resolvedSkillId || dsl.name || 'skill', traceId }, err);
90
101
  skillSpanId = null;
91
102
  }
92
103
  }
@@ -145,7 +156,7 @@ export class SubGraphCompiler {
145
156
  hitlRespondedBy: state['hitlRespondedBy'],
146
157
  };
147
158
  const result = await adapter.runIsolatedLlmStreamFlags(() => adapter.invoke(subGraph, subGraphState));
148
- // RT-6: 记录成功
159
+ // 进程内调试记录;生产记录是上面的 skill Span
149
160
  await auditCtx?.finish({
150
161
  status: 'success',
151
162
  tokenUsage: result.tokenUsage,
@@ -160,8 +171,8 @@ export class SubGraphCompiler {
160
171
  totalTokens: result.tokenUsage?.total,
161
172
  });
162
173
  }
163
- catch {
164
- // ignore
174
+ catch (err) {
175
+ warnTraceSpanFailure('completeNode', { type: 'skill', name: skillName, traceId, spanId: skillSpanId }, err);
165
176
  }
166
177
  }
167
178
  // 4. 只回写子图本步新增消息(含内部 tool 回合),禁止把入口历史再 append
@@ -224,8 +235,8 @@ export class SubGraphCompiler {
224
235
  error: err instanceof Error ? err.message : String(err),
225
236
  });
226
237
  }
227
- catch {
228
- // ignore
238
+ catch (completeErr) {
239
+ warnTraceSpanFailure('completeNode', { type: 'skill', name: skillName, traceId, spanId: skillSpanId }, completeErr);
229
240
  }
230
241
  }
231
242
  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
  });
@@ -43,6 +43,12 @@ export interface ToolNodeProgress {
43
43
  node?: string;
44
44
  skillId?: string;
45
45
  skillDirs?: SkillDirRef[];
46
+ /** 已在 Skill 子图内(state.skillSpanId);读 SKILL.md 不再发 skill.started */
47
+ inSkillSubgraph?: boolean;
48
+ skillSpace?: 'agent' | 'user' | 'local';
49
+ skillVersion?: string;
50
+ skillName?: string;
51
+ toolVersion?: string;
46
52
  }
47
53
  export declare class ToolNode {
48
54
  private readonly orchestrator;
@@ -10,12 +10,13 @@
10
10
  * @see requirements.md Requirement 8 AC1, AC3, AC5
11
11
  */
12
12
  import { resolveJailFromState } from '../../tool/harness-primitives.js';
13
+ import { recordSkillEntrySpan, skillEntryInvocationFromTool, skillTraceIdentity } from '../adapters/skill-entry-span.js';
13
14
  import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
14
15
  import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
15
16
  import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
16
17
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
17
- import { resolveTraceId } from '../../common/trace-id.js';
18
- import { toolsetCodeFromLoaded } from '../../tool/mcp-list.js';
18
+ import { toolTraceIdentity } from '../../tool/mcp-list.js';
19
+ import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
19
20
  function skillDirsFromState(state) {
20
21
  const defs = state['skillDefinitions'];
21
22
  return Object.values(defs ?? {})
@@ -102,19 +103,21 @@ export class ToolNode {
102
103
  };
103
104
  });
104
105
  const executionContext = state['executionContext'];
105
- const traceCollector = state['traceCollector'];
106
- const traceId = resolveTraceId(state);
106
+ // 不读 state.traceCollector:存档会剥掉,后续节点会空。
107
+ const { collector: traceCollector, traceId } = resolveExecutionTrace(state);
107
108
  const agentCode = String(state['agentCode'] ?? '');
108
109
  const nodeId = this.nodeId ?? 'tool-node';
109
110
  const skillId = typeof state['activatedSkill'] === 'string' && state['activatedSkill']
110
111
  ? state['activatedSkill']
111
112
  : undefined;
113
+ const inSkillSubgraph = typeof state['skillSpanId'] === 'string' && Boolean(state['skillSpanId']);
112
114
  const emitProgress = (data) => {
113
115
  this.onProgress?.({
114
116
  ...data,
115
117
  node: nodeId,
116
118
  ...(skillId ? { skillId } : {}),
117
119
  ...(skillDirs.length > 0 ? { skillDirs } : {}),
120
+ ...(inSkillSubgraph ? { inSkillSubgraph: true } : {}),
118
121
  });
119
122
  };
120
123
  // 5. 并行执行所有 Tool 调用(附 Trace span)
@@ -125,6 +128,7 @@ export class ToolNode {
125
128
  const parentNodeId = typeof state['skillSpanId'] === 'string' && state['skillSpanId']
126
129
  ? state['skillSpanId']
127
130
  : undefined;
131
+ const identity = toolTraceIdentity(loadedTools?.[req.toolName], req.toolName);
128
132
  return await traceCollector.createNode({
129
133
  traceId,
130
134
  name: req.toolName,
@@ -132,6 +136,7 @@ export class ToolNode {
132
136
  parentNodeId,
133
137
  toolRuntime: req.toolRuntime === 'both' ? 'both' : req.toolRuntime,
134
138
  agentCode: agentCode || undefined,
139
+ ...(identity.toolVersion ? { version: identity.toolVersion } : {}),
135
140
  input: req.arguments,
136
141
  metadata: {
137
142
  toolCallId: req.toolCallId,
@@ -139,10 +144,12 @@ export class ToolNode {
139
144
  taskId: req.taskId,
140
145
  correlationId: req.correlationId,
141
146
  skillId,
147
+ ...(identity.toolset ? { toolset: identity.toolset } : {}),
142
148
  },
143
149
  });
144
150
  }
145
- catch {
151
+ catch (err) {
152
+ warnTraceSpanFailure('createNode', { type: 'tool', name: req.toolName, traceId, toolCallId: req.toolCallId }, err);
146
153
  return null;
147
154
  }
148
155
  }));
@@ -154,13 +161,15 @@ export class ToolNode {
154
161
  const needsHitl = Boolean(this.hitlHandler) &&
155
162
  requests.some((req) => confirmationTools.has(req.toolName));
156
163
  const runOne = async (req, i) => {
157
- const toolset = toolsetCodeFromLoaded(loadedTools?.[req.toolName]);
164
+ const toolIdentity = toolTraceIdentity(loadedTools?.[req.toolName], req.toolName);
165
+ const toolset = toolIdentity.toolset;
158
166
  const resumingHitl = isHitlDecision(state['hitlChoice']);
159
167
  if (!resumingHitl) {
160
168
  emitProgress({
161
169
  toolCallId: req.toolCallId,
162
170
  toolName: req.toolName,
163
171
  ...(toolset ? { toolset } : {}),
172
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
164
173
  status: 'started',
165
174
  arguments: req.arguments,
166
175
  });
@@ -197,14 +206,27 @@ export class ToolNode {
197
206
  }
198
207
  const executionTime = Date.now() - startedAt;
199
208
  if (result.success) {
209
+ const catalog = skillEntryInvocationFromTool({
210
+ toolName: req.toolName,
211
+ arguments: req.arguments,
212
+ skillDirs,
213
+ alreadyInSkillSpan: inSkillSubgraph,
214
+ });
215
+ const skillIdentity = catalog
216
+ ? skillTraceIdentity(state, catalog.skillId, catalog.path)
217
+ : undefined;
200
218
  emitProgress({
201
219
  toolCallId: result.toolCallId,
202
220
  toolName: result.toolName,
203
221
  ...(toolset ? { toolset } : {}),
222
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
204
223
  status: 'completed',
205
224
  arguments: req.arguments,
206
225
  content: result.content,
207
226
  executionTime,
227
+ ...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
228
+ ...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
229
+ ...(skillIdentity?.skillName ? { skillName: skillIdentity.skillName } : {}),
208
230
  });
209
231
  }
210
232
  else {
@@ -212,6 +234,7 @@ export class ToolNode {
212
234
  toolCallId: result.toolCallId,
213
235
  toolName: result.toolName,
214
236
  ...(toolset ? { toolset } : {}),
237
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
215
238
  status: 'error',
216
239
  arguments: req.arguments,
217
240
  error: result.error ?? 'unknown error',
@@ -227,16 +250,24 @@ export class ToolNode {
227
250
  error: result.error,
228
251
  });
229
252
  }
230
- catch {
231
- // Trace 失败不影响主路径
253
+ catch (err) {
254
+ warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: nodeId }, err);
232
255
  }
233
256
  }
257
+ if (result.success) {
258
+ await recordSkillEntrySpan(state, {
259
+ toolName: req.toolName,
260
+ arguments: req.arguments,
261
+ skillDirs,
262
+ });
263
+ }
234
264
  return result;
235
265
  }
236
266
  catch (err) {
237
267
  // HITL 暂停必须向上抛出,供 invoke/stream 返回 waiting_hitl(不可降级为 tool failure)
238
268
  const hitlPaused = unwrapHitlPaused(err);
239
269
  if (hitlPaused) {
270
+ // 暂停不 completeNode:span 保持 running,id 随错误交给 resume/timeout。
240
271
  hitlPaused.toolSpanId = hitlPaused.toolSpanId ?? spanIds[i] ?? undefined;
241
272
  hitlPaused.skillSpanId =
242
273
  hitlPaused.skillSpanId ??
@@ -254,6 +285,18 @@ export class ToolNode {
254
285
  error: message,
255
286
  executionTime,
256
287
  });
288
+ const failedSpanId = spanIds[i];
289
+ if (traceCollector && failedSpanId) {
290
+ try {
291
+ await traceCollector.completeNode(failedSpanId, {
292
+ status: 'failed',
293
+ error: message,
294
+ });
295
+ }
296
+ catch (completeErr) {
297
+ warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: failedSpanId }, completeErr);
298
+ }
299
+ }
257
300
  return {
258
301
  toolCallId: req.toolCallId,
259
302
  toolName: req.toolName,
@@ -1,8 +1,8 @@
1
1
  /**
2
- * SkillAuditRecorder — Skill 调用台账(RT-6)
2
+ * SkillAuditRecorder — 进程内 Skill 调用记录(调试用)
3
3
  *
4
- * 记录每次 Skill(子图)激活的审计明细,供后续分析与看板使用。
5
- * Runtime 负责写入;Gateway 看不到 Skill 调用。
4
+ * 生产记录是 Trace 上的 skill Span(子图激活,或成功读 SKILL.md)。
5
+ * 本记录器默认内存、不对外查询;分析从 Span 聚合,不读这里。
6
6
  *
7
7
  * @see docs/status/module-status-and-backlog.md RT-6
8
8
  */
@@ -48,7 +48,7 @@ export interface SkillInvocationRecord {
48
48
  /**
49
49
  * ISkillAuditStore — Skill 审计存储接口
50
50
  *
51
- * 默认实现为内存存储;生产环境应接入 PG 或外部审计服务。
51
+ * 默认内存存储。生产不依赖本接口;Skill 调用记录以 Trace skill Span 为准。
52
52
  */
53
53
  export interface ISkillAuditStore {
54
54
  /** 写入审计记录 */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * SkillAuditRecorder — Skill 调用台账(RT-6)
2
+ * SkillAuditRecorder — 进程内 Skill 调用记录(调试用)
3
3
  *
4
- * 记录每次 Skill(子图)激活的审计明细,供后续分析与看板使用。
5
- * Runtime 负责写入;Gateway 看不到 Skill 调用。
4
+ * 生产记录是 Trace 上的 skill Span(子图激活,或成功读 SKILL.md)。
5
+ * 本记录器默认内存、不对外查询;分析从 Span 聚合,不读这里。
6
6
  *
7
7
  * @see docs/status/module-status-and-backlog.md RT-6
8
8
  */