@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.
- package/dist/api/handlers/invoke.handler.js +10 -16
- package/dist/api/handlers/stream.handler.js +3 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/trace-collector.d.ts +16 -8
- package/dist/graph/adapters/langgraph.adapter.d.ts +1 -0
- package/dist/graph/adapters/langgraph.adapter.js +36 -1
- package/dist/graph/adapters/skill-entry-span.d.ts +37 -0
- package/dist/graph/adapters/skill-entry-span.js +78 -0
- package/dist/graph/adapters/skill-read.d.ts +10 -0
- package/dist/graph/adapters/skill-read.js +33 -0
- package/dist/graph/adapters/subgraph-compiler.js +13 -3
- package/dist/graph/nodes/tool-node.d.ts +6 -0
- package/dist/graph/nodes/tool-node.js +31 -2
- package/dist/graph/skill-audit-recorder.d.ts +4 -4
- package/dist/graph/skill-audit-recorder.js +3 -3
- package/dist/graph/trace-span.d.ts +2 -9
- package/dist/graph/trace-span.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/runtime/agent-runtime.js +22 -0
- package/dist/sse/trace-collector.d.ts +2 -9
- package/dist/token/index.d.ts +2 -0
- package/dist/token/index.js +1 -0
- package/dist/token/record-task-token-usage.d.ts +21 -0
- package/dist/token/record-task-token-usage.js +52 -0
- package/dist/token/token-stats.service.d.ts +2 -0
- package/dist/token/token-stats.service.js +4 -0
- package/dist/tool/index.d.ts +1 -1
- package/dist/tool/index.js +1 -1
- package/dist/tool/mcp-list.d.ts +12 -0
- package/dist/tool/mcp-list.js +15 -0
- 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
|
-
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
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
|
|
2
|
+
* SkillAuditRecorder — 进程内 Skill 调用记录(调试用)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
51
|
+
* 默认内存存储。生产不依赖本接口;Skill 调用记录以 Trace skill Span 为准。
|
|
52
52
|
*/
|
|
53
53
|
export interface ISkillAuditStore {
|
|
54
54
|
/** 写入审计记录 */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SkillAuditRecorder — Skill
|
|
2
|
+
* SkillAuditRecorder — 进程内 Skill 调用记录(调试用)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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'];
|
package/dist/graph/trace-span.js
CHANGED
|
@@ -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: {
|
package/dist/token/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/token/index.js
CHANGED
|
@@ -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
|
*
|
package/dist/tool/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/tool/index.js
CHANGED
|
@@ -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';
|
package/dist/tool/mcp-list.d.ts
CHANGED
|
@@ -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;
|
package/dist/tool/mcp-list.js
CHANGED
|
@@ -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。
|