@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.
- package/dist/api/handlers/invoke.handler.js +10 -16
- package/dist/api/handlers/stream.handler.js +3 -0
- package/dist/checkpoint/checkpoint-ref-notify.js +5 -4
- 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 +5 -12
- package/dist/graph/adapters/langgraph.adapter.js +109 -64
- 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 +22 -11
- package/dist/graph/contracts/executable-graph.d.ts +4 -1
- package/dist/graph/execution-trace-context.d.ts +38 -0
- package/dist/graph/execution-trace-context.js +62 -0
- package/dist/graph/hitl/hitl-handler.d.ts +2 -1
- package/dist/graph/hitl/hitl-handler.js +24 -6
- package/dist/graph/nodes/tool-node.d.ts +6 -0
- package/dist/graph/nodes/tool-node.js +51 -8
- 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 +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/runtime/agent-runtime.js +22 -0
- package/dist/runtime/types.d.ts +1 -1
- 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
|
@@ -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 {
|
|
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
|
-
|
|
64
|
-
const traceId =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
18
|
-
import {
|
|
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
|
-
|
|
106
|
-
const traceId =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
*/
|