@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.
@@ -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 { classifySkillRead } from './skill-read.js';
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 = classifySkillRead(progress.toolName, progress.arguments, progress.skillDirs);
287
- if (skillRead && progress.status === 'completed') {
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
- arguments: progress.arguments,
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: { ...progress },
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 || JSON.stringify(result),
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
- arguments?: Record<string, unknown>;
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
- arguments?: Record<string, unknown>;
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 { classifySkillRead, classifySkillSpace } from './skill-read.js';
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
- const hit = classifySkillRead(input.toolName, input.arguments, input.skillDirs);
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
- arguments: input.arguments,
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
- * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
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 escapeRegExp(value) {
76
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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
- * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
89
+ * 实际读到的文件是否落在已装配技能包内。不解析定位符。
80
90
  */
81
- export function classifySkillRead(toolName, args, skillDirs) {
82
- const n = toolName.toLowerCase().replace(/-/g, '_');
83
- if (n !== 'read_file' && n !== 'read_skill_file' && n !== 'readfile')
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 norm = filePath.replace(/\\/g, '/');
89
- const matched = longestSkillMatch(filePath, skillDirs);
90
- const isEntry = /(^|\/)(SKILL\.md|skill\.md)$/i.test(norm);
91
- if (matched) {
92
- return {
93
- kind: isEntry ? 'loaded' : 'resource',
94
- path: filePath,
95
- skillId: matched.skillId,
96
- };
97
- }
98
- if (isEntry) {
99
- return { kind: 'loaded', path: filePath, skillId: skillIdFromSkillsSegment(norm) };
100
- }
101
- if (/(^|\/)(?:preinstall\/)?skills\//i.test(norm)) {
102
- return { kind: 'resource', path: filePath, skillId: skillIdFromSkillsSegment(norm) };
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
- arguments: req.arguments,
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
- arguments: req.arguments,
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, existsSync, readdirSync, realpathSync } from 'node:fs';
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gt-fe/eap-runtime",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",