@gt-fe/eap-runtime 0.0.19 → 0.0.21

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.
Files changed (131) hide show
  1. package/README.md +3 -2
  2. package/dist/api/handlers/invoke.handler.d.ts +7 -0
  3. package/dist/api/handlers/invoke.handler.js +57 -24
  4. package/dist/api/handlers/stream.handler.d.ts +6 -0
  5. package/dist/api/handlers/stream.handler.js +59 -39
  6. package/dist/api/routes/agent.routes.d.ts +8 -1
  7. package/dist/common/types.d.ts +2 -2
  8. package/dist/common/types.js +1 -1
  9. package/dist/contracts/index.d.ts +1 -1
  10. package/dist/contracts/trace-collector.d.ts +16 -8
  11. package/dist/execution/index.d.ts +2 -0
  12. package/dist/execution/index.js +2 -0
  13. package/dist/execution/interceptors.d.ts +40 -0
  14. package/dist/execution/interceptors.js +23 -0
  15. package/dist/execution/ports.d.ts +18 -0
  16. package/dist/execution/ports.js +13 -0
  17. package/dist/governance/audit-reporter.d.ts +14 -0
  18. package/dist/governance/audit-reporter.js +18 -0
  19. package/dist/governance/index.d.ts +2 -0
  20. package/dist/governance/index.js +1 -0
  21. package/dist/governance/permission-guard.d.ts +6 -2
  22. package/dist/governance/permission-guard.js +38 -5
  23. package/dist/graph/adapters/graph-nesting.d.ts +12 -0
  24. package/dist/graph/adapters/graph-nesting.js +29 -0
  25. package/dist/graph/adapters/index.d.ts +2 -0
  26. package/dist/graph/adapters/index.js +2 -0
  27. package/dist/graph/adapters/langgraph.adapter.d.ts +26 -11
  28. package/dist/graph/adapters/langgraph.adapter.js +178 -83
  29. package/dist/graph/adapters/skill-entry-span.d.ts +37 -0
  30. package/dist/graph/adapters/skill-entry-span.js +78 -0
  31. package/dist/graph/adapters/skill-isolate.d.ts +17 -0
  32. package/dist/graph/adapters/skill-isolate.js +50 -0
  33. package/dist/graph/adapters/skill-read.d.ts +10 -0
  34. package/dist/graph/adapters/skill-read.js +33 -0
  35. package/dist/graph/adapters/subgraph-compiler.d.ts +7 -2
  36. package/dist/graph/adapters/subgraph-compiler.js +81 -35
  37. package/dist/graph/contracts/executable-graph.d.ts +27 -5
  38. package/dist/graph/hitl/hitl-handler.js +2 -0
  39. package/dist/graph/nodes/index.d.ts +1 -0
  40. package/dist/graph/nodes/index.js +1 -0
  41. package/dist/graph/nodes/llm-node.d.ts +3 -1
  42. package/dist/graph/nodes/llm-node.js +26 -2
  43. package/dist/graph/nodes/rag/dedup.d.ts +20 -0
  44. package/dist/graph/nodes/rag/dedup.js +77 -0
  45. package/dist/graph/nodes/rag/denoise.d.ts +58 -0
  46. package/dist/graph/nodes/rag/denoise.js +220 -0
  47. package/dist/graph/nodes/rag/fusion/fuse.d.ts +11 -0
  48. package/dist/graph/nodes/rag/fusion/fuse.js +25 -0
  49. package/dist/graph/nodes/rag/fusion/normalize.d.ts +9 -0
  50. package/dist/graph/nodes/rag/fusion/normalize.js +135 -0
  51. package/dist/graph/nodes/rag/index.d.ts +18 -0
  52. package/dist/graph/nodes/rag/index.js +17 -0
  53. package/dist/graph/nodes/rag/prompts.d.ts +12 -0
  54. package/dist/graph/nodes/rag/prompts.js +40 -0
  55. package/dist/graph/nodes/rag/rag-node.d.ts +21 -0
  56. package/dist/graph/nodes/rag/rag-node.js +135 -0
  57. package/dist/graph/nodes/rag/rag-search-tool.d.ts +48 -0
  58. package/dist/graph/nodes/rag/rag-search-tool.js +230 -0
  59. package/dist/graph/nodes/rag/react-loop.d.ts +57 -0
  60. package/dist/graph/nodes/rag/react-loop.js +467 -0
  61. package/dist/graph/nodes/rag/retrievers/http-embedder.d.ts +29 -0
  62. package/dist/graph/nodes/rag/retrievers/http-embedder.js +66 -0
  63. package/dist/graph/nodes/rag/retrievers/in-memory-retriever.d.ts +17 -0
  64. package/dist/graph/nodes/rag/retrievers/in-memory-retriever.js +18 -0
  65. package/dist/graph/nodes/rag/retrievers/local-sqlite-retriever.d.ts +46 -0
  66. package/dist/graph/nodes/rag/retrievers/local-sqlite-retriever.js +85 -0
  67. package/dist/graph/nodes/rag/tools.d.ts +90 -0
  68. package/dist/graph/nodes/rag/tools.js +260 -0
  69. package/dist/graph/nodes/rag/types.d.ts +226 -0
  70. package/dist/graph/nodes/rag/types.js +9 -0
  71. package/dist/graph/nodes/tool-node.d.ts +25 -1
  72. package/dist/graph/nodes/tool-node.js +193 -35
  73. package/dist/graph/skill-audit-recorder.d.ts +4 -4
  74. package/dist/graph/skill-audit-recorder.js +3 -3
  75. package/dist/graph/trace-span.d.ts +2 -9
  76. package/dist/graph/trace-span.js +1 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/index.js +1 -0
  79. package/dist/memory/semantic-retrieval.service.d.ts +1 -1
  80. package/dist/memory/semantic-retrieval.service.js +13 -23
  81. package/dist/providers/index.d.ts +1 -1
  82. package/dist/providers/index.js +1 -1
  83. package/dist/providers/routing-llm-provider.d.ts +5 -0
  84. package/dist/providers/routing-llm-provider.js +66 -1
  85. package/dist/runtime/agent-runtime.d.ts +5 -0
  86. package/dist/runtime/agent-runtime.js +105 -63
  87. package/dist/runtime/dev-rag-deps.d.ts +21 -0
  88. package/dist/runtime/dev-rag-deps.js +186 -0
  89. package/dist/runtime/dev-runtime.d.ts +16 -1
  90. package/dist/runtime/dev-runtime.js +59 -5
  91. package/dist/runtime/index.d.ts +1 -1
  92. package/dist/runtime/types.d.ts +10 -9
  93. package/dist/session/apply-session-compression.d.ts +81 -0
  94. package/dist/session/apply-session-compression.js +262 -0
  95. package/dist/session/apply-session-memory.d.ts +21 -0
  96. package/dist/session/apply-session-memory.js +48 -0
  97. package/dist/session/index.d.ts +2 -0
  98. package/dist/session/index.js +2 -0
  99. package/dist/session/persist-turn-messages.d.ts +51 -0
  100. package/dist/session/persist-turn-messages.js +140 -3
  101. package/dist/sse/events/index.d.ts +13 -4
  102. package/dist/sse/events/index.js +9 -1
  103. package/dist/sse/events/rag-events.d.ts +63 -0
  104. package/dist/sse/events/rag-events.js +9 -0
  105. package/dist/sse/events/session-events.d.ts +38 -0
  106. package/dist/sse/events/session-events.js +4 -0
  107. package/dist/sse/trace-collector.d.ts +2 -9
  108. package/dist/token/index.d.ts +3 -0
  109. package/dist/token/index.js +2 -0
  110. package/dist/token/record-task-token-usage.d.ts +23 -0
  111. package/dist/token/record-task-token-usage.js +76 -0
  112. package/dist/token/token-detail-mode.d.ts +4 -4
  113. package/dist/token/token-detail-mode.js +4 -4
  114. package/dist/token/token-detail.d.ts +8 -0
  115. package/dist/token/token-detail.js +22 -0
  116. package/dist/token/token-stats.service.d.ts +4 -2
  117. package/dist/token/token-stats.service.js +5 -1
  118. package/dist/tool/create-tool-executor.d.ts +6 -0
  119. package/dist/tool/create-tool-executor.js +4 -3
  120. package/dist/tool/harness-primitives.d.ts +9 -1
  121. package/dist/tool/harness-primitives.js +43 -12
  122. package/dist/tool/index.d.ts +3 -2
  123. package/dist/tool/index.js +3 -2
  124. package/dist/tool/mcp-list.d.ts +12 -0
  125. package/dist/tool/mcp-list.js +15 -0
  126. package/dist/tool/tool-executor.interface.d.ts +27 -0
  127. package/dist/tool/tool-orchestrator.d.ts +3 -1
  128. package/dist/tool/tool-orchestrator.js +15 -6
  129. package/dist/tool/tool-retry.d.ts +15 -0
  130. package/dist/tool/tool-retry.js +69 -0
  131. package/package.json +2 -2
