@gt-fe/eap-runtime 0.0.31 → 0.0.33

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.
@@ -27,7 +27,7 @@ import { classifyRuntimeError } from '../../utils/http-error.js';
27
27
  import { DEFAULT_GRAPH_TIMEOUT_MS } from '../../common/types.js';
28
28
  import { resolveTaskTimeoutMs } from '../../task/resolve-task-timeout.js';
29
29
  import { runWithExecutionTrace } from '../../graph/execution-trace-context.js';
30
- import { resolveAgentSystemPromptFromLoader } from '../../registry/graph-loader.js';
30
+ import { resolveAgentDisplayNameFromLoader, resolveAgentSystemPromptFromLoader, } from '../../registry/graph-loader.js';
31
31
  import { toLoadedToolMap } from '../../tool/loaded-tool-definition.js';
32
32
  import { toLoadedSkillMap } from '../../tool/system-prompt-builder.js';
33
33
  import { assembleLoadedTools } from '../../tool/harness-primitives.js';
@@ -204,6 +204,7 @@ export class InvokeOrchestrator {
204
204
  ? (await this.graphLoader.resolveAgentDir(agentCode, req.agentVersion)) ?? undefined
205
205
  : undefined;
206
206
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.graphLoader, agentCode, req.agentVersion);
207
+ const agentName = await resolveAgentDisplayNameFromLoader(this.graphLoader, agentCode, req.agentVersion);
207
208
  // 模型上下文:Checkpoint 已有 messages 则压它;压成功用 __set 写回最新 Checkpoint。Message 库不改。
