@gt-fe/eap-runtime 0.0.19 → 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.
@@ -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 同序)───
@@ -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 正文。
@@ -60,6 +60,7 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
60
60
  * Tool 节点执行旁路:started → tool.called + tool.progress;
61
61
  * completed/error → 对应终态 + tool.progress。同一 toolCallId。
62
62
  * 读 SKILL.md / 技能包内文件:成功后再发 skill.loaded / skill.resource_read,同时保留 tool.*。
63
+ * 读 SKILL.md 另发 skill.started / skill.completed(与 Trace type=skill Span 对齐);子图内不重复。
63
64
  */
64
65
  private emitToolExecutionEvents;
65
66
  /**
@@ -30,6 +30,7 @@ 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';
34
35
  import { executionTraceFromState, getExecutionStream, runWithExecutionTrace, } from '../execution-trace-context.js';
35
36
  /** 图未写 `config.maxIterations` 时的 recursionLimit。`agent ⇄ tools` 一圈约 2,50 ≈ 24 次工具往返。 */
@@ -171,6 +172,7 @@ export class LangGraphAdapter {
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
178
  const stream = getExecutionStream();
@@ -185,16 +187,48 @@ export class LangGraphAdapter {
185
187
  const skillRead = classifySkillRead(progress.toolName, progress.arguments, progress.skillDirs);
186
188
  if (skillRead && progress.status === 'completed') {
187
189
  if (skillRead.kind === 'loaded') {
190
+ const skillId = skillRead.skillId || loc.skillId || skillRead.path;
188
191
  stream.sink({
189
192
  type: 'skill.loaded',
190
193
  taskId,
191
194
  correlationId,
192
195
  timestamp,
193
196
  payload: {
194
- skillId: skillRead.skillId || loc.skillId || skillRead.path,
197
+ skillId,
195
198
  path: skillRead.path,
196
199
  },
197
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
+ }
198
232
  }
199
233
  else {
200
234
  stream.sink({
@@ -216,6 +250,7 @@ export class LangGraphAdapter {
216
250
  toolCallId: progress.toolCallId,
217
251
  toolName: progress.toolName,
218
252
  ...(progress.toolset ? { toolset: progress.toolset } : {}),
253
+ ...(progress.toolVersion ? { toolVersion: progress.toolVersion } : {}),
219
254
  arguments: progress.arguments ?? {},
220
255
  ...loc,
221
256
  },
@@ -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>;
@@ -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
  */
@@ -10,6 +10,7 @@
10
10
  import { assertSkillPermission } from '../../governance/permission-guard.js';
11
11
  import { LangGraphAdapter } from './langgraph.adapter.js';
12
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';
@@ -63,6 +64,7 @@ export class SubGraphCompiler {
63
64
  // 用父图这次调用上的 collector;内层 invoke 只补 collector/traceId,沿用外层流式袋。
64
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,19 +73,27 @@ 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
99
  catch (err) {
@@ -146,7 +156,7 @@ export class SubGraphCompiler {
146
156
  hitlRespondedBy: state['hitlRespondedBy'],
147
157
  };
148
158
  const result = await adapter.runIsolatedLlmStreamFlags(() => adapter.invoke(subGraph, subGraphState));
149
- // RT-6: 记录成功
159
+ // 进程内调试记录;生产记录是上面的 skill Span
150
160
  await auditCtx?.finish({
151
161
  status: 'success',
152
162
  tokenUsage: result.tokenUsage,
@@ -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,11 +10,12 @@
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 { toolsetCodeFromLoaded } from '../../tool/mcp-list.js';
18
+ import { toolTraceIdentity } from '../../tool/mcp-list.js';
18
19
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
19
20
  function skillDirsFromState(state) {
20
21
  const defs = state['skillDefinitions'];
@@ -109,12 +110,14 @@ export class ToolNode {
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,6 +144,7 @@ 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
  }
@@ -155,13 +161,15 @@ export class ToolNode {
155
161
  const needsHitl = Boolean(this.hitlHandler) &&
156
162
  requests.some((req) => confirmationTools.has(req.toolName));
157
163
  const runOne = async (req, i) => {
158
- const toolset = toolsetCodeFromLoaded(loadedTools?.[req.toolName]);
164
+ const toolIdentity = toolTraceIdentity(loadedTools?.[req.toolName], req.toolName);
165
+ const toolset = toolIdentity.toolset;
159
166
  const resumingHitl = isHitlDecision(state['hitlChoice']);
160
167
  if (!resumingHitl) {
161
168
  emitProgress({
162
169
  toolCallId: req.toolCallId,
163
170
  toolName: req.toolName,
164
171
  ...(toolset ? { toolset } : {}),
172
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
165
173
  status: 'started',
166
174
  arguments: req.arguments,
167
175
  });
@@ -198,14 +206,27 @@ export class ToolNode {
198
206
  }
199
207
  const executionTime = Date.now() - startedAt;
200
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;
201
218
  emitProgress({
202
219
  toolCallId: result.toolCallId,
203
220
  toolName: result.toolName,
204
221
  ...(toolset ? { toolset } : {}),
222
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
205
223
  status: 'completed',
206
224
  arguments: req.arguments,
207
225
  content: result.content,
208
226
  executionTime,
227
+ ...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
228
+ ...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
229
+ ...(skillIdentity?.skillName ? { skillName: skillIdentity.skillName } : {}),
209
230
  });
210
231
  }
211
232
  else {
@@ -213,6 +234,7 @@ export class ToolNode {
213
234
  toolCallId: result.toolCallId,
214
235
  toolName: result.toolName,
215
236
  ...(toolset ? { toolset } : {}),
237
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
216
238
  status: 'error',
217
239
  arguments: req.arguments,
218
240
  error: result.error ?? 'unknown error',
@@ -232,6 +254,13 @@ export class ToolNode {
232
254
  warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: nodeId }, err);
233
255
  }
234
256
  }
257
+ if (result.success) {
258
+ await recordSkillEntrySpan(state, {
259
+ toolName: req.toolName,
260
+ arguments: req.arguments,
261
+ skillDirs,
262
+ });
263
+ }
235
264
  return result;
236
265
  }
237
266
  catch (err) {
@@ -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
  */
@@ -1,21 +1,14 @@
1
1
  /**
2
2
  * Graph 节点 Span 辅助:createNode / completeNode 失败不挡主路径。
3
3
  */
4
- import type { ITraceCollector, ExecutionNodeEvent } from '../contracts/trace-collector.js';
4
+ import type { ITraceCollector, ExecutionNodeEvent, CompleteNodeParams } from '../contracts/trace-collector.js';
5
5
  export declare function startGraphSpan(state: Record<string, unknown>, input: {
6
6
  name: string;
7
7
  type: ExecutionNodeEvent['type'];
8
8
  parentSpanId?: string;
9
9
  extra?: Partial<Omit<ExecutionNodeEvent, 'traceId' | 'name' | 'type' | 'parentNodeId'>>;
10
10
  }): Promise<string | undefined>;
11
- export declare function completeGraphSpan(state: Record<string, unknown>, spanId: string | undefined, params: {
12
- status: 'completed' | 'failed';
13
- output?: unknown;
14
- error?: string;
15
- inputTokens?: number;
16
- outputTokens?: number;
17
- totalTokens?: number;
18
- }, collectorOverride?: ITraceCollector): Promise<void>;
11
+ export declare function completeGraphSpan(state: Record<string, unknown>, spanId: string | undefined, params: CompleteNodeParams, collectorOverride?: ITraceCollector): Promise<void>;
19
12
  export declare function withGraphSpan(state: Record<string, unknown>, spec: {
20
13
  name: string;
21
14
  type: ExecutionNodeEvent['type'];
@@ -57,6 +57,7 @@ export async function withGraphSpan(state, spec, run) {
57
57
  totalTokens: usage?.inputTokens != null || usage?.outputTokens != null
58
58
  ? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)
59
59
  : undefined,
60
+ ...(typeof usage?.model === 'string' && usage.model ? { model: usage.model } : {}),
60
61
  });
61
62
  return spanId ? { ...result, lastSpanId: spanId } : result;
62
63
  }
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export * from './memory/index.js';
15
15
  export * from './common/index.js';
16
16
  export * from './registry/index.js';
17
17
  export * from './providers/index.js';
18
- export type { ITraceCollector, TraceEvent, TraceSession, ExecutionFlowEvent, ExecutionNodeEvent, TraceLinkEvent, TraceLinkNodeEvent, } from './contracts/trace-collector.js';
18
+ export type { ITraceCollector, CompleteNodeParams, TraceEvent, TraceSession, ExecutionFlowEvent, ExecutionNodeEvent, TraceLinkEvent, TraceLinkNodeEvent, } from './contracts/trace-collector.js';
19
19
  export type { ExecutionContext, CallerType, GovernancePermissionCheckParams, GovernancePermissionCheckResult, PermissionDenyReason, } from '@gt-fe/eap-contracts';
20
20
  export { HttpGovernanceClient, AllowAllGovernanceClient, resolveExecutionContext, assertToolPermission, assertSkillPermission, permissionMessage, PermissionDeniedError, GovernanceUnavailableError, } from './governance/index.js';
21
21
  export type { IGovernanceClient } from './governance/index.js';
@@ -17,6 +17,7 @@ import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
17
17
  import { resolveExecutionContext } from '../governance/governance-client.js';
18
18
  import { resolveAgentCode } from '../common/agent-code.js';
19
19
  import { resolveTraceId } from '../common/trace-id.js';
20
+ import { collectStreamNodeTokenUsage, recordTaskTokenUsages, } from '../token/record-task-token-usage.js';
20
21
  import { resolveSkillInputFromInvokeInput } from '../registry/skill-io-guard.js';
21
22
  const HITL_TERMINAL_STATUSES = [
22
23
  'success',
@@ -286,6 +287,7 @@ export class AgentRuntime {
286
287
  let assistantReasoningContent;
287
288
  const pendingToolCalls = [];
288
289
  const toolResults = [];
290
+ const nodeTokenUsages = [];
289
291
  try {
290
292
  for await (const runtimeEvent of this.deps.graphEngine.stream(graph, {
291
293
  messages,
@@ -345,8 +347,10 @@ export class AgentRuntime {
345
347
  assistantText += delta;
346
348
  }
347
349
  }
350
+ collectStreamNodeTokenUsage(runtimeEvent, nodeTokenUsages);
348
351
  yield this.runtimeEventToSSE(runtimeEvent, correlationId, task.id);
349
352
  }
353
+ await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
350
354
  await this.deps.taskService.updateStatus(task.id, 'success');
351
355
  await persistTurnTranscript(this.deps.messageService, {
352
356
  sessionId,
@@ -368,6 +372,7 @@ export class AgentRuntime {
368
372
  catch (err) {
369
373
  const hitlPaused = unwrapHitlPaused(err);
370
374
  if (hitlPaused) {
375
+ await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
371
376
  await this.deps.taskService.updateStatus(task.id, 'waiting_hitl', buildHitlWaitingTaskOutput({
372
377
  threadId: hitlPaused.threadId,
373
378
  question: hitlPaused.question,
@@ -516,6 +521,7 @@ export class AgentRuntime {
516
521
  let assistantReasoningContent;
517
522
  const pendingToolCalls = [];
518
523
  const toolResults = [];
524
+ const nodeTokenUsages = [];
519
525
  try {
520
526
  for await (const runtimeEvent of this.deps.graphEngine.stream(graph, continueState)) {
521
527
  if (runtimeEvent.type === 'tool.called') {
@@ -555,8 +561,10 @@ export class AgentRuntime {
555
561
  assistantText += delta;
556
562
  }
557
563
  }
564
+ collectStreamNodeTokenUsage(runtimeEvent, nodeTokenUsages);
558
565
  yield this.runtimeEventToSSE(runtimeEvent, correlationId, taskId);
559
566
  }
567
+ await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
560
568
  await this.deps.taskService.updateStatus(taskId, 'success', assistantText || undefined);
561
569
  if (sessionId) {
562
570
  await persistTurnTranscript(this.deps.messageService, {
@@ -580,6 +588,7 @@ export class AgentRuntime {
580
588
  catch (err) {
581
589
  const hitlPaused = unwrapHitlPaused(err);
582
590
  if (hitlPaused) {
591
+ await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
583
592
  await this.deps.taskService.updateStatus(taskId, 'waiting_hitl', buildHitlWaitingTaskOutput({
584
593
  threadId: hitlPaused.threadId,
585
594
  question: hitlPaused.question,
@@ -676,6 +685,19 @@ export class AgentRuntime {
676
685
  traceId,
677
686
  traceCollector: this.deps.traceCollector,
678
687
  });
688
+ if (result.tokenUsage.perNode && result.tokenUsage.perNode.length > 0) {
689
+ await recordTaskTokenUsages(this.deps.taskService, task.id, result.tokenUsage.perNode);
690
+ }
691
+ else if (result.tokenUsage.total > 0) {
692
+ await recordTaskTokenUsages(this.deps.taskService, task.id, [
693
+ {
694
+ nodeId: 'graph',
695
+ model: 'unknown',
696
+ inputTokens: result.tokenUsage.input,
697
+ outputTokens: result.tokenUsage.output,
698
+ },
699
+ ]);
700
+ }
679
701
  await this.deps.taskService.updateStatus(task.id, 'success', result.output);
680
702
  if (this.deps.recordForkRelation) {
681
703
  try {
@@ -6,7 +6,7 @@
6
6
  * @see contracts/trace-collector.ts
7
7
  * @see sse/sse-emitter.ts
8
8
  */
9
- import type { ExecutionFlowEvent, ExecutionNodeEvent, ITraceCollector, TraceEvent, TraceLinkEvent, TraceLinkNodeEvent, TraceSession } from '../contracts/trace-collector.js';
9
+ import type { CompleteNodeParams, ExecutionFlowEvent, ExecutionNodeEvent, ITraceCollector, TraceEvent, TraceLinkEvent, TraceLinkNodeEvent, TraceSession } from '../contracts/trace-collector.js';
10
10
  /**
11
11
  * InMemoryTraceCollector — 内存 Trace 采集器(测试用)
12
12
  */
@@ -18,14 +18,7 @@ export declare class InMemoryTraceCollector implements ITraceCollector {
18
18
  getSession(sessionId: string): Promise<TraceSession | null>;
19
19
  createFlow(_event: ExecutionFlowEvent): Promise<string>;
20
20
  createNode(_event: ExecutionNodeEvent): Promise<string>;
21
- completeNode(_nodeId: string, _params: {
22
- status: 'completed' | 'failed';
23
- output?: unknown;
24
- error?: string;
25
- inputTokens?: number;
26
- outputTokens?: number;
27
- totalTokens?: number;
28
- }): Promise<void>;
21
+ completeNode(_nodeId: string, _params: CompleteNodeParams): Promise<void>;
29
22
  createLink(_event: TraceLinkEvent): Promise<string>;
30
23
  createLinkNode(_event: TraceLinkNodeEvent): Promise<string>;
31
24
  completeLinkNode(_nodeId: string, _params: {
@@ -6,6 +6,8 @@ export { TokenBudgetGuard, TokenBudgetExhaustedError, } from './token-budget-gua
6
6
  export { SessionBudgetChecker, SessionBudgetExhaustedError, } from './session-budget-checker.js';
7
7
  export type { NodeTokenUsage, TokenBreakdown, TaskTokenSummary, } from './token-stats.service.js';
8
8
  export { TokenStatsService } from './token-stats.service.js';
9
+ export { recordTaskTokenUsages, tokenUsageFromNodePayload, collectStreamNodeTokenUsage, } from './record-task-token-usage.js';
10
+ export type { TaskTokenUsageWriter } from './record-task-token-usage.js';
9
11
  export type { TokenDetailMode } from './token-detail-mode.js';
10
12
  export { contextWindowUsage, buildContextUsage, splitContextUsage, turnSectionChars, pickContextFields, type ContextWindowUsage, type ContextUsageParts, } from './context-window.js';
11
13
  export { TokenStatsEmitter } from './token-detail-mode.js';
@@ -3,6 +3,7 @@ export { PostgresTokenBudgetRepository } from './pg-token-budget.repository.js';
3
3
  export { TokenBudgetGuard, TokenBudgetExhaustedError, } from './token-budget-guard.js';
4
4
  export { SessionBudgetChecker, SessionBudgetExhaustedError, } from './session-budget-checker.js';
5
5
  export { TokenStatsService } from './token-stats.service.js';
6
+ export { recordTaskTokenUsages, tokenUsageFromNodePayload, collectStreamNodeTokenUsage, } from './record-task-token-usage.js';
6
7
  export { contextWindowUsage, buildContextUsage, splitContextUsage, turnSectionChars, pickContextFields, } from './context-window.js';
7
8
  export { TokenStatsEmitter } from './token-detail-mode.js';
8
9
  // 上下文窗口预算分配
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Task.token_usage 写入:invoke / stream / HITL resume / fork 同一收口。
3
+ *
4
+ * 假设:一次 LLM 调用产生一条 TokenUsageRecord(nodeId + model + in/out)。
5
+ * 来源可以是 Graph.perNode,也可以是 SSE stats.node_token_usage,字段同一套。
6
+ * 写入失败只 warn,不挡 Task 终态 / HITL(与 Span 采集同一策略)。
7
+ */
8
+ import type { RuntimeEvent } from '../graph/contracts/executable-graph.js';
9
+ import type { TokenUsageRecord } from '../task/contracts/task-manager.contract.js';
10
+ export interface TaskTokenUsageWriter {
11
+ recordTokenUsage(taskId: string, usage: TokenUsageRecord): Promise<void>;
12
+ }
13
+ export declare function tokenUsageFromNodePayload(payload: unknown): TokenUsageRecord | undefined;
14
+ export declare function collectStreamNodeTokenUsage(event: Pick<RuntimeEvent, 'type' | 'payload'>, bucket: TokenUsageRecord[]): void;
15
+ export declare function recordTaskTokenUsages(taskService: TaskTokenUsageWriter, taskId: string, usages: Array<{
16
+ nodeId: string;
17
+ model: string;
18
+ inputTokens: number;
19
+ outputTokens: number;
20
+ costEstimate?: number;
21
+ }>): Promise<void>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Task.token_usage 写入:invoke / stream / HITL resume / fork 同一收口。
3
+ *
4
+ * 假设:一次 LLM 调用产生一条 TokenUsageRecord(nodeId + model + in/out)。
5
+ * 来源可以是 Graph.perNode,也可以是 SSE stats.node_token_usage,字段同一套。
6
+ * 写入失败只 warn,不挡 Task 终态 / HITL(与 Span 采集同一策略)。
7
+ */
8
+ import { emitRuntimeLog } from '../common/logger.js';
9
+ export function tokenUsageFromNodePayload(payload) {
10
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
11
+ return undefined;
12
+ const record = payload;
13
+ const inputTokens = Number(record['inputTokens'] ?? 0);
14
+ const outputTokens = Number(record['outputTokens'] ?? 0);
15
+ if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens))
16
+ return undefined;
17
+ return {
18
+ nodeId: typeof record['nodeId'] === 'string' && record['nodeId'] ? record['nodeId'] : 'unknown',
19
+ model: typeof record['model'] === 'string' && record['model'] ? record['model'] : 'unknown',
20
+ inputTokens,
21
+ outputTokens,
22
+ costEstimate: Number(record['costEstimate'] ?? 0) || 0,
23
+ };
24
+ }
25
+ export function collectStreamNodeTokenUsage(event, bucket) {
26
+ if (event.type !== 'stats.node_token_usage')
27
+ return;
28
+ const usage = tokenUsageFromNodePayload(event.payload);
29
+ if (usage)
30
+ bucket.push(usage);
31
+ }
32
+ export async function recordTaskTokenUsages(taskService, taskId, usages) {
33
+ for (const usage of usages) {
34
+ try {
35
+ await taskService.recordTokenUsage(taskId, {
36
+ nodeId: usage.nodeId,
37
+ model: usage.model,
38
+ inputTokens: usage.inputTokens,
39
+ outputTokens: usage.outputTokens,
40
+ costEstimate: usage.costEstimate ?? 0,
41
+ });
42
+ }
43
+ catch (err) {
44
+ emitRuntimeLog('warn', 'runtime.token', 'task token usage persist failed', {
45
+ event: 'runtime.token.persist_failed',
46
+ taskId,
47
+ nodeId: usage.nodeId,
48
+ error: err instanceof Error ? err.message : String(err),
49
+ });
50
+ }
51
+ }
52
+ }
@@ -67,6 +67,8 @@ export declare class TokenStatsService {
67
67
  * @param usage - 本次调用的 Token 消耗数据
68
68
  */
69
69
  record(taskId: string, usage: NodeTokenUsage): void;
70
+ /** 按记录顺序返回本 Task 的每次 LLM 用量(写入 Task.token_usage 用) */
71
+ list(taskId: string): NodeTokenUsage[];
70
72
  /**
71
73
  * 获取 Task 级聚合统计
72
74
  *
@@ -35,6 +35,10 @@ export class TokenStatsService {
35
35
  this.usageStore.set(taskId, [usage]);
36
36
  }
37
37
  }
38
+ /** 按记录顺序返回本 Task 的每次 LLM 用量(写入 Task.token_usage 用) */
39
+ list(taskId) {
40
+ return [...(this.usageStore.get(taskId) ?? [])];
41
+ }
38
42
  /**
39
43
  * 获取 Task 级聚合统计
40
44
  *
@@ -1,5 +1,5 @@
1
1
  export type { AuthContext, ToolCallRequest, ToolCallResult, LLMToolCall, LLMToolCallResponse, IToolExecutor, McpListedTool, } from './tool-executor.interface.js';
2
- export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, } from './mcp-list.js';
2
+ export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
3
3
  export { toLoadedToolMap, type LoadedToolDefinition, type LoadedToolMap, type LoadedToolManifestRef, } from './loaded-tool-definition.js';
4
4
  export { ToolOrchestrator } from './tool-orchestrator.js';
5
5
  export { HttpToolExecutor, type HttpToolExecutorOptions, } from './http-tool-executor.js';
@@ -1,4 +1,4 @@
1
- export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, } from './mcp-list.js';
1
+ export { hydrateMcpTools, isMcpDiscoverStub, needsMcpHydration, parseListedTools, toolsetCodeFromLoaded, toolTraceIdentity, } from './mcp-list.js';
2
2
  export { toLoadedToolMap, } from './loaded-tool-definition.js';
3
3
  export { ToolOrchestrator } from './tool-orchestrator.js';
4
4
  export { HttpToolExecutor, } from './http-tool-executor.js';
@@ -32,6 +32,18 @@ export declare function toolsetCodeFromLoaded(loaded: {
32
32
  id?: string;
33
33
  };
34
34
  } | undefined): string | undefined;
35
+ /** Tool Span:工具集 code + 声明版本。与 Skill 的 skillSpace/version 同一假设。 */
36
+ export declare function toolTraceIdentity(loaded: {
37
+ toolId?: string;
38
+ name?: string;
39
+ manifest?: {
40
+ id?: string;
41
+ version?: string;
42
+ };
43
+ } | undefined, toolName: string): {
44
+ toolset?: string;
45
+ toolVersion?: string;
46
+ };
35
47
  export interface HydrateMcpToolsAuth {
36
48
  authContext: AuthContext;
37
49
  correlationId?: string;
@@ -84,6 +84,21 @@ export function toolsetCodeFromLoaded(loaded) {
84
84
  }
85
85
  return undefined;
86
86
  }
87
+ function declaredToolVersion(toolName, version) {
88
+ const raw = version?.trim();
89
+ if (!raw || raw === toolName)
90
+ return undefined;
91
+ return raw;
92
+ }
93
+ /** Tool Span:工具集 code + 声明版本。与 Skill 的 skillSpace/version 同一假设。 */
94
+ export function toolTraceIdentity(loaded, toolName) {
95
+ const toolset = toolsetCodeFromLoaded(loaded);
96
+ const toolVersion = declaredToolVersion(toolName, loaded?.manifest?.version);
97
+ return {
98
+ ...(toolset ? { toolset } : {}),
99
+ ...(toolVersion ? { toolVersion } : {}),
100
+ };
101
+ }
87
102
  /**
88
103
  * 将 MCP discover stub 替换为 tools/list 结果。
89
104
  * 无 stub 时原样返回(同一引用)。按 endpoint+user+agent 在执行器内缓存 list。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gt-fe/eap-runtime",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",