@@ -9,11 +9,13 @@
9
9
  * @see design.md §1.4 ToolNode: LangGraph的ToolNode封装
10
10
  * @see requirements.md Requirement 8 AC1, AC3, AC5
11
11
  */
12
+ import type { NodeRetryConfig } from '../contracts/executable-graph.js';
12
13
  import type { AuthContext } from '../../tool/tool-executor.interface.js';
13
14
  import type { ToolOrchestrator } from '../../tool/tool-orchestrator.js';
14
15
  import type { ToolResultInjector } from '../../tool/tool-result-injector.js';
15
16
  import type { SkillDirRef } from '../adapters/skill-read.js';
16
17
  import type { HitlHandler } from '../hitl/hitl-handler.js';
18
+ import { type ExecutionInterceptors } from '../../execution/interceptors.js';
17
19
  /**
18
20
  * LangGraphState — LangGraph 内部状态(与 LLMNode 对齐)
19
21
  */
@@ -43,6 +45,14 @@ export interface ToolNodeProgress {
43
45
  node?: string;
44
46
  skillId?: string;
45
47
  skillDirs?: SkillDirRef[];
48
+ /** 已在 Skill 子图内(state.skillSpanId);读 SKILL.md 不再发 skill.started */
49
+ inSkillSubgraph?: boolean;
50
+ skillSpace?: 'agent' | 'user' | 'local';
51
+ skillVersion?: string;
52
+ skillName?: string;
53
+ toolVersion?: string;
54
+ /** 本失败后是否还会再试。缺省 false */
55
+ willRetry?: boolean;
46
56
  }
