@gt-fe/eap-runtime 0.0.31 → 0.0.32
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/graph/adapters/langgraph.adapter.js +27 -7
- package/dist/graph/adapters/skill-entry-span.d.ts +4 -2
- package/dist/graph/adapters/skill-entry-span.js +10 -3
- package/dist/graph/adapters/skill-read.d.ts +18 -2
- package/dist/graph/adapters/skill-read.js +53 -27
- package/dist/graph/nodes/tool-node.d.ts +2 -0
- package/dist/graph/nodes/tool-node.js +3 -2
- package/dist/tool/harness-primitives.d.ts +1 -7
- package/dist/tool/harness-primitives.js +4 -70
- package/dist/tool/skill-relative-read.d.ts +14 -0
- package/dist/tool/skill-relative-read.js +98 -0
- package/dist/tool/tool-executor.interface.d.ts +5 -0
- package/package.json +1 -1
|
@@ -34,8 +34,8 @@ import { preloadSkillNotFound } from '../../registry/preload-errors.js';
|
|
|
34
34
|
import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
|
|
35
35
|
import { pickContextFields } from '../../token/context-window.js';
|
|
36
36
|
import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
|
|
37
|
-
import {
|
|
38
|
-
import { skillEntryInvocationFromTool } from './skill-entry-span.js';
|
|
37
|
+
import { classifySkillFile, isReadTool } from './skill-read.js';
|
|
38
|
+
import { recordSkillEntrySpan, skillEntryInvocationFromTool } from './skill-entry-span.js';
|
|
39
39
|
import { llmSpanInput, llmSpanOutputFromResult, withGraphSpan } from '../trace-span.js';
|
|
40
40
|
import { executionTraceFromState, getExecutionStream, getExecutionTrace, runWithExecutionTrace, } from '../execution-trace-context.js';
|
|
41
41
|
import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
|
|
@@ -283,8 +283,10 @@ export class LangGraphAdapter {
|
|
|
283
283
|
...(progress.node ? { node: progress.node } : {}),
|
|
284
284
|
...(progress.skillId ? { skillId: progress.skillId } : {}),
|
|
285
285
|
};
|
|
286
|
-
const skillRead =
|
|
287
|
-
|
|
286
|
+
const skillRead = progress.status === 'completed' && progress.resolvedPath && isReadTool(progress.toolName)
|
|
287
|
+
? classifySkillFile(progress.resolvedPath, progress.skillDirs)
|
|
288
|
+
: null;
|
|
289
|
+
if (skillRead) {
|
|
288
290
|
if (skillRead.kind === 'loaded') {
|
|
289
291
|
const skillId = skillRead.skillId || loc.skillId || skillRead.path;
|
|
290
292
|
stream.sink({
|
|
@@ -299,7 +301,7 @@ export class LangGraphAdapter {
|
|
|
299
301
|
});
|
|
300
302
|
const catalogSkill = skillEntryInvocationFromTool({
|
|
301
303
|
toolName: progress.toolName,
|
|
302
|
-
|
|
304
|
+
resolvedPath: progress.resolvedPath,
|
|
303
305
|
skillDirs: progress.skillDirs,
|
|
304
306
|
alreadyInSkillSpan: progress.inSkillSubgraph,
|
|
305
307
|
});
|
|
@@ -355,12 +357,14 @@ export class LangGraphAdapter {
|
|
|
355
357
|
},
|
|
356
358
|
});
|
|
357
359
|
}
|
|
360
|
+
// resolvedPath / skillDirs 只给上面 classifySkillFile;inSkillSubgraph 只给 skill.started 去重。不进对话流。
|
|
361
|
+
const { resolvedPath: _resolvedPath, skillDirs: _skillDirs, inSkillSubgraph: _inSkillSubgraph, ...progressView } = progress;
|
|
358
362
|
stream.sink({
|
|
359
363
|
type: 'tool.progress',
|
|
360
364
|
taskId,
|
|
361
365
|
correlationId,
|
|
362
366
|
timestamp,
|
|
363
|
-
payload:
|
|
367
|
+
payload: progressView,
|
|
364
368
|
});
|
|
365
369
|
if (progress.status === 'completed') {
|
|
366
370
|
stream.sink({
|
|
@@ -936,6 +940,13 @@ export class LangGraphAdapter {
|
|
|
936
940
|
node: node.id,
|
|
937
941
|
});
|
|
938
942
|
const skillDefs = state['skillDefinitions'];
|
|
943
|
+
const jail = resolveJailFromState({
|
|
944
|
+
agentDir: state['agentDir'],
|
|
945
|
+
userId: state['userId'],
|
|
946
|
+
executionContext: state['executionContext'],
|
|
947
|
+
loadedTools: state['loadedTools'],
|
|
948
|
+
skillDefinitions: skillDefs,
|
|
949
|
+
});
|
|
939
950
|
const jailExtraRoots = Object.values(skillDefs ?? {})
|
|
940
951
|
.map((skill) => skill.bundlePath)
|
|
941
952
|
.filter((p) => Boolean(p));
|
|
@@ -946,6 +957,7 @@ export class LangGraphAdapter {
|
|
|
946
957
|
authContext: { userId, delegationToken, agentCode },
|
|
947
958
|
correlationId,
|
|
948
959
|
taskId,
|
|
960
|
+
...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
|
|
949
961
|
...(jailExtraRoots.length > 0 ? { jailExtraRoots } : {}),
|
|
950
962
|
};
|
|
951
963
|
const retry = resolveToolRetryConfig(node.retry);
|
|
@@ -981,13 +993,21 @@ export class LangGraphAdapter {
|
|
|
981
993
|
executionTime,
|
|
982
994
|
node: node.id,
|
|
983
995
|
...(skillDirs.length > 0 ? { skillDirs } : {}),
|
|
996
|
+
...(result.resolvedPath ? { resolvedPath: result.resolvedPath } : {}),
|
|
984
997
|
});
|
|
998
|
+
if (result.success) {
|
|
999
|
+
await recordSkillEntrySpan(state, {
|
|
1000
|
+
toolName: result.toolName,
|
|
1001
|
+
resolvedPath: result.resolvedPath,
|
|
1002
|
+
skillDirs,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
985
1005
|
// 重要:messages channel 的 reducer 是 append 语义,只返回新增消息
|
|
986
1006
|
return {
|
|
987
1007
|
messages: [
|
|
988
1008
|
{
|
|
989
1009
|
role: 'tool',
|
|
990
|
-
content: result.content
|
|
1010
|
+
content: result.content ?? '',
|
|
991
1011
|
toolCallId,
|
|
992
1012
|
toolName,
|
|
993
1013
|
success: result.success ?? true,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 成功读 SKILL.md 时写 Trace `type=skill` Span。
|
|
3
3
|
*
|
|
4
4
|
* 假设:Skill 被调用 = 一次成功读取技能包入口(SKILL.md),
|
|
5
|
+
* 入口是执行器返回的实际文件(resolvedPath)对已装配 skillDirs。
|
|
6
|
+
* 没有实际路径不记,不再用 arguments.path 猜。
|
|
5
7
|
* 与子图激活走同一 collector.createNode({ type: 'skill' })。
|
|
6
8
|
* 声明版本在 createNode.version;skillSpace 只在 input。
|
|
7
9
|
* 子图已有 skillSpanId 时不记第二条。write_file / 包内附属读 / 失败读不是调用。
|
|
@@ -23,7 +25,7 @@ export declare function skillSpanFields(identity: SkillTraceIdentity, extra?: {
|
|
|
23
25
|
};
|
|
24
26
|
export declare function skillEntryInvocationFromTool(input: {
|
|
25
27
|
toolName: string;
|
|
26
|
-
|
|
28
|
+
resolvedPath?: string;
|
|
27
29
|
skillDirs?: SkillDirRef[];
|
|
28
30
|
alreadyInSkillSpan?: boolean;
|
|
29
31
|
}): {
|
|
@@ -32,6 +34,6 @@ export declare function skillEntryInvocationFromTool(input: {
|
|
|
32
34
|
} | null;
|
|
33
35
|
export declare function recordSkillEntrySpan(state: Record<string, unknown>, input: {
|
|
34
36
|
toolName: string;
|
|
35
|
-
|
|
37
|
+
resolvedPath?: string;
|
|
36
38
|
skillDirs?: SkillDirRef[];
|
|
37
39
|
}): Promise<void>;
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* 成功读 SKILL.md 时写 Trace `type=skill` Span。
|
|
3
3
|
*
|
|
4
4
|
* 假设:Skill 被调用 = 一次成功读取技能包入口(SKILL.md),
|
|
5
|
+
* 入口是执行器返回的实际文件(resolvedPath)对已装配 skillDirs。
|
|
6
|
+
* 没有实际路径不记,不再用 arguments.path 猜。
|
|
5
7
|
* 与子图激活走同一 collector.createNode({ type: 'skill' })。
|
|
6
8
|
* 声明版本在 createNode.version;skillSpace 只在 input。
|
|
7
9
|
* 子图已有 skillSpanId 时不记第二条。write_file / 包内附属读 / 失败读不是调用。
|
|
8
10
|
*/
|
|
9
|
-
import {
|
|
11
|
+
import { classifySkillFile, classifySkillSpace, isReadTool, } from './skill-read.js';
|
|
10
12
|
import { completeGraphSpan, startGraphSpan } from '../trace-span.js';
|
|
11
13
|
function declaredSkillVersion(skillId, version) {
|
|
12
14
|
const raw = version?.trim();
|
|
@@ -46,7 +48,12 @@ export function skillSpanFields(identity, extra) {
|
|
|
46
48
|
export function skillEntryInvocationFromTool(input) {
|
|
47
49
|
if (input.alreadyInSkillSpan)
|
|
48
50
|
return null;
|
|
49
|
-
|
|
51
|
+
if (!isReadTool(input.toolName))
|
|
52
|
+
return null;
|
|
53
|
+
const resolved = input.resolvedPath?.trim();
|
|
54
|
+
if (!resolved)
|
|
55
|
+
return null;
|
|
56
|
+
const hit = classifySkillFile(resolved, input.skillDirs);
|
|
50
57
|
if (!hit || hit.kind !== 'loaded' || !hit.skillId)
|
|
51
58
|
return null;
|
|
52
59
|
return { skillId: hit.skillId, path: hit.path };
|
|
@@ -55,7 +62,7 @@ export async function recordSkillEntrySpan(state, input) {
|
|
|
55
62
|
const alreadyInSkillSpan = typeof state['skillSpanId'] === 'string' && Boolean(state['skillSpanId']);
|
|
56
63
|
const hit = skillEntryInvocationFromTool({
|
|
57
64
|
toolName: input.toolName,
|
|
58
|
-
|
|
65
|
+
resolvedPath: input.resolvedPath,
|
|
59
66
|
skillDirs: input.skillDirs,
|
|
60
67
|
alreadyInSkillSpan,
|
|
61
68
|
});
|
|
@@ -12,10 +12,26 @@ export declare function classifySkillSpace(bundleOrFilePath: string, opts?: {
|
|
|
12
12
|
agentDir?: string;
|
|
13
13
|
userId?: string;
|
|
14
14
|
}): SkillSpace | undefined;
|
|
15
|
+
/** 与 Harness `read_file` 相同的相对路径定位(workdir / skillDir / overlay extraRoots)。 */
|
|
16
|
+
export interface SkillReadLocate {
|
|
17
|
+
workdir?: string;
|
|
18
|
+
skillDir?: string;
|
|
19
|
+
extraRoots?: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function isReadTool(toolName: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 实际读到的文件是否落在已装配技能包内。不解析定位符。
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifySkillFile(absPath: string, skillDirs?: SkillDirRef[]): {
|
|
26
|
+
kind: 'loaded' | 'resource';
|
|
27
|
+
path: string;
|
|
28
|
+
skillId: string;
|
|
29
|
+
} | null;
|
|
15
30
|
/**
|
|
16
|
-
*
|
|
31
|
+
* 给 EAP_SKILL_DIR 推断:把 `arguments.path` 按 Harness 规则落到文件再对 skillDirs。
|
|
32
|
+
* Trace / SSE 入口判定走 `classifySkillFile`(实际读到的文件),不走这里。
|
|
17
33
|
*/
|
|
18
|
-
export declare function classifySkillRead(toolName: string, args: Record<string, unknown> | undefined, skillDirs?: SkillDirRef[]): {
|
|
34
|
+
export declare function classifySkillRead(toolName: string, args: Record<string, unknown> | undefined, skillDirs?: SkillDirRef[], locate?: SkillReadLocate): {
|
|
19
35
|
kind: 'loaded' | 'resource';
|
|
20
36
|
path: string;
|
|
21
37
|
skillId: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { userRoot } from '../../common/user-root.js';
|
|
4
|
+
import { resolveSkillReadTarget } from '../../tool/skill-relative-read.js';
|
|
4
5
|
function tryRealpath(p) {
|
|
5
6
|
try {
|
|
6
7
|
return realpathSync(p);
|
|
@@ -30,10 +31,6 @@ function longestSkillMatch(filePath, skillDirs) {
|
|
|
30
31
|
}
|
|
31
32
|
return best;
|
|
32
33
|
}
|
|
33
|
-
function skillIdFromSkillsSegment(norm) {
|
|
34
|
-
const match = norm.match(/(?:^|\/)(?:preinstall\/)?skills\/([^/]+)(?:\/|$)/i);
|
|
35
|
-
return match?.[1] ?? '';
|
|
36
|
-
}
|
|
37
34
|
/**
|
|
38
35
|
* 按实际 bundle 路径判定空间。有 agentDir 时相对预装根与 users/ 判定;
|
|
39
36
|
* 否则看路径段 `preinstall/skills` / `users/<id>/skills`。
|
|
@@ -72,34 +69,63 @@ export function classifySkillSpace(bundleOrFilePath, opts) {
|
|
|
72
69
|
return 'user';
|
|
73
70
|
return undefined;
|
|
74
71
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
72
|
+
function extraRootsForSkillRead(skillDirs, locate) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const dir of skillDirs ?? []) {
|
|
75
|
+
if (dir.bundlePath)
|
|
76
|
+
out.push(dir.bundlePath);
|
|
77
|
+
}
|
|
78
|
+
for (const root of locate?.extraRoots ?? []) {
|
|
79
|
+
if (root)
|
|
80
|
+
out.push(root);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
export function isReadTool(toolName) {
|
|
85
|
+
const n = toolName.toLowerCase().replace(/-/g, '_');
|
|
86
|
+
return n === 'read_file' || n === 'read_skill_file' || n === 'readfile';
|
|
77
87
|
}
|
|
78
88
|
/**
|
|
79
|
-
*
|
|
89
|
+
* 实际读到的文件是否落在已装配技能包内。不解析定位符。
|
|
80
90
|
*/
|
|
81
|
-
export function
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
91
|
+
export function classifySkillFile(absPath, skillDirs) {
|
|
92
|
+
const trimmed = absPath.trim();
|
|
93
|
+
if (!trimmed)
|
|
94
|
+
return null;
|
|
95
|
+
const matched = longestSkillMatch(trimmed, skillDirs);
|
|
96
|
+
if (!matched)
|
|
97
|
+
return null;
|
|
98
|
+
const resolved = normalizeForMatch(trimmed);
|
|
99
|
+
const isEntry = /(^|\/)(SKILL\.md|skill.md)$/i.test(resolved);
|
|
100
|
+
return {
|
|
101
|
+
kind: isEntry ? 'loaded' : 'resource',
|
|
102
|
+
path: resolved,
|
|
103
|
+
skillId: matched.skillId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 给 EAP_SKILL_DIR 推断:把 `arguments.path` 按 Harness 规则落到文件再对 skillDirs。
|
|
108
|
+
* Trace / SSE 入口判定走 `classifySkillFile`(实际读到的文件),不走这里。
|
|
109
|
+
*/
|
|
110
|
+
export function classifySkillRead(toolName, args, skillDirs, locate) {
|
|
111
|
+
if (!isReadTool(toolName))
|
|
84
112
|
return null;
|
|
85
113
|
const filePath = String(args?.path ?? args?.file ?? '');
|
|
86
114
|
if (!filePath)
|
|
87
115
|
return null;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
116
|
+
const target = resolveSkillReadTarget(filePath, {
|
|
117
|
+
workdir: locate?.workdir,
|
|
118
|
+
skillDir: locate?.skillDir,
|
|
119
|
+
extraRoots: extraRootsForSkillRead(skillDirs, locate),
|
|
120
|
+
});
|
|
121
|
+
const matched = longestSkillMatch(target, skillDirs);
|
|
122
|
+
if (!matched)
|
|
123
|
+
return null;
|
|
124
|
+
const slash = target.replace(/\\/g, '/');
|
|
125
|
+
const isEntry = /(^|\/)(SKILL\.md|skill.md)$/i.test(slash);
|
|
126
|
+
return {
|
|
127
|
+
kind: isEntry ? 'loaded' : 'resource',
|
|
128
|
+
path: target,
|
|
129
|
+
skillId: matched.skillId,
|
|
130
|
+
};
|
|
105
131
|
}
|
|
@@ -48,6 +48,8 @@ export interface ToolNodeProgress {
|
|
|
48
48
|
node?: string;
|
|
49
49
|
skillId?: string;
|
|
50
50
|
skillDirs?: SkillDirRef[];
|
|
51
|
+
/** read_file 实际读到的文件;与 ToolCallResult.resolvedPath 相同 */
|
|
52
|
+
resolvedPath?: string;
|
|
51
53
|
/** 已在 Skill 子图内(state.skillSpanId);读 SKILL.md 不再发 skill.started */
|
|
52
54
|
inSkillSubgraph?: boolean;
|
|
53
55
|
skillSpace?: 'agent' | 'user' | 'local';
|
|
@@ -295,7 +295,7 @@ export class ToolNode {
|
|
|
295
295
|
if (result.success) {
|
|
296
296
|
const catalog = skillEntryInvocationFromTool({
|
|
297
297
|
toolName: req.toolName,
|
|
298
|
-
|
|
298
|
+
resolvedPath: result.resolvedPath,
|
|
299
299
|
skillDirs,
|
|
300
300
|
alreadyInSkillSpan: inSkillSubgraph,
|
|
301
301
|
});
|
|
@@ -312,6 +312,7 @@ export class ToolNode {
|
|
|
312
312
|
content: result.content,
|
|
313
313
|
...toolArtifactFields(result),
|
|
314
314
|
executionTime,
|
|
315
|
+
...(result.resolvedPath ? { resolvedPath: result.resolvedPath } : {}),
|
|
315
316
|
...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
|
|
316
317
|
...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
|
|
317
318
|
...(skillIdentity?.skillName ? { skillName: skillIdentity.skillName } : {}),
|
|
@@ -346,7 +347,7 @@ export class ToolNode {
|
|
|
346
347
|
if (result.success) {
|
|
347
348
|
await recordSkillEntrySpan(state, {
|
|
348
349
|
toolName: req.toolName,
|
|
349
|
-
|
|
350
|
+
resolvedPath: result.resolvedPath,
|
|
350
351
|
skillDirs,
|
|
351
352
|
});
|
|
352
353
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `createToolExecutor` 时钉进执行器 extraRoots;SDK `createAgent` 构造完会恢复 process.env,
|
|
10
10
|
* 请求时不再读这两项。相对 `read_file` 会把 overlay 根下直接子目录里带 SKILL.md 的包纳入唯一命中。
|
|
11
11
|
*/
|
|
12
|
+
export { locateSkillRelativeRead } from './skill-relative-read.js';
|
|
12
13
|
import type { LoadedToolMap } from './loaded-tool-definition.js';
|
|
13
14
|
import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
|
|
14
15
|
export declare function mergeHarnessPrimitives(loadedTools: LoadedToolMap | undefined): LoadedToolMap | undefined;
|
|
@@ -42,13 +43,6 @@ export declare function captureOverlayJailOptions(env?: NodeJS.ProcessEnv): Pick
|
|
|
42
43
|
export declare function resolveJailRoots(options?: HarnessJailOptions): string[];
|
|
43
44
|
/** 路径落在写根外的 extraRoot 时,返回该 extraRoot。 */
|
|
44
45
|
export declare function extraRootContaining(abs: string, extraRoots: string[], workdir: string): string | undefined;
|
|
45
|
-
/**
|
|
46
|
-
* 相对 read_file:`技能名/文件`、全库唯一命中、显式 skillDir,否则用户目录。
|
|
47
|
-
*/
|
|
48
|
-
export declare function locateSkillRelativeRead(filePath: string, workdir: string | undefined, skillDir: string | undefined, extraRoots?: string[]): {
|
|
49
|
-
path: string;
|
|
50
|
-
relativeBase: string | undefined;
|
|
51
|
-
};
|
|
52
46
|
export declare function resolveJailedPath(requested: string, roots: string[], opts?: {
|
|
53
47
|
mustExist?: boolean;
|
|
54
48
|
relativeBase?: string;
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* 请求时不再读这两项。相对 `read_file` 会把 overlay 根下直接子目录里带 SKILL.md 的包纳入唯一命中。
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
-
import { constants as fsConstants,
|
|
13
|
+
import { constants as fsConstants, realpathSync } from 'node:fs';
|
|
14
14
|
import { chmod, mkdir, open, readFile, stat } from 'node:fs/promises';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { MISSING_USER_DIR, userRoot } from '../common/user-root.js';
|
|
17
|
+
import { locateSkillRelativeRead, userReadBase } from './skill-relative-read.js';
|
|
18
|
+
export { locateSkillRelativeRead } from './skill-relative-read.js';
|
|
17
19
|
import { MAX_SNAPSHOT_ENUM, MAX_SNAPSHOT_URLS, PUBLIC_FILE_MODE, ensureUserSlots, maxWriteBytesFromEnv, publishFromFiles, resolveUserProductPath, snapshotCandidates, walkFilesTree, } from './static-artifact-url.js';
|
|
18
20
|
import { BUILTIN_TOOL_SOURCE, DEFAULT_BUILTIN_TIMEOUT_MS, MAX_BUILTIN_TIMEOUT_MS, clampBuiltinTimeout, isBuiltinToolExecution, resolveBuiltinToolsConfig, } from '@gt-fe/eap-contracts';
|
|
19
21
|
import { ensureInterpreterShims, formatSpawnError, killProcessTree, resolveShellSpawn, sandboxEnv, spawnOptionsFor, substituteSkillDir, } from './harness-command.js';
|
|
@@ -316,74 +318,6 @@ function resolveViaExistingAncestor(abs) {
|
|
|
316
318
|
cur = parent;
|
|
317
319
|
}
|
|
318
320
|
}
|
|
319
|
-
function userReadBase(workdir) {
|
|
320
|
-
return workdir?.trim()
|
|
321
|
-
? (tryRealpath(path.resolve(workdir)) ?? path.resolve(workdir))
|
|
322
|
-
: undefined;
|
|
323
|
-
}
|
|
324
|
-
function isSkillPackageDir(dir) {
|
|
325
|
-
return existsSync(path.join(dir, 'SKILL.md')) || existsSync(path.join(dir, 'skill.md'));
|
|
326
|
-
}
|
|
327
|
-
function skillPackageRoots(roots) {
|
|
328
|
-
const out = [];
|
|
329
|
-
const seen = new Set();
|
|
330
|
-
const add = (dir) => {
|
|
331
|
-
if (seen.has(dir))
|
|
332
|
-
return;
|
|
333
|
-
seen.add(dir);
|
|
334
|
-
out.push(dir);
|
|
335
|
-
};
|
|
336
|
-
for (const root of roots) {
|
|
337
|
-
const resolved = path.resolve(root);
|
|
338
|
-
if (isSkillPackageDir(resolved)) {
|
|
339
|
-
add(resolved);
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
let entries;
|
|
343
|
-
try {
|
|
344
|
-
entries = readdirSync(resolved, { withFileTypes: true });
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
for (const ent of entries) {
|
|
350
|
-
if (!ent.isDirectory())
|
|
351
|
-
continue;
|
|
352
|
-
const child = path.join(resolved, ent.name);
|
|
353
|
-
if (isSkillPackageDir(child))
|
|
354
|
-
add(child);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return out;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* 相对 read_file:`技能名/文件`、全库唯一命中、显式 skillDir,否则用户目录。
|
|
361
|
-
*/
|
|
362
|
-
export function locateSkillRelativeRead(filePath, workdir, skillDir, extraRoots) {
|
|
363
|
-
const trimmed = filePath.trim();
|
|
364
|
-
const userBase = userReadBase(workdir);
|
|
365
|
-
if (!trimmed || path.isAbsolute(trimmed))
|
|
366
|
-
return { path: trimmed, relativeBase: userBase };
|
|
367
|
-
const packages = skillPackageRoots([
|
|
368
|
-
...(skillDir?.trim() ? [skillDir] : []),
|
|
369
|
-
...(extraRoots ?? []),
|
|
370
|
-
]);
|
|
371
|
-
const slash = trimmed.replace(/\\/g, '/');
|
|
372
|
-
const segs = slash.split('/').filter(Boolean);
|
|
373
|
-
if (segs.length >= 2) {
|
|
374
|
-
const hit = packages.find((pkg) => path.basename(pkg) === segs[0]);
|
|
375
|
-
if (hit)
|
|
376
|
-
return { path: segs.slice(1).join('/'), relativeBase: hit };
|
|
377
|
-
}
|
|
378
|
-
const matches = packages.filter((pkg) => existsSync(path.resolve(pkg, trimmed)));
|
|
379
|
-
if (matches.length === 1)
|
|
380
|
-
return { path: trimmed, relativeBase: matches[0] };
|
|
381
|
-
const skillBase = skillDir?.trim() ? path.resolve(skillDir) : undefined;
|
|
382
|
-
if (skillBase && existsSync(path.resolve(skillBase, trimmed))) {
|
|
383
|
-
return { path: trimmed, relativeBase: skillBase };
|
|
384
|
-
}
|
|
385
|
-
return { path: trimmed, relativeBase: userBase };
|
|
386
|
-
}
|
|
387
321
|
export function resolveJailedPath(requested, roots, opts) {
|
|
388
322
|
const trimmed = requested.trim();
|
|
389
323
|
if (!trimmed || trimmed.includes('\0')) {
|
|
@@ -530,7 +464,7 @@ export class HarnessPrimitiveExecutor {
|
|
|
530
464
|
if (isSkillEntryFile(target)) {
|
|
531
465
|
text = expandSkillScriptRefs(text, path.dirname(target));
|
|
532
466
|
}
|
|
533
|
-
return ok(request, text);
|
|
467
|
+
return { ...ok(request, text), resolvedPath: target };
|
|
534
468
|
}
|
|
535
469
|
async writeFile(request, append) {
|
|
536
470
|
const filePath = String(request.arguments.path ?? '');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function userReadBase(workdir: string | undefined): string | undefined;
|
|
2
|
+
/**
|
|
3
|
+
* 相对 read_file:`技能名/文件`、全库唯一命中、显式 skillDir,否则用户目录。
|
|
4
|
+
*/
|
|
5
|
+
export declare function locateSkillRelativeRead(filePath: string, workdir: string | undefined, skillDir: string | undefined, extraRoots?: string[]): {
|
|
6
|
+
path: string;
|
|
7
|
+
relativeBase: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
/** 定位符落到实际文件:绝对路径原样;相对路径按 locateSkillRelativeRead 的包根拼接。 */
|
|
10
|
+
export declare function resolveSkillReadTarget(filePath: string, opts?: {
|
|
11
|
+
workdir?: string;
|
|
12
|
+
skillDir?: string;
|
|
13
|
+
extraRoots?: string[];
|
|
14
|
+
}): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read_file 相对路径如何落到技能包:Harness 读文件与 Trace 判定入口共用。
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
function tryRealpath(p) {
|
|
7
|
+
try {
|
|
8
|
+
return realpathSync(p);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function userReadBase(workdir) {
|
|
15
|
+
return workdir?.trim()
|
|
16
|
+
? (tryRealpath(path.resolve(workdir)) ?? path.resolve(workdir))
|
|
17
|
+
: undefined;
|
|
18
|
+
}
|
|
19
|
+
function isSkillPackageDir(dir) {
|
|
20
|
+
return existsSync(path.join(dir, 'SKILL.md')) || existsSync(path.join(dir, 'skill.md'));
|
|
21
|
+
}
|
|
22
|
+
function skillPackageRoots(roots) {
|
|
23
|
+
const out = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const add = (dir) => {
|
|
26
|
+
if (seen.has(dir))
|
|
27
|
+
return;
|
|
28
|
+
seen.add(dir);
|
|
29
|
+
out.push(dir);
|
|
30
|
+
};
|
|
31
|
+
for (const root of roots) {
|
|
32
|
+
const resolved = path.resolve(root);
|
|
33
|
+
if (isSkillPackageDir(resolved)) {
|
|
34
|
+
add(resolved);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = readdirSync(resolved, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const ent of entries) {
|
|
45
|
+
if (!ent.isDirectory())
|
|
46
|
+
continue;
|
|
47
|
+
const child = path.join(resolved, ent.name);
|
|
48
|
+
if (isSkillPackageDir(child))
|
|
49
|
+
add(child);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 相对 read_file:`技能名/文件`、全库唯一命中、显式 skillDir,否则用户目录。
|
|
56
|
+
*/
|
|
57
|
+
export function locateSkillRelativeRead(filePath, workdir, skillDir, extraRoots) {
|
|
58
|
+
const trimmed = filePath.trim();
|
|
59
|
+
const userBase = userReadBase(workdir);
|
|
60
|
+
if (!trimmed || path.isAbsolute(trimmed))
|
|
61
|
+
return { path: trimmed, relativeBase: userBase };
|
|
62
|
+
const packages = skillPackageRoots([
|
|
63
|
+
...(skillDir?.trim() ? [skillDir] : []),
|
|
64
|
+
...(extraRoots ?? []),
|
|
65
|
+
]);
|
|
66
|
+
const slash = trimmed.replace(/\\/g, '/');
|
|
67
|
+
const segs = slash.split('/').filter(Boolean);
|
|
68
|
+
if (segs.length >= 2) {
|
|
69
|
+
const hit = packages.find((pkg) => path.basename(pkg) === segs[0]);
|
|
70
|
+
if (hit)
|
|
71
|
+
return { path: segs.slice(1).join('/'), relativeBase: hit };
|
|
72
|
+
}
|
|
73
|
+
const matches = packages.filter((pkg) => existsSync(path.resolve(pkg, trimmed)));
|
|
74
|
+
if (matches.length === 1)
|
|
75
|
+
return { path: trimmed, relativeBase: matches[0] };
|
|
76
|
+
const skillBase = skillDir?.trim() ? path.resolve(skillDir) : undefined;
|
|
77
|
+
if (skillBase && existsSync(path.resolve(skillBase, trimmed))) {
|
|
78
|
+
return { path: trimmed, relativeBase: skillBase };
|
|
79
|
+
}
|
|
80
|
+
return { path: trimmed, relativeBase: userBase };
|
|
81
|
+
}
|
|
82
|
+
/** 定位符落到实际文件:绝对路径原样;相对路径按 locateSkillRelativeRead 的包根拼接。 */
|
|
83
|
+
export function resolveSkillReadTarget(filePath, opts) {
|
|
84
|
+
const trimmed = filePath.trim();
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
return '';
|
|
87
|
+
const located = locateSkillRelativeRead(trimmed, opts?.workdir, opts?.skillDir, opts?.extraRoots);
|
|
88
|
+
if (path.isAbsolute(trimmed)) {
|
|
89
|
+
const resolved = path.resolve(trimmed);
|
|
90
|
+
return (tryRealpath(resolved) ?? resolved).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
91
|
+
}
|
|
92
|
+
if (located.relativeBase) {
|
|
93
|
+
const resolved = path.resolve(located.relativeBase, located.path);
|
|
94
|
+
return (tryRealpath(resolved) ?? resolved).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
95
|
+
}
|
|
96
|
+
const resolved = path.resolve(trimmed);
|
|
97
|
+
return (tryRealpath(resolved) ?? resolved).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
98
|
+
}
|
|
@@ -122,6 +122,11 @@ export interface ToolCallResult {
|
|
|
122
122
|
size: number;
|
|
123
123
|
url: string;
|
|
124
124
|
}>;
|
|
125
|
+
/**
|
|
126
|
+
* `read_file` 实际读到的文件。只给 Runtime 记 Skill 节点 / SSE,不进模型正文。
|
|
127
|
+
* 没有该字段时不记 Skill 节点,不再用 `arguments.path` 猜。
|
|
128
|
+
*/
|
|
129
|
+
resolvedPath?: string;
|
|
125
130
|
}
|
|
126
131
|
export interface LLMToolCall {
|
|
127
132
|
id: string;
|