208
209
  const facing = await loadModelFacingHistory({
209
210
  sessionHistory: messageRecordsToGraphMessages(sessionContext.messages),
@@ -261,6 +262,7 @@ export class InvokeOrchestrator {
261
262
  promptRoots,
262
263
  agentDir,
263
264
  agentSystemPrompt,
265
+ agentName,
264
266
  traceId: resolveTraceId(req),
265
267
  traceCollector: this.traceCollector,
266
268
  abortSignal,
@@ -31,7 +31,7 @@ import { SSEEmitter } from '../../sse/sse-emitter.js';
31
31
  import { resolveExecutionContext } from '../../governance/governance-client.js';
32
32
  import { resolveSkillInputFromInvokeInput } from '../../registry/skill-io-guard.js';
33
33
  import { ErrorCodes } from '@gt-fe/eap-contracts';
34
- import { resolveAgentSystemPromptFromLoader } from '../../registry/graph-loader.js';
34
+ import { resolveAgentDisplayNameFromLoader, resolveAgentSystemPromptFromLoader, } from '../../registry/graph-loader.js';
35
35
  import { classifyRuntimeError } from '../../utils/http-error.js';
36
36
  import { toLoadedToolMap } from '../../tool/loaded-tool-definition.js';
37
37
  import { toLoadedSkillMap } from '../../tool/system-prompt-builder.js';
@@ -222,6 +222,7 @@ export class StreamOrchestrator {
222
222
  ? (await this.graphLoader.resolveAgentDir(agentCode, req.agentVersion)) ?? undefined
223
223
  : undefined;
224
224
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.graphLoader, agentCode, req.agentVersion);
225
+ const agentName = await resolveAgentDisplayNameFromLoader(this.graphLoader, agentCode, req.agentVersion);
225
226
  // 模型上下文:Checkpoint 已有 messages 则压它;压成功用 __set 写回最新 Checkpoint。Message 库不改。
226
227
  const facing = await loadModelFacingHistory({
227
228
  sessionHistory: messageRecordsToGraphMessages(sessionContext.messages),
@@ -293,6 +294,7 @@ export class StreamOrchestrator {
293
294
  promptRoots,
294
295
  agentDir,
295
296
  agentSystemPrompt,
297
+ agentName,
296
298
  traceId: resolveTraceId(req),
297
299
  traceCollector: this.traceCollector,
298
300
  abortSignal,
@@ -127,7 +127,6 @@ export declare class LangGraphAdapter implements IStateGraphEngine {
127
127
  private createNodeFunction;
128
128
  private createNodeFunctionBody;
129
129
  private createSkillBindNodeFunction;
130
- private buildSkillCatalog;
131
130
  private createToolNodeFunction;
132
131
  private createLLMNodeFunction;
133
132
  /**
@@ -23,7 +23,7 @@ import { ToolResultInjector } from '../../tool/tool-result-injector.js';
23
23
  import { fileArtifactFromToolOutput } from '../../tool/file-artifact.js';
24
24
  import { EchoLLMProvider } from '../../providers/echo-llm-provider.js';
25
25
  import { ConditionEvaluator } from '../condition-evaluator.js';
26
- import { SystemPromptBuilder, buildToolCatalog, loadedToolsToApiTools, resolveLlmPromptInputs } from '../../tool/system-prompt-builder.js';
26
+ import { SystemPromptBuilder, buildSkillCatalog, buildToolCatalog, loadedToolsToApiTools, resolveLlmPromptInputs, spokenName } from '../../tool/system-prompt-builder.js';
27
27
  import { resolveJailFromState, resolveJailRoots } from '../../tool/harness-primitives.js';
28
28
  import os from 'node:os';
29
29
  import { bindSelectedSkill } from '../nodes/skill-bind-node.js';
@@ -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,11 +301,12 @@ 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
  });
306
308
  if (catalogSkill) {
309
+ const skillName = spokenName(progress.skillName, catalogSkill.skillId) ?? catalogSkill.skillId;
307
310
  stream.sink({
308
311
  type: 'skill.started',
309
312
  taskId,
@@ -311,7 +314,7 @@ export class LangGraphAdapter {
311
314
  timestamp,
312
315
  payload: {
313
316
  skillId: catalogSkill.skillId,
314
- skillName: progress.skillName || catalogSkill.skillId,
317
+ skillName,
315
318
  path: catalogSkill.path,
316
319
  ...(progress.skillSpace ? { skillSpace: progress.skillSpace } : {}),
317
320
  ...(progress.skillVersion ? { skillVersion: progress.skillVersion } : {}),
@@ -324,7 +327,7 @@ export class LangGraphAdapter {
324
327
  timestamp,
325
328
  payload: {
326
329
  skillId: catalogSkill.skillId,
327
- skillName: catalogSkill.skillId,
330
+ skillName,
328
331
  },
329
332
  });
330
333
  }
@@ -355,12 +358,14 @@ export class LangGraphAdapter {
355
358
  },
356
359
  });
357
360
  }
361
+ // resolvedPath / skillDirs 只给上面 classifySkillFile;inSkillSubgraph 只给 skill.started 去重。不进对话流。
362
+ const { resolvedPath: _resolvedPath, skillDirs: _skillDirs, inSkillSubgraph: _inSkillSubgraph, ...progressView } = progress;
358
363
  stream.sink({
359
364
  type: 'tool.progress',
360
365
  taskId,
361
366
  correlationId,
362
367
  timestamp,
363
- payload: { ...progress },
368
+ payload: progressView,
364
369
  });
365
370
  if (progress.status === 'completed') {
366
371
  stream.sink({
@@ -456,6 +461,7 @@ export class LangGraphAdapter {
456
461
  // agentId 已废弃:channel 保留仅为旧 Checkpoint 反序列化兼容,新代码只写 agentCode
457
462
  agentId: ephemeral(''),
458
463
  agentCode: ephemeral(''),
464
+ agentName: ephemeral(undefined),
459
465
  agentVersion: ephemeral(undefined),
460
466
  delegationToken: ephemeral(''),
461
467
  correlationId: ephemeral(''),
@@ -852,7 +858,7 @@ export class LangGraphAdapter {
852
858
  correlationId: undefined,
853
859
  timestamp: new Date().toISOString(),
854
860
  payload: {
855
- skillName: patch.skillName || patch.activatedSkill,
861
+ skillName: spokenName(patch.skillName, patch.activatedSkill) ?? patch.activatedSkill,
856
862
  skillId: patch.activatedSkill,
857
863
  confidence: 1,
858
864
  },
@@ -894,20 +900,6 @@ export class LangGraphAdapter {
894
900
  };
895
901
  };
896
902
  }
897
- buildSkillCatalog(defs) {
898
- if (!defs)
899
- return undefined;
900
- const rows = Object.values(defs).map((skill) => {
901
- const desc = (skill.description ?? '').replace(/\s+/g, ' ').trim();
902
- const entry = skill.entryFile ?? (skill.bundlePath ? `${skill.bundlePath}/SKILL.md` : '');
903
- const title = skill.name && skill.name !== skill.skillId ? `${skill.skillId}(${skill.name})` : skill.skillId;
904
- const pathLine = entry ? `\n SKILL.md: ${entry}` : '';
905
- return `- **${title}**${desc ? `:${desc}` : ''}${pathLine}`;
906
- });
907
- if (rows.length === 0)
908
- return undefined;
909
- return `# 可用技能\n要用某个技能时,调用 read_file 读取其 SKILL.md。脚本和参考文档默认在同一目录。\n${rows.join('\n')}`;
910
- }
911
903
  createToolNodeFunction(node) {
912
904
  const toolName = node.config?.['tool'] ?? node.id;
913
905
  const defaultArgs = node.config?.['arguments'] ?? {};
@@ -936,6 +928,13 @@ export class LangGraphAdapter {
936
928
  node: node.id,
937
929
  });
938
930
  const skillDefs = state['skillDefinitions'];
931
+ const jail = resolveJailFromState({
932
+ agentDir: state['agentDir'],
933
+ userId: state['userId'],
934
+ executionContext: state['executionContext'],
935
+ loadedTools: state['loadedTools'],
936
+ skillDefinitions: skillDefs,
937
+ });
939
938
  const jailExtraRoots = Object.values(skillDefs ?? {})
940
939
  .map((skill) => skill.bundlePath)
941
940
  .filter((p) => Boolean(p));
@@ -946,6 +945,7 @@ export class LangGraphAdapter {
946
945
  authContext: { userId, delegationToken, agentCode },
947
946
  correlationId,
948
947
  taskId,
948
+ ...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
949
949
  ...(jailExtraRoots.length > 0 ? { jailExtraRoots } : {}),
950
950
  };
951
951
  const retry = resolveToolRetryConfig(node.retry);
@@ -981,13 +981,21 @@ export class LangGraphAdapter {
981
981
  executionTime,
982
982
  node: node.id,
983
983
  ...(skillDirs.length > 0 ? { skillDirs } : {}),
984
+ ...(result.resolvedPath ? { resolvedPath: result.resolvedPath } : {}),
984
985
  });
986
+ if (result.success) {
987
+ await recordSkillEntrySpan(state, {
988
+ toolName: result.toolName,
989
+ resolvedPath: result.resolvedPath,
990
+ skillDirs,
991
+ });
992
+ }
985
993
  // 重要:messages channel 的 reducer 是 append 语义,只返回新增消息
986
994
  return {
987
995
  messages: [
988
996
  {
989
997
  role: 'tool',
990
- content: result.content || JSON.stringify(result),
998
+ content: result.content ?? '',
991
999
  toolCallId,
992
1000
  toolName,
993
1001
  success: result.success ?? true,
@@ -1041,6 +1049,7 @@ export class LangGraphAdapter {
1041
1049
  const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
1042
1050
  const loadedToolsBase = state['loadedTools'];
1043
1051
  const agentCode = typeof state['agentCode'] === 'string' ? state['agentCode'] : undefined;
1052
+ const agentName = typeof state['agentName'] === 'string' ? state['agentName'] : undefined;
1044
1053
  const skillDefs = state['skillDefinitions'];
1045
1054
  const execCtx = state['executionContext'];
1046
1055
  const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
@@ -1072,7 +1081,7 @@ export class LangGraphAdapter {
1072
1081
  const toolNames = loadedTools ? Object.keys(loadedTools) : [];
1073
1082
  console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
1074
1083
  }
1075
- const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
1084
+ const catalog = includeSkillCatalog ? buildSkillCatalog(skillDefs) : undefined;
1076
1085
  const toolCatalog = disableTools ? undefined : buildToolCatalog(loadedTools);
1077
1086
  const jail = resolveJailFromState({
1078
1087
  agentDir: state['agentDir'],
@@ -1103,6 +1112,7 @@ export class LangGraphAdapter {
1103
1112
  nodePrompt: promptInputs.nodePrompt,
1104
1113
  skillCatalog: catalog,
1105
1114
  toolCatalog,
1115
+ agentName,
1106
1116
  agentCode,
1107
1117
  userId,
1108
1118
  workingDirectory,
@@ -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,12 +2,15 @@
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';
13
+ import { spokenName } from '../../tool/system-prompt-builder.js';
11
14
  function declaredSkillVersion(skillId, version) {
12
15
  const raw = version?.trim();
13
16
  if (!raw || raw === skillId)
@@ -25,9 +28,10 @@ export function skillTraceIdentity(state, skillId, fallbackPath) {
25
28
  const bundle = info?.bundlePath || fallbackPath;
26
29
  const skillSpace = bundle ? classifySkillSpace(bundle, { agentDir, userId }) : undefined;
27
30
  const skillVersion = declaredSkillVersion(skillId, info?.version);
31
+ const skillName = spokenName(info?.name, undefined);
28
32
  return {
29
33
  skillId,
30
- ...(info?.name ? { skillName: info.name } : {}),
34
+ ...(skillName ? { skillName } : {}),
31
35
  ...(skillVersion ? { skillVersion } : {}),
32
36
  ...(skillSpace ? { skillSpace } : {}),
33
37
  };
@@ -37,6 +41,7 @@ export function skillSpanFields(identity, extra) {
37
41
  ...(identity.skillVersion ? { version: identity.skillVersion } : {}),
38
42
  input: {
39
43
  skillId: identity.skillId,
44
+ ...(identity.skillName ? { skillName: identity.skillName } : {}),
40
45
  ...(identity.skillSpace ? { skillSpace: identity.skillSpace } : {}),
41
46
  ...(extra?.path ? { path: extra.path } : {}),
42
47
  ...(extra?.source ? { source: extra.source } : {}),
@@ -46,7 +51,12 @@ export function skillSpanFields(identity, extra) {
46
51
  export function skillEntryInvocationFromTool(input) {
47
52
  if (input.alreadyInSkillSpan)
48
53
  return null;
49
- const hit = classifySkillRead(input.toolName, input.arguments, input.skillDirs);
54
+ if (!isReadTool(input.toolName))
55
+ return null;
56
+ const resolved = input.resolvedPath?.trim();
57
+ if (!resolved)
58
+ return null;
59
+ const hit = classifySkillFile(resolved, input.skillDirs);
50
60
  if (!hit || hit.kind !== 'loaded' || !hit.skillId)
51
61
  return null;
52
62
  return { skillId: hit.skillId, path: hit.path };
@@ -55,7 +65,7 @@ export async function recordSkillEntrySpan(state, input) {
55
65
  const alreadyInSkillSpan = typeof state['skillSpanId'] === 'string' && Boolean(state['skillSpanId']);
56
66
  const hit = skillEntryInvocationFromTool({
57
67
  toolName: input.toolName,
58
- arguments: input.arguments,
68
+ resolvedPath: input.resolvedPath,
59
69
  skillDirs: input.skillDirs,
60
70
  alreadyInSkillSpan,
61
71
  });
@@ -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
  }
@@ -13,7 +13,7 @@ import { LangGraphAdapter } from './langgraph.adapter.js';
13
13
  import { assertSkillGraphHasNoSubgraphNodes } from './graph-nesting.js';
14
14
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
15
15
  import { skillSpanFields, skillTraceIdentity } from './skill-entry-span.js';
16
- import { filterLoadedToolsForSkill, } from '../../tool/system-prompt-builder.js';
16
+ import { filterLoadedToolsForSkill, spokenName, } from '../../tool/system-prompt-builder.js';
17
17
  import { unwrapHitlPaused } from '../hitl/hitl-handler.js';
18
18
  import { pickContextFields } from '../../token/context-window.js';
19
19
  import { assertSkillIoIfEnabled, coerceSkillInputFromMessages, } from '../../registry/skill-io-guard.js';
@@ -65,11 +65,14 @@ export class SubGraphCompiler {
65
65
  ? state['skillSpanId']
66
66
  : '';
67
67
  const resuming = Boolean(existingSpanId);
68
+ const identity = skillTraceIdentity(state, resolvedSkillId);
69
+ const spanName = resolvedSkillId || dsl.name || 'skill';
70
+ const skillName = spokenName(identity.skillName, resolvedSkillId) ?? spanName;
68
71
  const auditCtx = resuming
69
72
  ? undefined
70
73
  : this.skillAuditRecorder?.startInvocation({
71
74
  skillId: resolvedSkillId,
72
- skillName: dsl.name,
75
+ skillName,
73
76
  agentCode: (state['agentCode'] ?? state['agentId']),
74
77
  userId: state['userId'] ?? '',
75
78
  taskId: state['taskId'],
@@ -77,8 +80,6 @@ export class SubGraphCompiler {
77
80
  });
78
81
  // 用父图这次调用上的 collector;内层 invoke 只补 collector/traceId,沿用外层流式袋。
79
82
  const { collector: traceCollector, traceId } = resolveExecutionTrace(state);
80
- const skillName = dsl.name || resolvedSkillId;
81
- const identity = skillTraceIdentity(state, resolvedSkillId);
82
83
  if (!resuming) {
83
84
  adapter.emitStreamEvent({
84
85
  type: 'skill.started',
@@ -87,7 +88,7 @@ export class SubGraphCompiler {
87
88
  timestamp: new Date().toISOString(),
88
89
  payload: {
89
90
  skillId: resolvedSkillId,
90
- skillName: identity.skillName || skillName,
91
+ skillName,
91
92
  ...(identity.skillSpace ? { skillSpace: identity.skillSpace } : {}),
92
93
  ...(identity.skillVersion ? { skillVersion: identity.skillVersion } : {}),
93
94
  },
@@ -99,7 +100,7 @@ export class SubGraphCompiler {
99
100
  const fields = skillSpanFields(identity, { source: 'subgraph' });
100
101
  skillSpanId = await traceCollector.createNode({
101
102
  traceId,
102
- name: resolvedSkillId || dsl.name || 'skill',
103
+ name: spanName,
103
104
  type: 'skill',
104
105
  agentCode: state['agentCode'] ?? state['agentId'],
105
106
  ...(fields.version ? { version: fields.version } : {}),
@@ -111,7 +112,7 @@ export class SubGraphCompiler {
111
112
  });
112
113
  }
113
114
  catch (err) {
114
- warnTraceSpanFailure('createNode', { type: 'skill', name: resolvedSkillId || dsl.name || 'skill', traceId }, err);
115
+ warnTraceSpanFailure('createNode', { type: 'skill', name: spanName, traceId }, err);
115
116
  skillSpanId = null;
116
117
  }
117
118
  }
@@ -154,6 +155,7 @@ export class SubGraphCompiler {
154
155
  userId,
155
156
  delegationToken,
156
157
  agentCode,
158
+ agentName: typeof state['agentName'] === 'string' ? state['agentName'] : undefined,
157
159
  taskId: state['taskId'],
158
160
  correlationId: state['correlationId'],
159
161
  sessionId: state['sessionId'],
@@ -194,7 +196,7 @@ export class SubGraphCompiler {
194
196
  });
195
197
  }
196
198
  catch (err) {
197
- warnTraceSpanFailure('completeNode', { type: 'skill', name: skillName, traceId, spanId: skillSpanId }, err);
199
+ warnTraceSpanFailure('completeNode', { type: 'skill', name: spanName, traceId, spanId: skillSpanId }, err);
198
200
  }
199
201
  }
200
202
  // 4. 只回写子图本步新增消息(含内部 tool 回合),禁止把入口历史再 append
@@ -269,7 +271,7 @@ export class SubGraphCompiler {
269
271
  });
270
272
  }
271
273
  catch (completeErr) {
272
- warnTraceSpanFailure('completeNode', { type: 'skill', name: skillName, traceId, spanId: skillSpanId }, completeErr);
274
+ warnTraceSpanFailure('completeNode', { type: 'skill', name: spanName, traceId, spanId: skillSpanId }, completeErr);
273
275
  }
274
276
  }
275
277
  adapter.emitStreamEvent({
@@ -181,6 +181,8 @@ export interface GraphState {
181
181
  agentId?: string;
182
182
  /** Agent 对外主键 */
183
183
  agentCode?: string;
184
+ /** Agent 展示名(manifest.name);身份段对用户说话用,缺省回退 agentCode */
185
+ agentName?: string;
184
186
  /** Hosting 路由选定的预装版本(AR-3b;写入 Checkpoint 供 HITL/Fork 恢复) */
185
187
  agentVersion?: string;
186
188
  correlationId?: string;
@@ -2,7 +2,7 @@
2
2
  * 可选图胶水:把 silent LLM 的结构化输出写成 skillRoute / activatedSkill。
3
3
  * 不是 Graph 节点类型。技能正文走短目录 + read_file,不灌 system。
4
4
  */
5
- import type { LoadedSkillInfo, LoadedSkillMap } from '../../tool/system-prompt-builder.js';
5
+ import { type LoadedSkillInfo, type LoadedSkillMap } from '../../tool/system-prompt-builder.js';
6
6
  export type SkillRoute = 'skill' | 'clarify' | 'none';
7
7
  export interface SkillBindPatch {
8
8
  skillRoute: SkillRoute;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * 可选图胶水:把 silent LLM 的结构化输出写成 skillRoute / activatedSkill。
3
+ * 不是 Graph 节点类型。技能正文走短目录 + read_file,不灌 system。
4
+ */
5
+ import { spokenName } from '../../tool/system-prompt-builder.js';
1
6
  function lastAssistantText(messages) {
2
7
  for (let i = messages.length - 1; i >= 0; i--) {
3
8
  const msg = messages[i];
@@ -89,7 +94,7 @@ export function bindSelectedSkill(state) {
89
94
  return {
90
95
  skillRoute: 'skill',
91
96
  activatedSkill: skill.skillId,
92
- skillName: skill.name || skill.skillId,
97
+ skillName: spokenName(skill.name, skill.skillId),
93
98
  skillInput: params,
94
99
  messages: nextMessages(),
95
100
  };
@@ -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';
@@ -203,6 +203,7 @@ export class ToolNode {
203
203
  correlationId: req.correlationId,
204
204
  skillId,
205
205
  ...(identity.toolset ? { toolset: identity.toolset } : {}),
206
+ ...(identity.toolDisplayName ? { toolDisplayName: identity.toolDisplayName } : {}),
206
207
  },
207
208
  });
208
209
  }
@@ -295,7 +296,7 @@ export class ToolNode {
295
296
  if (result.success) {
296
297
  const catalog = skillEntryInvocationFromTool({
297
298
  toolName: req.toolName,
298
- arguments: req.arguments,
299
+ resolvedPath: result.resolvedPath,
299
300
  skillDirs,
300
301
  alreadyInSkillSpan: inSkillSubgraph,
301
302
  });
@@ -312,6 +313,7 @@ export class ToolNode {
312
313
  content: result.content,
313
314
  ...toolArtifactFields(result),
314
315
  executionTime,
316
+ ...(result.resolvedPath ? { resolvedPath: result.resolvedPath } : {}),
315
317
  ...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
316
318
  ...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
317
319
  ...(skillIdentity?.skillName ? { skillName: skillIdentity.skillName } : {}),
@@ -346,7 +348,7 @@ export class ToolNode {
346
348
  if (result.success) {
347
349
  await recordSkillEntrySpan(state, {
348
350
  toolName: req.toolName,
349
- arguments: req.arguments,
351
+ resolvedPath: result.resolvedPath,
350
352
  skillDirs,
351
353
  });
352
354
  }
@@ -20,8 +20,13 @@ export interface LoadAgentOptions {
20
20
  export declare function resolveAgentSystemPromptFromLoader(loader: {
21
21
  resolveAgentSystemPrompt?: (agentCode: string, version?: string) => Promise<string | undefined>;
22
22
  } | undefined, agentCode: string, version?: string): Promise<string | undefined>;
23
+ export declare function resolveAgentDisplayNameFromLoader(loader: {
24
+ resolveAgentDisplayName?: (agentCode: string, version?: string) => Promise<string | undefined>;
25
+ } | undefined, agentCode: string, version?: string): Promise<string | undefined>;
23
26
  /** manifest.prompts.system;空白视为缺省。 */
24
27
  export declare function extractAgentSystemPrompt(manifest: unknown): string | undefined;
28
+ /** manifest.name;空白视为缺省。身份段对用户说话用,不当路由键。 */
29
+ export declare function extractAgentDisplayName(manifest: unknown): string | undefined;
25
30
  export interface GraphLoaderConfig {
26
31
  registryClient?: AgentRegistryClient;
27
32
  deploymentContext?: DeploymentContext;
@@ -116,6 +121,12 @@ export declare class GraphLoader {
116
121
  * 每轮 execute/stream 注入 GraphState.agentSystemPrompt;不写进节点 config。
117
122
  */
118
123
  resolveAgentSystemPrompt(agentCode: string, version?: string): Promise<string | undefined>;
124
+ /**
125
+ * Agent 展示名(manifest.name)。
126
+ * 每轮 execute/stream 注入 GraphState.agentName;身份段对用户说话用。
127
+ */
128
+ resolveAgentDisplayName(agentCode: string, version?: string): Promise<string | undefined>;
129
+ private fromAgentManifest;
119
130
  /**
120
131
  * 解析 agent tools[] + graph tool 节点 union;
121
132
  * server+预装:以预装 manifest 为准;desktop 合并 ILocalToolInstallStore。
@@ -64,6 +64,11 @@ export async function resolveAgentSystemPromptFromLoader(loader, agentCode, vers
64
64
  return undefined;
65
65
  return loader.resolveAgentSystemPrompt(agentCode, version);
66
66
  }
67
+ export async function resolveAgentDisplayNameFromLoader(loader, agentCode, version) {
68
+ if (!loader || typeof loader.resolveAgentDisplayName !== 'function')
69
+ return undefined;
70
+ return loader.resolveAgentDisplayName(agentCode, version);
71
+ }
67
72
  /** manifest.prompts.system;空白视为缺省。 */
68
73
  export function extractAgentSystemPrompt(manifest) {
69
74
  if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
@@ -77,6 +82,16 @@ export function extractAgentSystemPrompt(manifest) {
77
82
  const trimmed = system.trim();
78
83
  return trimmed ? trimmed : undefined;
79
84
  }
85
+ /** manifest.name;空白视为缺省。身份段对用户说话用,不当路由键。 */
86
+ export function extractAgentDisplayName(manifest) {
87
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest))
88
+ return undefined;
89
+ const name = manifest.name;
90
+ if (typeof name !== 'string')
91
+ return undefined;
92
+ const trimmed = name.trim();
93
+ return trimmed ? trimmed : undefined;
94
+ }
80
95
  function extractSkillPrompt(manifest) {
81
96
  const prompt = manifest.prompt;
82
97
  if (typeof prompt === 'string' && prompt.trim())
@@ -769,18 +784,27 @@ export class GraphLoader {
769
784
  * 每轮 execute/stream 注入 GraphState.agentSystemPrompt;不写进节点 config。
770
785
  */
771
786
  async resolveAgentSystemPrompt(agentCode, version) {
787
+ return this.fromAgentManifest(agentCode, version, extractAgentSystemPrompt);
788
+ }
789
+ /**
790
+ * Agent 展示名(manifest.name)。
791
+ * 每轮 execute/stream 注入 GraphState.agentName;身份段对用户说话用。
792
+ */
793
+ async resolveAgentDisplayName(agentCode, version) {
794
+ return this.fromAgentManifest(agentCode, version, extractAgentDisplayName);
795
+ }
796
+ async fromAgentManifest(agentCode, version, extract) {
772
797
  if (this.deploymentSnapshot) {
773
798
  const snap = await this.deploymentSnapshot.loadAgentSnapshot(agentCode, version);
774
- if (snap) {
775
- return extractAgentSystemPrompt(snap.manifest);
776
- }
799
+ if (snap)
800
+ return extract(snap.manifest);
777
801
  if (this.blockRegistryDefinitions)
778
802
  return undefined;
779
803
  }
780
804
  if (this.blockRegistryDefinitions || !this.registryClient)
781
805
  return undefined;
782
806
  const record = await this.registryClient.getAgent(agentCode);
783
- return extractAgentSystemPrompt(record?.manifest);
807
+ return extract(record?.manifest);
784
808
  }
785
809
  /**
786
810
  * 解析 agent tools[] + graph tool 节点 union;
@@ -11,7 +11,7 @@ import { DEFAULT_GRAPH_TIMEOUT_MS } from '../common/types.js';
11
11
  import { resolveTaskTimeoutMs } from '../task/resolve-task-timeout.js';
12
12
  import { runWithExecutionTrace } from '../graph/execution-trace-context.js';
13
13
  import { HITL_TIMEOUT_ERROR_CODE, HITL_TIMEOUT_MESSAGE, HitlAlreadyTimedOutError, HitlNotWaitingError, buildHitlWaitedPayload, isHitlDeadlinePassed, loadHitlCheckpoint, readPersistedHitlWaiting, unwrapHitlPaused, } from '../graph/hitl/hitl-handler.js';
14
- import { resolveAgentSystemPromptFromLoader } from '../registry/graph-loader.js';
14
+ import { resolveAgentDisplayNameFromLoader, resolveAgentSystemPromptFromLoader, } from '../registry/graph-loader.js';
15
15
  import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
16
16
  import { assembleLoadedTools } from '../tool/harness-primitives.js';
17
17
  import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
@@ -330,6 +330,7 @@ export class AgentRuntime {
330
330
  ? (await this.deps.graphLoader.resolveAgentDir(agentCode, params.agentVersion)) ?? undefined
331
331
  : undefined;
332
332
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, params.agentVersion);
333
+ const agentName = await resolveAgentDisplayNameFromLoader(this.deps.graphLoader, agentCode, params.agentVersion);
333
334
  const facing = await loadModelFacingHistory({
334
335
  sessionHistory: messageRecordsToGraphMessages(sessionContext.messages),
335
336
  checkpointSaver: this.deps.checkpointSaver,
@@ -402,6 +403,7 @@ export class AgentRuntime {
402
403
  promptRoots,
403
404
  agentDir,
404
405
  agentSystemPrompt,
406
+ agentName,
405
407
  skillInput: resolveSkillInputFromInvokeInput(params.input),
406
408
  traceId: resolveTraceId(params),
407
409
  traceCollector: this.deps.traceCollector,
@@ -617,6 +619,9 @@ export class AgentRuntime {
617
619
  agentSystemPrompt: (await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, agentVersion)) ?? (typeof resumedState.agentSystemPrompt === 'string'
618
620
  ? resumedState.agentSystemPrompt
619
621
  : undefined),
622
+ agentName: (await resolveAgentDisplayNameFromLoader(this.deps.graphLoader, agentCode, agentVersion)) ?? (typeof resumedState.agentName === 'string' && resumedState.agentName.trim()
623
+ ? resumedState.agentName.trim()
624
+ : undefined),
620
625
  activatedSkill: resumedState.activatedSkill,
621
626
  skillRoute: resumedState.skillRoute ??
622
627
  (typeof resumedState.activatedSkill === 'string' && resumedState.activatedSkill
@@ -814,6 +819,9 @@ export class AgentRuntime {
814
819
  const agentSystemPrompt = (await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, agentVersion)) ?? (typeof forkedCheckpoint.state.agentSystemPrompt === 'string'
815
820
  ? forkedCheckpoint.state.agentSystemPrompt
816
821
  : undefined);
822
+ const agentName = (await resolveAgentDisplayNameFromLoader(this.deps.graphLoader, agentCode, agentVersion)) ?? (typeof forkedCheckpoint.state.agentName === 'string' && forkedCheckpoint.state.agentName.trim()
823
+ ? forkedCheckpoint.state.agentName.trim()
824
+ : undefined);
817
825
  const abortDeadlineMs = Date.now() + timeout;
818
826
  try {
819
827
  const forkState = { ...forkedCheckpoint.state };
@@ -827,6 +835,7 @@ export class AgentRuntime {
827
835
  agentVersion,
828
836
  agentDir,
829
837
  agentSystemPrompt,
838
+ agentName,
830
839
  userId,
831
840
  delegationToken: overrides.executionContext?.delegationToken,
832
841
  executionContext: overrides.executionContext,
@@ -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 ?? '');
@@ -22,4 +22,4 @@ export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './s
22
22
  export { ToolResultInjector, type ToolMessage, } from './tool-result-injector.js';
23
23
  export { ToolEventEmitter } from './tool-event-emitter.js';
24
24
  export { ToolConfirmationInterceptor } from './tool-confirmation.js';
25
- export { SystemPromptBuilder, naturalTimeWindows, loadedToolsToApiTools, toLoadedSkillMap, filterLoadedToolsForSkill, type PromptBuildContext, type PromptSection, type AssembledSystemPrompt, type ApiToolDefinition, type LoadedSkillInfo, type LoadedSkillMap, } from './system-prompt-builder.js';
25
+ export { SystemPromptBuilder, naturalTimeWindows, buildSkillCatalog, buildToolCatalog, catalogEntryTitle, spokenName, loadedToolsToApiTools, toLoadedSkillMap, filterLoadedToolsForSkill, type PromptBuildContext, type PromptSection, type AssembledSystemPrompt, type ApiToolDefinition, type LoadedSkillInfo, type LoadedSkillMap, } from './system-prompt-builder.js';
@@ -21,4 +21,4 @@ export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './s
21
21
  export { ToolResultInjector, } from './tool-result-injector.js';
22
22
  export { ToolEventEmitter } from './tool-event-emitter.js';
23
23
  export { ToolConfirmationInterceptor } from './tool-confirmation.js';
24
- export { SystemPromptBuilder, naturalTimeWindows, loadedToolsToApiTools, toLoadedSkillMap, filterLoadedToolsForSkill, } from './system-prompt-builder.js';
24
+ export { SystemPromptBuilder, naturalTimeWindows, buildSkillCatalog, buildToolCatalog, catalogEntryTitle, spokenName, loadedToolsToApiTools, toLoadedSkillMap, filterLoadedToolsForSkill, } from './system-prompt-builder.js';
@@ -53,17 +53,19 @@ export declare function mcpToolsetCode(loaded: {
53
53
  execution?: unknown;
54
54
  };
55
55
  } | undefined): string | undefined;
56
- /** Tool Span:工具集 code + 声明版本。与 Skill 的 skillSpace/version 同一假设。 */
56
+ /** Tool Span:工具集 code + 声明版本 + 展示名(与函数名不同才写)。 */
57
57
  export declare function toolTraceIdentity(loaded: {
58
58
  toolId?: string;
59
59
  name?: string;
60
60
  manifest?: {
61
61
  id?: string;
62
+ name?: string;
62
63
  version?: string;
63
64
  };
64
65
  } | undefined, toolName: string): {
65
66
  toolset?: string;
66
67
  toolVersion?: string;
68
+ toolDisplayName?: string;
67
69
  };
68
70
  export interface HydrateMcpToolsAuth {
69
71
  authContext: AuthContext;
@@ -130,13 +130,16 @@ function declaredToolVersion(toolName, version) {
130
130
  return undefined;
131
131
  return raw;
132
132
  }
133
- /** Tool Span:工具集 code + 声明版本。与 Skill 的 skillSpace/version 同一假设。 */
133
+ /** Tool Span:工具集 code + 声明版本 + 展示名(与函数名不同才写)。 */
134
134
  export function toolTraceIdentity(loaded, toolName) {
135
135
  const toolset = toolsetCodeFromLoaded(loaded);
136
136
  const toolVersion = declaredToolVersion(toolName, loaded?.manifest?.version);
137
+ const display = loaded?.manifest?.name?.trim();
138
+ const toolDisplayName = display && display !== toolName ? display : undefined;
137
139
  return {
138
140
  ...(toolset ? { toolset } : {}),
139
141
  ...(toolVersion ? { toolVersion } : {}),
142
+ ...(toolDisplayName ? { toolDisplayName } : {}),
140
143
  };
141
144
  }
142
145
  /**
@@ -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
+ }
@@ -7,8 +7,8 @@
7
7
  * ```
8
8
  * [平台规则常量] ← 代码内置,安全约束 + 行为准则(最高优先级)
9
9
  * [当前时间] ← 运行时动态注入(自然周等时间规则的判定基准)
10
- * [Agent 身份] ← agentName / agentCode(目录身份,不是 prompts.system)
11
- * [可用工具短目录] ← 名称 + 一句话用途;完整 schema 仍走 API tools
10
+ * [Agent 身份] ← 展示名 agentName,缺省 agentCode(对用户说话,不是路由键)
11
+ * [可用工具短目录] ← 展示名 + 调用名 + 一句话用途;完整 schema 仍走 API tools
12
12
  * [Agent 基线] ← agent.manifest.json prompts.system;非 silent LLM
13
13
  * [节点提示词] ← Graph LLM 节点 config.systemPrompt / systemPromptFile
14
14
  * ```
@@ -109,6 +109,10 @@ export interface AssembledSystemPrompt {
109
109
  text: string;
110
110
  sections: PromptSection[];
111
111
  }
112
+ /** 对人展示的名称。有展示名用展示名,否则用编码。 */
113
+ export declare function spokenName(displayName: string | undefined, code: string | undefined): string | undefined;
114
+ /** 短目录标题:展示名在前,调用键在行内。名称与编码相同则只写编码。 */
115
+ export declare function catalogEntryTitle(code: string, displayName: string | undefined, codePhrase: string): string;
112
116
  /**
113
117
  * 非 silent LLM 才带 Agent 基线;节点提示词空则省略。
114
118
  * silent 只保留节点增量(选技能 / JSON)。
@@ -166,4 +170,6 @@ export type ApiToolDefinition = LLMToolDefinition;
166
170
  */
167
171
  /** 名称 + 一句话用途,供 system「可用工具」段。schema 仍走 loadedToolsToApiTools。 */
168
172
  export declare function buildToolCatalog(loadedTools: LoadedToolMap | undefined): string | undefined;
173
+ /** 展示名 + skillId,供 system「可用技能」段。read_file 仍用 skillId。 */
174
+ export declare function buildSkillCatalog(defs: LoadedSkillMap | undefined): string | undefined;
169
175
  export declare function loadedToolsToApiTools(loadedTools: LoadedToolMap | undefined, skillTools?: string[]): ApiToolDefinition[];
@@ -7,8 +7,8 @@
7
7
  * ```
8
8
  * [平台规则常量] ← 代码内置,安全约束 + 行为准则(最高优先级)
9
9
  * [当前时间] ← 运行时动态注入(自然周等时间规则的判定基准)
10
- * [Agent 身份] ← agentName / agentCode(目录身份,不是 prompts.system)
11
- * [可用工具短目录] ← 名称 + 一句话用途;完整 schema 仍走 API tools
10
+ * [Agent 身份] ← 展示名 agentName,缺省 agentCode(对用户说话,不是路由键)
11
+ * [可用工具短目录] ← 展示名 + 调用名 + 一句话用途;完整 schema 仍走 API tools
12
12
  * [Agent 基线] ← agent.manifest.json prompts.system;非 silent LLM
13
13
  * [节点提示词] ← Graph LLM 节点 config.systemPrompt / systemPromptFile
14
14
  * ```
@@ -86,6 +86,22 @@ function trimPrompt(value) {
86
86
  const trimmed = value.trim();
87
87
  return trimmed ? trimmed : undefined;
88
88
  }
89
+ /** 对人展示的名称。有展示名用展示名,否则用编码。 */
90
+ export function spokenName(displayName, code) {
91
+ const name = displayName?.trim();
92
+ if (name)
93
+ return name;
94
+ const id = code?.trim();
95
+ return id || undefined;
96
+ }
97
+ /** 短目录标题:展示名在前,调用键在行内。名称与编码相同则只写编码。 */
98
+ export function catalogEntryTitle(code, displayName, codePhrase) {
99
+ const id = code.trim();
100
+ const name = displayName?.trim();
101
+ if (name && name !== id)
102
+ return `**${name}**(${codePhrase})`;
103
+ return `**${id}**`;
104
+ }
89
105
  /**
90
106
  * 非 silent LLM 才带 Agent 基线;节点提示词空则省略。
91
107
  * silent 只保留节点增量(选技能 / JSON)。
@@ -154,7 +170,7 @@ export class SystemPromptBuilder {
154
170
  return lines.join('\n');
155
171
  }
156
172
  buildAgentIdentity(context) {
157
- const name = context.agentName ?? context.agentCode;
173
+ const name = spokenName(context.agentName, context.agentCode);
158
174
  return name ? `## Agent 身份\n你的名称是「${name}」。` : '';
159
175
  }
160
176
  buildJail(context) {
@@ -262,11 +278,8 @@ export function buildToolCatalog(loadedTools) {
262
278
  return undefined;
263
279
  const rows = Object.values(loadedTools).map((tool) => {
264
280
  const desc = (tool.manifest.description ?? '').replace(/\s+/g, ' ').trim();
265
- const displayName = tool.manifest.name?.trim();
266
- const title = displayName && displayName !== tool.name
267
- ? `${tool.name}(${displayName})`
268
- : tool.name;
269
- return `- **${title}**${desc ? `:${desc}` : ''}`;
281
+ const title = catalogEntryTitle(tool.name, tool.manifest.name, `调用名 ${tool.name}`);
282
+ return `- ${title}${desc ? `:${desc}` : ''}`;
270
283
  });
271
284
  if (rows.length === 0)
272
285
  return undefined;
@@ -276,6 +289,21 @@ export function buildToolCatalog(loadedTools) {
276
289
  rows.join('\n'),
277
290
  ].join('\n');
278
291
  }
292
+ /** 展示名 + skillId,供 system「可用技能」段。read_file 仍用 skillId。 */
293
+ export function buildSkillCatalog(defs) {
294
+ if (!defs)
295
+ return undefined;
296
+ const rows = Object.values(defs).map((skill) => {
297
+ const desc = (skill.description ?? '').replace(/\s+/g, ' ').trim();
298
+ const entry = skill.entryFile ?? (skill.bundlePath ? `${skill.bundlePath}/SKILL.md` : '');
299
+ const title = catalogEntryTitle(skill.skillId, skill.name, `skillId ${skill.skillId}`);
300
+ const pathLine = entry ? `\n SKILL.md: ${entry}` : '';
301
+ return `- ${title}${desc ? `:${desc}` : ''}${pathLine}`;
302
+ });
303
+ if (rows.length === 0)
304
+ return undefined;
305
+ return `# 可用技能\n要用某个技能时,调用 read_file 读取其 SKILL.md。脚本和参考文档默认在同一目录。\n${rows.join('\n')}`;
306
+ }
279
307
  export function loadedToolsToApiTools(loadedTools, skillTools) {
280
308
  if (!loadedTools)
281
309
  return [];
@@ -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.33",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",