47
57
  export declare class ToolNode {
48
58
  private readonly orchestrator;
@@ -51,7 +61,20 @@ export declare class ToolNode {
51
61
  private readonly onProgress?;
52
62
  private readonly hitlHandler?;
53
63
  private readonly nodeId?;
54
- constructor(orchestrator: ToolOrchestrator, injector: ToolResultInjector, defaultAuthContext: AuthContext, onProgress?: ((data: ToolNodeProgress) => void) | undefined, hitlHandler?: HitlHandler | undefined, nodeId?: string | undefined);
64
+ private readonly interceptors?;
65
+ /** 图声明的 rag 检索面(config.retrievalSources,工具形态声明位)——随请求转递给 rag_search 装饰器 */
66
+ private readonly ragSourcesConfig?;
67
+ /** 图声明的融合总条数(config.totalK,工具形态声明位)——同路转递 */
68
+ private readonly ragTotalK?;
69
+ /** 节点 `retry`;未设则只试一次 */
70
+ private readonly retry?;
71
+ constructor(orchestrator: ToolOrchestrator, injector: ToolResultInjector, defaultAuthContext: AuthContext, onProgress?: ((data: ToolNodeProgress) => void) | undefined, hitlHandler?: HitlHandler | undefined, nodeId?: string | undefined, interceptors?: ExecutionInterceptors | undefined,
72
+ /** 图声明的 rag 检索面(config.retrievalSources,工具形态声明位)——随请求转递给 rag_search 装饰器 */
73
+ ragSourcesConfig?: import("../nodes/rag/types.js").RagRetrievalSources | undefined,
74
+ /** 图声明的融合总条数(config.totalK,工具形态声明位)——同路转递 */
75
+ ragTotalK?: number | undefined,
76
+ /** 节点 `retry`;未设则只试一次 */
77
+ retry?: NodeRetryConfig | undefined);
55
78
  /**
56
79
  * execute — StateGraph 节点函数
57
80
  *
@@ -65,6 +88,7 @@ export declare class ToolNode {
65
88
  * 从 state 中提取 messages 数组
66
89
  */
67
90
  private extractMessages;
91
+ private executeSingleWithRetry;
68
92
  /**
69
93
  * 从 messages 中找到最后一条 assistant 消息
70
94
  */
@@ -9,13 +9,19 @@
9
9
  * @see design.md §1.4 ToolNode: LangGraph的ToolNode封装
10
10
  * @see requirements.md Requirement 8 AC1, AC3, AC5
11
11
  */
12
+ import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
12
13
  import { resolveJailFromState } from '../../tool/harness-primitives.js';
14
+ import { recordSkillEntrySpan, skillEntryInvocationFromTool, skillTraceIdentity } from '../adapters/skill-entry-span.js';
13
15
  import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
14
16
  import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
15
17
  import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
16
18
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
17
- import { toolsetCodeFromLoaded } from '../../tool/mcp-list.js';
19
+ import { toolTraceIdentity } from '../../tool/mcp-list.js';
18
20
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
21
+ import { emitRuntimeLog } from '../../common/logger.js';
22
+ import { decideBeforeTool, } from '../../execution/interceptors.js';
23
+ import { isRetryableToolFailure, resolveToolRetryConfig, sleepToolRetry, toolRetryDelayMs, } from '../../tool/tool-retry.js';
24
+ import { assertIsolatedSkillAllowsHitl, isIsolatedSkillState } from '../adapters/skill-isolate.js';
19
25
  function skillDirsFromState(state) {
20
26
  const defs = state['skillDefinitions'];
21
27
  return Object.values(defs ?? {})
@@ -29,13 +35,27 @@ export class ToolNode {
29
35
  onProgress;
30
36
  hitlHandler;
31
37
  nodeId;
32
- constructor(orchestrator, injector, defaultAuthContext, onProgress, hitlHandler, nodeId) {
38
+ interceptors;
39
+ ragSourcesConfig;
40
+ ragTotalK;
41
+ retry;
42
+ constructor(orchestrator, injector, defaultAuthContext, onProgress, hitlHandler, nodeId, interceptors,
43
+ /** 图声明的 rag 检索面(config.retrievalSources,工具形态声明位)——随请求转递给 rag_search 装饰器 */
44
+ ragSourcesConfig,
45
+ /** 图声明的融合总条数(config.totalK,工具形态声明位)——同路转递 */
46
+ ragTotalK,
47
+ /** 节点 `retry`;未设则只试一次 */
48
+ retry) {
33
49
  this.orchestrator = orchestrator;
34
50
  this.injector = injector;
35
51
  this.defaultAuthContext = defaultAuthContext;
36
52
  this.onProgress = onProgress;
37
53
  this.hitlHandler = hitlHandler;
38
54
  this.nodeId = nodeId;
55
+ this.interceptors = interceptors;
56
+ this.ragSourcesConfig = ragSourcesConfig;
57
+ this.ragTotalK = ragTotalK;
58
+ this.retry = retry;
39
59
  }
40
60
  /**
41
61
  * execute — StateGraph 节点函数
@@ -99,6 +119,12 @@ export class ToolNode {
99
119
  ...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
100
120
  ...(jailExtraRoots.length > 0 ? { jailExtraRoots } : {}),
101
121
  ...(skillDir ? { skillDir } : {}),
122
+ // rag 会话级控制与工具表快照(rag_search 装饰器消费;普通工具执行器忽略)
123
+ ...(state['ragSources'] ? { ragSources: state['ragSources'] } : {}),
124
+ ...(state['ragScope'] ? { ragScope: state['ragScope'] } : {}),
125
+ ...(this.ragSourcesConfig ? { ragSourcesConfig: this.ragSourcesConfig } : {}),
126
+ ...(this.ragTotalK !== undefined ? { ragTotalK: this.ragTotalK } : {}),
127
+ ...(loadedTools ? { loadedTools } : {}),
102
128
  };
103
129
  });
104
130
  const executionContext = state['executionContext'];
@@ -109,22 +135,48 @@ export class ToolNode {
109
135
  const skillId = typeof state['activatedSkill'] === 'string' && state['activatedSkill']
110
136
  ? state['activatedSkill']
111
137
  : undefined;
138
+ const inSkillSubgraph = typeof state['skillSpanId'] === 'string' && Boolean(state['skillSpanId']);
112
139
  const emitProgress = (data) => {
113
140
  this.onProgress?.({
114
141
  ...data,
115
142
  node: nodeId,
116
143
  ...(skillId ? { skillId } : {}),
117
144
  ...(skillDirs.length > 0 ? { skillDirs } : {}),
145
+ ...(inSkillSubgraph ? { inSkillSubgraph: true } : {}),
118
146
  });
119
147
  };
148
+ // require_confirmation 工具走 HITL 拦截:改为顺序执行,thread_id 与 Checkpoint 对齐
149
+ const confirmationTools = new Set(Object.entries(loadedTools ?? {})
150
+ .filter(([, def]) => Boolean(def?.manifest.requireConfirmation))
151
+ .map(([name]) => name));
152
+ const needsHitl = Boolean(this.hitlHandler) &&
153
+ (requests.some((req) => confirmationTools.has(req.toolName)) ||
154
+ Boolean(this.interceptors?.beforeTool));
155
+ const resumingHitl = isHitlDecision(state['hitlChoice']);
156
+ const resumeToolSpanId = resumingHitl && typeof state['toolSpanId'] === 'string' && state['toolSpanId']
157
+ ? state['toolSpanId']
158
+ : undefined;
159
+ const resumeToolCallId = typeof state['hitlToolCallId'] === 'string' && state['hitlToolCallId']
160
+ ? state['hitlToolCallId']
161
+ : undefined;
162
+ const confirmationCount = requests.filter((req) => confirmationTools.has(req.toolName)).length;
120
163
  // 5. 并行执行所有 Tool 调用(附 Trace span)
121
164
  const spanIds = await Promise.all(requests.map(async (req) => {
165
+ const reusePausedSpan = Boolean(resumeToolSpanId) &&
166
+ ((resumeToolCallId && req.toolCallId === resumeToolCallId) ||
167
+ (!resumeToolCallId &&
168
+ confirmationTools.has(req.toolName) &&
169
+ confirmationCount === 1));
170
+ if (reusePausedSpan) {
171
+ return resumeToolSpanId ?? null;
172
+ }
122
173
  if (!traceCollector || !traceId)
123
174
  return null;
124
175
  try {
125
176
  const parentNodeId = typeof state['skillSpanId'] === 'string' && state['skillSpanId']
126
177
  ? state['skillSpanId']
127
178
  : undefined;
179
+ const identity = toolTraceIdentity(loadedTools?.[req.toolName], req.toolName);
128
180
  return await traceCollector.createNode({
129
181
  traceId,
130
182
  name: req.toolName,
@@ -132,6 +184,7 @@ export class ToolNode {
132
184
  parentNodeId,
133
185
  toolRuntime: req.toolRuntime === 'both' ? 'both' : req.toolRuntime,
134
186
  agentCode: agentCode || undefined,
187
+ ...(identity.toolVersion ? { version: identity.toolVersion } : {}),
135
188
  input: req.arguments,
136
189
  metadata: {
137
190
  toolCallId: req.toolCallId,
@@ -139,6 +192,7 @@ export class ToolNode {
139
192
  taskId: req.taskId,
140
193
  correlationId: req.correlationId,
141
194
  skillId,
195
+ ...(identity.toolset ? { toolset: identity.toolset } : {}),
142
196
  },
143
197
  });
144
198
  }
@@ -147,21 +201,15 @@ export class ToolNode {
147
201
  return null;
148
202
  }
149
203
  }));
150
- // 并行执行,但各自独立计时(避免共用 startedAt 导致时长不准/看起来像“写死”)
151
- // require_confirmation 工具走 HITL 拦截:改为顺序执行,thread_id 与 Checkpoint 对齐
152
- const confirmationTools = new Set(Object.entries(loadedTools ?? {})
153
- .filter(([, def]) => Boolean(def?.manifest.requireConfirmation))
154
- .map(([name]) => name));
155
- const needsHitl = Boolean(this.hitlHandler) &&
156
- requests.some((req) => confirmationTools.has(req.toolName));
157
204
  const runOne = async (req, i) => {
158
- const toolset = toolsetCodeFromLoaded(loadedTools?.[req.toolName]);
159
- const resumingHitl = isHitlDecision(state['hitlChoice']);
205
+ const toolIdentity = toolTraceIdentity(loadedTools?.[req.toolName], req.toolName);
206
+ const toolset = toolIdentity.toolset;
160
207
  if (!resumingHitl) {
161
208
  emitProgress({
162
209
  toolCallId: req.toolCallId,
163
210
  toolName: req.toolName,
164
211
  ...(toolset ? { toolset } : {}),
212
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
165
213
  status: 'started',
166
214
  arguments: req.arguments,
167
215
  });
@@ -169,43 +217,97 @@ export class ToolNode {
169
217
  const startedAt = Date.now();
170
218
  try {
171
219
  let result;
172
- if (needsHitl && this.hitlHandler && confirmationTools.has(req.toolName)) {
173
- const interceptor = new ToolConfirmationInterceptor(confirmationTools, this.hitlHandler, {
174
- execute: (r) => this.orchestrator.executeSingle(r, executionContext),
175
- });
176
- const graphState = this.toGraphState(state);
177
- const threadId = resolveCheckpointThreadId({
178
- sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
179
- taskId: req.taskId,
180
- });
181
- const confirmed = await interceptor.executeWithConfirmation(req, graphState, req.taskId ?? String(state['taskId'] ?? req.toolCallId), threadId, {
182
- toolSpanId: spanIds[i] ?? undefined,
183
- skillSpanId: typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined,
184
- });
185
- result =
186
- confirmed ??
187
- {
188
- toolCallId: req.toolCallId,
189
- toolName: req.toolName,
190
- content: `用户拒绝执行工具 ${req.toolName}`,
191
- success: false,
192
- error: `用户拒绝执行工具 ${req.toolName}`,
193
- errorCode: 'hitl_rejected',
194
- };
220
+ const gate = await decideBeforeTool(this.interceptors?.beforeTool, req, executionContext);
221
+ if (gate.action === 'deny') {
222
+ result = {
223
+ toolCallId: req.toolCallId,
224
+ toolName: req.toolName,
225
+ content: '',
226
+ success: false,
227
+ error: gate.reason,
228
+ errorCode: 'interceptor_denied',
229
+ };
195
230
  }
196
231
  else {
197
- result = await this.orchestrator.executeSingle(req, executionContext);
232
+ const askHitl = gate.action === 'ask' || (needsHitl && confirmationTools.has(req.toolName));
233
+ if (askHitl && this.hitlHandler) {
234
+ // 隔离子图未按独立 HITL 线程设计;interrupt 前拒绝,避免写错父 Checkpoint。
235
+ if (!resumingHitl) {
236
+ assertIsolatedSkillAllowsHitl(state);
237
+ }
238
+ const confirmSet = new Set(confirmationTools);
239
+ if (gate.action === 'ask')
240
+ confirmSet.add(req.toolName);
241
+ const interceptor = new ToolConfirmationInterceptor(confirmSet, this.hitlHandler, {
242
+ execute: (r) => this.orchestrator.executeSingle(r, executionContext),
243
+ });
244
+ const graphState = this.toGraphState(state);
245
+ const threadId = resolveCheckpointThreadId({
246
+ sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
247
+ taskId: req.taskId,
248
+ });
249
+ const confirmed = await interceptor.executeWithConfirmation(req, graphState, req.taskId ?? String(state['taskId'] ?? req.toolCallId), threadId, {
250
+ toolSpanId: spanIds[i] ?? undefined,
251
+ skillSpanId: typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined,
252
+ });
253
+ result =
254
+ confirmed ??
255
+ {
256
+ toolCallId: req.toolCallId,
257
+ toolName: req.toolName,
258
+ content: `用户拒绝执行工具 ${req.toolName}`,
259
+ success: false,
260
+ error: `用户拒绝执行工具 ${req.toolName}`,
261
+ errorCode: 'hitl_rejected',
262
+ };
263
+ }
264
+ else if (gate.action === 'ask') {
265
+ result = {
266
+ toolCallId: req.toolCallId,
267
+ toolName: req.toolName,
268
+ content: '',
269
+ success: false,
270
+ error: '需要人工确认',
271
+ errorCode: 'interceptor_ask',
272
+ };
273
+ }
274
+ else {
275
+ result = await this.executeSingleWithRetry(req, executionContext, emitProgress, toolset, toolIdentity);
276
+ }
277
+ }
278
+ try {
279
+ await this.interceptors?.afterTool?.(req, result);
280
+ }
281
+ catch (err) {
282
+ emitRuntimeLog('warn', 'runtime.intercept', 'afterTool failed', {
283
+ event: 'runtime.intercept.after_tool_failed',
284
+ toolName: req.toolName,
285
+ error: err instanceof Error ? err.message : String(err),
286
+ });
198
287
  }
199
288
  const executionTime = Date.now() - startedAt;
200
289
  if (result.success) {
290
+ const catalog = skillEntryInvocationFromTool({
291
+ toolName: req.toolName,
292
+ arguments: req.arguments,
293
+ skillDirs,
294
+ alreadyInSkillSpan: inSkillSubgraph,
295
+ });
296
+ const skillIdentity = catalog
297
+ ? skillTraceIdentity(state, catalog.skillId, catalog.path)
298
+ : undefined;
201
299
  emitProgress({
202
300
  toolCallId: result.toolCallId,
203
301
  toolName: result.toolName,
204
302
  ...(toolset ? { toolset } : {}),
303
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
205
304
  status: 'completed',
206
305
  arguments: req.arguments,
207
306
  content: result.content,
208
307
  executionTime,
308
+ ...(skillIdentity?.skillSpace ? { skillSpace: skillIdentity.skillSpace } : {}),
309
+ ...(skillIdentity?.skillVersion ? { skillVersion: skillIdentity.skillVersion } : {}),
310
+ ...(skillIdentity?.skillName ? { skillName: skillIdentity.skillName } : {}),
209
311
  });
210
312
  }
211
313
  else {
@@ -213,6 +315,7 @@ export class ToolNode {
213
315
  toolCallId: result.toolCallId,
214
316
  toolName: result.toolName,
215
317
  ...(toolset ? { toolset } : {}),
318
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
216
319
  status: 'error',
217
320
  arguments: req.arguments,
218
321
  error: result.error ?? 'unknown error',
@@ -232,6 +335,13 @@ export class ToolNode {
232
335
  warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: nodeId }, err);
233
336
  }
234
337
  }
338
+ if (result.success) {
339
+ await recordSkillEntrySpan(state, {
340
+ toolName: req.toolName,
341
+ arguments: req.arguments,
342
+ skillDirs,
343
+ });
344
+ }
235
345
  return result;
236
346
  }
237
347
  catch (err) {
@@ -245,6 +355,23 @@ export class ToolNode {
245
355
  (typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined);
246
356
  throw hitlPaused;
247
357
  }
358
+ if (err instanceof AppError &&
359
+ err.code === ErrorCodes.GRAPH_EXECUTION_ERROR &&
360
+ isIsolatedSkillState(state)) {
361
+ const failedSpanId = spanIds[i];
362
+ if (traceCollector && failedSpanId) {
363
+ try {
364
+ await traceCollector.completeNode(failedSpanId, {
365
+ status: 'failed',
366
+ error: err.message,
367
+ });
368
+ }
369
+ catch (completeErr) {
370
+ warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: failedSpanId }, completeErr);
371
+ }
372
+ }
373
+ throw err;
374
+ }
248
375
  const executionTime = Date.now() - startedAt;
249
376
  const message = err instanceof Error ? err.message : String(err);
250
377
  emitProgress({
@@ -333,6 +460,37 @@ export class ToolNode {
333
460
  return { role: 'user', content: String(msg) };
334
461
  });
335
462
  }
463
+ async executeSingleWithRetry(req, executionContext, emitProgress, toolset, toolIdentity) {
464
+ const retry = resolveToolRetryConfig(this.retry);
465
+ const maxRetries = retry?.maxRetries ?? 0;
466
+ let last;
467
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
468
+ last = await this.orchestrator.executeSingle(req, executionContext);
469
+ if (last.success)
470
+ return last;
471
+ const willRetry = attempt < maxRetries && isRetryableToolFailure(last);
472
+ if (!willRetry)
473
+ return last;
474
+ emitProgress({
475
+ toolCallId: req.toolCallId,
476
+ toolName: req.toolName,
477
+ ...(toolset ? { toolset } : {}),
478
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
479
+ status: 'error',
480
+ arguments: req.arguments,
481
+ error: last.error ?? 'unknown error',
482
+ willRetry: true,
483
+ });
484
+ await sleepToolRetry(toolRetryDelayMs(attempt, retry.backoff));
485
+ }
486
+ return last ?? {
487
+ toolCallId: req.toolCallId,
488
+ toolName: req.toolName,
489
+ content: '',
490
+ success: false,
491
+ error: 'unknown error',
492
+ };
493
+ }
336
494
  /**
337
495
  * 从 messages 中找到最后一条 assistant 消息
338
496
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * SkillAuditRecorder — Skill 调用台账(RT-6)
2
+ * SkillAuditRecorder — 进程内 Skill 调用记录(调试用)
3
3
  *
4
- * 记录每次 Skill(子图)激活的审计明细,供后续分析与看板使用。
5
- * Runtime 负责写入;Gateway 看不到 Skill 调用。
4
+ * 生产记录是 Trace 上的 skill Span(子图激活,或成功读 SKILL.md)。
5
+ * 本记录器默认内存、不对外查询;分析从 Span 聚合,不读这里。
6
6
  *
7
7
  * @see docs/status/module-status-and-backlog.md RT-6
8
8
  */
@@ -48,7 +48,7 @@ export interface SkillInvocationRecord {
48
48
  /**
49
49
  * ISkillAuditStore — Skill 审计存储接口
50
50
  *
51
- * 默认实现为内存存储;生产环境应接入 PG 或外部审计服务。
51
+ * 默认内存存储。生产不依赖本接口;Skill 调用记录以 Trace skill Span 为准。
52
52
  */
53
53
  export interface ISkillAuditStore {
54
54
  /** 写入审计记录 */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * SkillAuditRecorder — Skill 调用台账(RT-6)
2
+ * SkillAuditRecorder — 进程内 Skill 调用记录(调试用)
3
3
  *
4
- * 记录每次 Skill(子图)激活的审计明细,供后续分析与看板使用。
5
- * Runtime 负责写入;Gateway 看不到 Skill 调用。
4
+ * 生产记录是 Trace 上的 skill Span(子图激活,或成功读 SKILL.md)。
5
+ * 本记录器默认内存、不对外查询;分析从 Span 聚合,不读这里。
6
6
  *
7
7
  * @see docs/status/module-status-and-backlog.md RT-6
8
8
  */
@@ -1,21 +1,14 @@
1
1
  /**
2
2
  * Graph 节点 Span 辅助:createNode / completeNode 失败不挡主路径。
3
3
  */
4
- import type { ITraceCollector, ExecutionNodeEvent } from '../contracts/trace-collector.js';
4
+ import type { ITraceCollector, ExecutionNodeEvent, CompleteNodeParams } from '../contracts/trace-collector.js';
5
5
  export declare function startGraphSpan(state: Record<string, unknown>, input: {
6
6
  name: string;
7
7
  type: ExecutionNodeEvent['type'];
8
8
  parentSpanId?: string;
9
9
  extra?: Partial<Omit<ExecutionNodeEvent, 'traceId' | 'name' | 'type' | 'parentNodeId'>>;
10
10
  }): Promise<string | undefined>;
11
- export declare function completeGraphSpan(state: Record<string, unknown>, spanId: string | undefined, params: {
12
- status: 'completed' | 'failed';
13
- output?: unknown;
14
- error?: string;
15
- inputTokens?: number;
16
- outputTokens?: number;
17
- totalTokens?: number;
18
- }, collectorOverride?: ITraceCollector): Promise<void>;
11
+ export declare function completeGraphSpan(state: Record<string, unknown>, spanId: string | undefined, params: CompleteNodeParams, collectorOverride?: ITraceCollector): Promise<void>;
19
12
  export declare function withGraphSpan(state: Record<string, unknown>, spec: {
20
13
  name: string;
21
14
  type: ExecutionNodeEvent['type'];
@@ -57,6 +57,7 @@ export async function withGraphSpan(state, spec, run) {
57
57
  totalTokens: usage?.inputTokens != null || usage?.outputTokens != null
58
58
  ? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)
59
59
  : undefined,
60
+ ...(typeof usage?.model === 'string' && usage.model ? { model: usage.model } : {}),
60
61
  });
61
62
  return spanId ? { ...result, lastSpanId: spanId } : result;
62
63
  }
package/dist/index.d.ts CHANGED
@@ -15,10 +15,12 @@ export * from './memory/index.js';
15
15
  export * from './common/index.js';
16
16
  export * from './registry/index.js';
17
17
  export * from './providers/index.js';
18
- export type { ITraceCollector, TraceEvent, TraceSession, ExecutionFlowEvent, ExecutionNodeEvent, TraceLinkEvent, TraceLinkNodeEvent, } from './contracts/trace-collector.js';
18
+ export type { ITraceCollector, CompleteNodeParams, TraceEvent, TraceSession, ExecutionFlowEvent, ExecutionNodeEvent, TraceLinkEvent, TraceLinkNodeEvent, } from './contracts/trace-collector.js';
19
19
  export type { ExecutionContext, CallerType, GovernancePermissionCheckParams, GovernancePermissionCheckResult, PermissionDenyReason, } from '@gt-fe/eap-contracts';
20
20
  export { HttpGovernanceClient, AllowAllGovernanceClient, resolveExecutionContext, assertToolPermission, assertSkillPermission, permissionMessage, PermissionDeniedError, GovernanceUnavailableError, } from './governance/index.js';
21
21
  export type { IGovernanceClient } from './governance/index.js';
22
+ export { decideBeforeModel, decideBeforeTool, } from './execution/index.js';
23
+ export type { AfterModelInterceptor, AfterToolInterceptor, BeforeModelInput, BeforeModelInterceptor, BeforeToolInterceptor, ExecutionInterceptors, ModelInterceptorDecision, ToolInterceptorDecision, } from './execution/index.js';
22
24
  export { FileLlmTurnLog, InMemoryLlmTurnLog, createFileLlmTurnLogFromEnv, maskApiKey, resolveLlmTurnDir, } from './debug/llm-turn-log.js';
23
25
  export { PostgresLlmTurnLog } from './debug/pg-llm-turn-log.js';
24
26
  export type { ILlmTurnLog, LlmTurnRecord, LlmTurnIndexEntry, LlmTurnContext, LlmTurnLogKind, } from './debug/llm-turn-log.js';
package/dist/index.js CHANGED
@@ -30,5 +30,6 @@ export * from './registry/index.js';
30
30
  // LLM Providers
31
31
  export * from './providers/index.js';
32
32
  export { HttpGovernanceClient, AllowAllGovernanceClient, resolveExecutionContext, assertToolPermission, assertSkillPermission, permissionMessage, PermissionDeniedError, GovernanceUnavailableError, } from './governance/index.js';
33
+ export { decideBeforeModel, decideBeforeTool, } from './execution/index.js';
33
34
  export { FileLlmTurnLog, InMemoryLlmTurnLog, createFileLlmTurnLogFromEnv, maskApiKey, resolveLlmTurnDir, } from './debug/llm-turn-log.js';
34
35
  export { PostgresLlmTurnLog } from './debug/pg-llm-turn-log.js';
@@ -98,7 +98,7 @@ export type EmbeddingFunction = (text: string) => Promise<number[]>;
98
98
  export declare class SemanticRetrievalService {
99
99
  private readonly config;
100
100
  private readonly vectorStore?;
101
- private readonly embedding;
101
+ private readonly embedding?;
102
102
  constructor(config?: Partial<RetrievalConfig>, vectorStore?: IVectorStore, embedding?: EmbeddingFunction);
103
103
  /**
104
104
  * 检索相关记忆
@@ -20,21 +20,6 @@ export const DEFAULT_RETRIEVAL_CONFIG = {
20
20
  keywordWeight: 0.3,
21
21
  vectorWeight: 0.7,
22
22
  };
23
- /**
24
- * 默认 Embedding 函数(占位)
25
- *
26
- * 实际使用时应注入真实的 Embedding 函数(如 OpenAI Embedding)。
27
- */
28
- async function defaultEmbedding(text) {
29
- // 简单的哈希向量(占位,实际应使用真实的 Embedding 模型)
30
- const vector = new Array(128).fill(0);
31
- for (let i = 0; i < text.length; i++) {
32
- vector[i % 128] += text.charCodeAt(i) / 1000;
33
- }
34
- // 归一化
35
- const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
36
- return vector.map((v) => v / norm);
37
- }
38
23
  // ─────────────────────────────────────────────
39
24
  // 语义检索服务
40
25
  // ─────────────────────────────────────────────
@@ -48,7 +33,7 @@ export class SemanticRetrievalService {
48
33
  constructor(config, vectorStore, embedding) {
49
34
  this.config = { ...DEFAULT_RETRIEVAL_CONFIG, ...config };
50
35
  this.vectorStore = vectorStore;
51
- this.embedding = embedding ?? defaultEmbedding;
36
+ this.embedding = embedding;
52
37
  }
53
38
  /**
54
39
  * 检索相关记忆
@@ -106,11 +91,9 @@ export class SemanticRetrievalService {
106
91
  * 基于向量相似度的检索。
107
92
  */
108
93
  async vectorRetrieve(query, memories) {
109
- if (!this.vectorStore) {
110
- // 如果没有向量存储,回退到关键词检索
94
+ if (!this.vectorStore || !this.embedding) {
111
95
  return this.keywordRetrieve(query, memories);
112
96
  }
113
- // 生成查询向量
114
97
  const queryVector = await this.embedding(query);
115
98
  // 查询相似向量
116
99
  const vectorResults = await this.vectorStore.query(queryVector, this.config.limit, this.config.similarityThreshold);
@@ -183,12 +166,19 @@ export class SemanticRetrievalService {
183
166
  'under', 'again', 'further', 'then', 'once',
184
167
  ]);
185
168
  // 分词(简单按空格和标点分割)
186
- const words = text
187
- .toLowerCase()
188
- .replace(/[^\w\u4e00-\u9fff]/g, ' ')
169
+ const cleaned = text.toLowerCase().replace(/[^\w\u4e00-\u9fff]/g, ' ');
170
+ const words = cleaned
189
171
  .split(/\s+/)
190
172
  .filter((word) => word.length > 1 && !stopWords.has(word));
191
- return [...new Set(words)];
173
+ const grams = [];
174
+ for (const word of words) {
175
+ if (/[\u4e00-\u9fff]/.test(word) && word.length > 2) {
176
+ for (let i = 0; i < word.length - 1; i++) {
177
+ grams.push(word.slice(i, i + 2));
178
+ }
179
+ }
180
+ }
181
+ return [...new Set([...words, ...grams])];
192
182
  }
193
183
  }
194
184
  // ─────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  export { EchoLLMProvider, createDefaultLLMProvider } from './echo-llm-provider.js';
2
2
  export { HttpLLMProvider, DEFAULT_LLM_HTTP_TIMEOUT_MS, type HttpLLMProviderConfig } from './http-llm-provider.js';
3
- export { RoutingLLMProvider, type RoutingLLMProviderConfig } from './routing-llm-provider.js';
3
+ export { RoutingLLMProvider, type RoutingLLMProviderConfig, assertFoundationModelsInvokeAllowsStream, isFoundationModelsInvokeEnabled, MODELS_INVOKE_STREAM_MESSAGE, } from './routing-llm-provider.js';
4
4
  export { resolveLlmModelName, canOverrideGraphModel } from './resolve-llm-model.js';
5
5
  export { createLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
6
6
  export type { LocalLlmConnection, LocalLlmConnectionRef, LocalLlmConnectionRefs, LocalLlmConnections, } from './local-llm-provider.js';
@@ -1,5 +1,5 @@
1
1
  export { EchoLLMProvider, createDefaultLLMProvider } from './echo-llm-provider.js';
2
2
  export { HttpLLMProvider, DEFAULT_LLM_HTTP_TIMEOUT_MS } from './http-llm-provider.js';
3
- export { RoutingLLMProvider } from './routing-llm-provider.js';
3
+ export { RoutingLLMProvider, assertFoundationModelsInvokeAllowsStream, isFoundationModelsInvokeEnabled, MODELS_INVOKE_STREAM_MESSAGE, } from './routing-llm-provider.js';
4
4
  export { resolveLlmModelName, canOverrideGraphModel } from './resolve-llm-model.js';
5
5
  export { createLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
@@ -14,6 +14,10 @@
14
14
  import type { GraphMessage } from '../graph/contracts/executable-graph.js';
15
15
  import type { ILLMProvider, LLMNodeConfig, LLMResponse, LLMChunk } from '../graph/nodes/llm-node.js';
16
16
  import type { ILlmTurnLog } from '../debug/llm-turn-log.js';
17
+ export declare const MODELS_INVOKE_STREAM_MESSAGE = "EAP_RUNTIME_MODELS_INVOKE \u5B9E\u9A8C\u53E3\u53EA\u652F\u6301\u540C\u6B65 invoke\uFF08POST /models/invoke\uFF09\uFF0C\u4E0D\u652F\u6301 stream\u3002\u5173\u6389\u8BE5\u5F00\u5173\uFF0C\u6216\u6539\u8D70 Foundation /models/select + \u672C\u673A HTTP\u3002";
18
+ export declare function isFoundationModelsInvokeEnabled(env?: NodeJS.ProcessEnv): boolean;
19
+ /** `/models/invoke` 无真流式;开了该开关时 RoutingLLMProvider.stream 必须失败。 */
20
+ export declare function assertFoundationModelsInvokeAllowsStream(env?: NodeJS.ProcessEnv): void;
17
21
  export interface RoutingLLMProviderConfig {
18
22
  foundationUrl: string;
19
23
  fallbackProvider?: ILLMProvider;
@@ -28,5 +32,6 @@ export declare class RoutingLLMProvider implements ILLMProvider {
28
32
  stream(messages: GraphMessage[], config: LLMNodeConfig): AsyncIterableIterator<LLMChunk>;
29
33
  private withCallDefaults;
30
34
  private selectModel;
35
+ private useFoundationInvoke;
31
36
  private createProviderForModel;
32
37
  }