@gt-fe/eap-runtime 0.0.20 → 0.0.22

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 (133) hide show
  1. package/README.md +4 -3
  2. package/dist/api/handlers/invoke.handler.d.ts +7 -0
  3. package/dist/api/handlers/invoke.handler.js +52 -10
  4. package/dist/api/handlers/stream.handler.d.ts +6 -0
  5. package/dist/api/handlers/stream.handler.js +60 -39
  6. package/dist/api/routes/agent.routes.d.ts +8 -1
  7. package/dist/checkpoint/file-checkpoint-saver.d.ts +4 -0
  8. package/dist/checkpoint/file-checkpoint-saver.js +51 -22
  9. package/dist/checkpoint/index.d.ts +1 -1
  10. package/dist/checkpoint/index.js +1 -1
  11. package/dist/common/types.d.ts +2 -2
  12. package/dist/common/types.js +1 -1
  13. package/dist/execution/index.d.ts +2 -0
  14. package/dist/execution/index.js +2 -0
  15. package/dist/execution/interceptors.d.ts +40 -0
  16. package/dist/execution/interceptors.js +23 -0
  17. package/dist/execution/ports.d.ts +18 -0
  18. package/dist/execution/ports.js +13 -0
  19. package/dist/governance/audit-reporter.d.ts +14 -0
  20. package/dist/governance/audit-reporter.js +18 -0
  21. package/dist/governance/index.d.ts +2 -0
  22. package/dist/governance/index.js +1 -0
  23. package/dist/governance/permission-guard.d.ts +6 -2
  24. package/dist/governance/permission-guard.js +38 -5
  25. package/dist/graph/adapters/graph-nesting.d.ts +12 -0
  26. package/dist/graph/adapters/graph-nesting.js +29 -0
  27. package/dist/graph/adapters/index.d.ts +2 -0
  28. package/dist/graph/adapters/index.js +2 -0
  29. package/dist/graph/adapters/langgraph-checkpointer.d.ts +11 -0
  30. package/dist/graph/adapters/langgraph-checkpointer.js +48 -9
  31. package/dist/graph/adapters/langgraph.adapter.d.ts +33 -11
  32. package/dist/graph/adapters/langgraph.adapter.js +200 -98
  33. package/dist/graph/adapters/skill-isolate.d.ts +13 -0
  34. package/dist/graph/adapters/skill-isolate.js +39 -0
  35. package/dist/graph/adapters/subgraph-compiler.d.ts +7 -2
  36. package/dist/graph/adapters/subgraph-compiler.js +63 -30
  37. package/dist/graph/contracts/executable-graph.d.ts +33 -5
  38. package/dist/graph/hitl/hitl-handler.d.ts +13 -4
  39. package/dist/graph/hitl/hitl-handler.js +80 -7
  40. package/dist/graph/nodes/index.d.ts +1 -0
  41. package/dist/graph/nodes/index.js +1 -0
  42. package/dist/graph/nodes/llm-node.d.ts +3 -1
  43. package/dist/graph/nodes/llm-node.js +26 -2
  44. package/dist/graph/nodes/rag/dedup.d.ts +20 -0
  45. package/dist/graph/nodes/rag/dedup.js +77 -0
  46. package/dist/graph/nodes/rag/denoise.d.ts +58 -0
  47. package/dist/graph/nodes/rag/denoise.js +220 -0
  48. package/dist/graph/nodes/rag/fusion/fuse.d.ts +11 -0
  49. package/dist/graph/nodes/rag/fusion/fuse.js +25 -0
  50. package/dist/graph/nodes/rag/fusion/normalize.d.ts +9 -0
  51. package/dist/graph/nodes/rag/fusion/normalize.js +135 -0
  52. package/dist/graph/nodes/rag/index.d.ts +18 -0
  53. package/dist/graph/nodes/rag/index.js +17 -0
  54. package/dist/graph/nodes/rag/prompts.d.ts +12 -0
  55. package/dist/graph/nodes/rag/prompts.js +40 -0
  56. package/dist/graph/nodes/rag/rag-node.d.ts +21 -0
  57. package/dist/graph/nodes/rag/rag-node.js +135 -0
  58. package/dist/graph/nodes/rag/rag-search-tool.d.ts +48 -0
  59. package/dist/graph/nodes/rag/rag-search-tool.js +230 -0
  60. package/dist/graph/nodes/rag/react-loop.d.ts +57 -0
  61. package/dist/graph/nodes/rag/react-loop.js +467 -0
  62. package/dist/graph/nodes/rag/retrievers/http-embedder.d.ts +29 -0
  63. package/dist/graph/nodes/rag/retrievers/http-embedder.js +66 -0
  64. package/dist/graph/nodes/rag/retrievers/in-memory-retriever.d.ts +17 -0
  65. package/dist/graph/nodes/rag/retrievers/in-memory-retriever.js +18 -0
  66. package/dist/graph/nodes/rag/retrievers/local-sqlite-retriever.d.ts +46 -0
  67. package/dist/graph/nodes/rag/retrievers/local-sqlite-retriever.js +85 -0
  68. package/dist/graph/nodes/rag/tools.d.ts +90 -0
  69. package/dist/graph/nodes/rag/tools.js +260 -0
  70. package/dist/graph/nodes/rag/types.d.ts +226 -0
  71. package/dist/graph/nodes/rag/types.js +9 -0
  72. package/dist/graph/nodes/tool-node.d.ts +19 -1
  73. package/dist/graph/nodes/tool-node.js +158 -33
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.js +1 -0
  76. package/dist/memory/semantic-retrieval.service.d.ts +1 -1
  77. package/dist/memory/semantic-retrieval.service.js +13 -23
  78. package/dist/providers/index.d.ts +2 -2
  79. package/dist/providers/index.js +2 -2
  80. package/dist/providers/local-llm-provider.d.ts +6 -0
  81. package/dist/providers/local-llm-provider.js +29 -5
  82. package/dist/providers/routing-llm-provider.d.ts +7 -2
  83. package/dist/providers/routing-llm-provider.js +71 -16
  84. package/dist/registry/deployment-snapshot.js +18 -4
  85. package/dist/registry/graph-loader.d.ts +2 -0
  86. package/dist/registry/graph-loader.js +39 -2
  87. package/dist/registry/preload-flags.d.ts +1 -1
  88. package/dist/registry/preload-flags.js +1 -1
  89. package/dist/runtime/agent-runtime.d.ts +5 -0
  90. package/dist/runtime/agent-runtime.js +107 -70
  91. package/dist/runtime/dev-rag-deps.d.ts +21 -0
  92. package/dist/runtime/dev-rag-deps.js +186 -0
  93. package/dist/runtime/dev-runtime.d.ts +18 -2
  94. package/dist/runtime/dev-runtime.js +68 -8
  95. package/dist/runtime/index.d.ts +1 -1
  96. package/dist/runtime/types.d.ts +10 -9
  97. package/dist/session/apply-session-compression.d.ts +85 -0
  98. package/dist/session/apply-session-compression.js +281 -0
  99. package/dist/session/apply-session-memory.d.ts +21 -0
  100. package/dist/session/apply-session-memory.js +48 -0
  101. package/dist/session/index.d.ts +2 -0
  102. package/dist/session/index.js +2 -0
  103. package/dist/session/persist-turn-messages.d.ts +57 -1
  104. package/dist/session/persist-turn-messages.js +160 -5
  105. package/dist/sse/events/index.d.ts +13 -4
  106. package/dist/sse/events/index.js +9 -1
  107. package/dist/sse/events/rag-events.d.ts +63 -0
  108. package/dist/sse/events/rag-events.js +9 -0
  109. package/dist/sse/events/session-events.d.ts +38 -0
  110. package/dist/sse/events/session-events.js +4 -0
  111. package/dist/token/index.d.ts +2 -1
  112. package/dist/token/index.js +2 -1
  113. package/dist/token/record-task-token-usage.d.ts +25 -0
  114. package/dist/token/record-task-token-usage.js +44 -0
  115. package/dist/token/token-detail-mode.d.ts +4 -4
  116. package/dist/token/token-detail-mode.js +4 -4
  117. package/dist/token/token-detail.d.ts +8 -0
  118. package/dist/token/token-detail.js +22 -0
  119. package/dist/token/token-stats.service.d.ts +2 -2
  120. package/dist/token/token-stats.service.js +1 -1
  121. package/dist/tool/create-tool-executor.d.ts +6 -0
  122. package/dist/tool/create-tool-executor.js +4 -3
  123. package/dist/tool/harness-primitives.d.ts +9 -1
  124. package/dist/tool/harness-primitives.js +43 -12
  125. package/dist/tool/index.d.ts +2 -1
  126. package/dist/tool/index.js +2 -1
  127. package/dist/tool/tool-confirmation.js +8 -5
  128. package/dist/tool/tool-executor.interface.d.ts +27 -0
  129. package/dist/tool/tool-orchestrator.d.ts +3 -1
  130. package/dist/tool/tool-orchestrator.js +15 -6
  131. package/dist/tool/tool-retry.d.ts +15 -0
  132. package/dist/tool/tool-retry.js +69 -0
  133. 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
  */
@@ -49,6 +51,8 @@ export interface ToolNodeProgress {
49
51
  skillVersion?: string;
50
52
  skillName?: string;
51
53
  toolVersion?: string;
54
+ /** 本失败后是否还会再试。缺省 false */
55
+ willRetry?: boolean;
52
56
  }
53
57
  export declare class ToolNode {
54
58
  private readonly orchestrator;
@@ -57,7 +61,20 @@ export declare class ToolNode {
57
61
  private readonly onProgress?;
58
62
  private readonly hitlHandler?;
59
63
  private readonly nodeId?;
60
- 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);
61
78
  /**
62
79
  * execute — StateGraph 节点函数
63
80
  *
@@ -71,6 +88,7 @@ export declare class ToolNode {
71
88
  * 从 state 中提取 messages 数组
72
89
  */
73
90
  private extractMessages;
91
+ private executeSingleWithRetry;
74
92
  /**
75
93
  * 从 messages 中找到最后一条 assistant 消息
76
94
  */
@@ -9,6 +9,7 @@
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';
13
14
  import { recordSkillEntrySpan, skillEntryInvocationFromTool, skillTraceIdentity } from '../adapters/skill-entry-span.js';
14
15
  import { inferHarnessSkillDir } from '../../tool/skill-dir-infer.js';
@@ -17,6 +18,10 @@ import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
17
18
  import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
18
19
  import { toolTraceIdentity } from '../../tool/mcp-list.js';
19
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 { isIsolatedSkillState } from '../adapters/skill-isolate.js';
20
25
  function skillDirsFromState(state) {
21
26
  const defs = state['skillDefinitions'];
22
27
  return Object.values(defs ?? {})
@@ -30,13 +35,27 @@ export class ToolNode {
30
35
  onProgress;
31
36
  hitlHandler;
32
37
  nodeId;
33
- 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) {
34
49
  this.orchestrator = orchestrator;
35
50
  this.injector = injector;
36
51
  this.defaultAuthContext = defaultAuthContext;
37
52
  this.onProgress = onProgress;
38
53
  this.hitlHandler = hitlHandler;
39
54
  this.nodeId = nodeId;
55
+ this.interceptors = interceptors;
56
+ this.ragSourcesConfig = ragSourcesConfig;
57
+ this.ragTotalK = ragTotalK;
58
+ this.retry = retry;
40
59
  }
41
60
  /**
42
61
  * execute — StateGraph 节点函数
@@ -100,6 +119,12 @@ export class ToolNode {
100
119
  ...(jail.workdir ? { jailWorkdir: jail.workdir } : {}),
101
120
  ...(jailExtraRoots.length > 0 ? { jailExtraRoots } : {}),
102
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 } : {}),
103
128
  };
104
129
  });
105
130
  const executionContext = state['executionContext'];
@@ -120,8 +145,31 @@ export class ToolNode {
120
145
  ...(inSkillSubgraph ? { inSkillSubgraph: true } : {}),
121
146
  });
122
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;
123
163
  // 5. 并行执行所有 Tool 调用(附 Trace span)
124
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
+ }
125
173
  if (!traceCollector || !traceId)
126
174
  return null;
127
175
  try {
@@ -153,17 +201,9 @@ export class ToolNode {
153
201
  return null;
154
202
  }
155
203
  }));
156
- // 并行执行,但各自独立计时(避免共用 startedAt 导致时长不准/看起来像“写死”)
157
- // require_confirmation 工具走 HITL 拦截:改为顺序执行,thread_id 与 Checkpoint 对齐
158
- const confirmationTools = new Set(Object.entries(loadedTools ?? {})
159
- .filter(([, def]) => Boolean(def?.manifest.requireConfirmation))
160
- .map(([name]) => name));
161
- const needsHitl = Boolean(this.hitlHandler) &&
162
- requests.some((req) => confirmationTools.has(req.toolName));
163
204
  const runOne = async (req, i) => {
164
205
  const toolIdentity = toolTraceIdentity(loadedTools?.[req.toolName], req.toolName);
165
206
  const toolset = toolIdentity.toolset;
166
- const resumingHitl = isHitlDecision(state['hitlChoice']);
167
207
  if (!resumingHitl) {
168
208
  emitProgress({
169
209
  toolCallId: req.toolCallId,
@@ -177,32 +217,69 @@ export class ToolNode {
177
217
  const startedAt = Date.now();
178
218
  try {
179
219
  let result;
180
- if (needsHitl && this.hitlHandler && confirmationTools.has(req.toolName)) {
181
- const interceptor = new ToolConfirmationInterceptor(confirmationTools, this.hitlHandler, {
182
- execute: (r) => this.orchestrator.executeSingle(r, executionContext),
183
- });
184
- const graphState = this.toGraphState(state);
185
- const threadId = resolveCheckpointThreadId({
186
- sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
187
- taskId: req.taskId,
188
- });
189
- const confirmed = await interceptor.executeWithConfirmation(req, graphState, req.taskId ?? String(state['taskId'] ?? req.toolCallId), threadId, {
190
- toolSpanId: spanIds[i] ?? undefined,
191
- skillSpanId: typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined,
192
- });
193
- result =
194
- confirmed ??
195
- {
196
- toolCallId: req.toolCallId,
197
- toolName: req.toolName,
198
- content: `用户拒绝执行工具 ${req.toolName}`,
199
- success: false,
200
- error: `用户拒绝执行工具 ${req.toolName}`,
201
- errorCode: 'hitl_rejected',
202
- };
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
+ };
203
230
  }
204
231
  else {
205
- result = await this.orchestrator.executeSingle(req, executionContext);
232
+ const askHitl = gate.action === 'ask' || (needsHitl && confirmationTools.has(req.toolName));
233
+ if (askHitl && this.hitlHandler) {
234
+ const confirmSet = new Set(confirmationTools);
235
+ if (gate.action === 'ask')
236
+ confirmSet.add(req.toolName);
237
+ const interceptor = new ToolConfirmationInterceptor(confirmSet, this.hitlHandler, {
238
+ execute: (r) => this.orchestrator.executeSingle(r, executionContext),
239
+ });
240
+ const graphState = this.toGraphState(state);
241
+ const threadId = resolveCheckpointThreadId({
242
+ sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
243
+ taskId: req.taskId,
244
+ });
245
+ const confirmed = await interceptor.executeWithConfirmation(req, graphState, req.taskId ?? String(state['taskId'] ?? req.toolCallId), threadId, {
246
+ toolSpanId: spanIds[i] ?? undefined,
247
+ skillSpanId: typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined,
248
+ });
249
+ result =
250
+ confirmed ??
251
+ {
252
+ toolCallId: req.toolCallId,
253
+ toolName: req.toolName,
254
+ content: `用户拒绝执行工具 ${req.toolName}`,
255
+ success: false,
256
+ error: `用户拒绝执行工具 ${req.toolName}`,
257
+ errorCode: 'hitl_rejected',
258
+ };
259
+ }
260
+ else if (gate.action === 'ask') {
261
+ result = {
262
+ toolCallId: req.toolCallId,
263
+ toolName: req.toolName,
264
+ content: '',
265
+ success: false,
266
+ error: '需要人工确认',
267
+ errorCode: 'interceptor_ask',
268
+ };
269
+ }
270
+ else {
271
+ result = await this.executeSingleWithRetry(req, executionContext, emitProgress, toolset, toolIdentity);
272
+ }
273
+ }
274
+ try {
275
+ await this.interceptors?.afterTool?.(req, result);
276
+ }
277
+ catch (err) {
278
+ emitRuntimeLog('warn', 'runtime.intercept', 'afterTool failed', {
279
+ event: 'runtime.intercept.after_tool_failed',
280
+ toolName: req.toolName,
281
+ error: err instanceof Error ? err.message : String(err),
282
+ });
206
283
  }
207
284
  const executionTime = Date.now() - startedAt;
208
285
  if (result.success) {
@@ -274,6 +351,23 @@ export class ToolNode {
274
351
  (typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined);
275
352
  throw hitlPaused;
276
353
  }
354
+ if (err instanceof AppError &&
355
+ err.code === ErrorCodes.GRAPH_EXECUTION_ERROR &&
356
+ isIsolatedSkillState(state)) {
357
+ const failedSpanId = spanIds[i];
358
+ if (traceCollector && failedSpanId) {
359
+ try {
360
+ await traceCollector.completeNode(failedSpanId, {
361
+ status: 'failed',
362
+ error: err.message,
363
+ });
364
+ }
365
+ catch (completeErr) {
366
+ warnTraceSpanFailure('completeNode', { type: 'tool', name: req.toolName, traceId, spanId: failedSpanId }, completeErr);
367
+ }
368
+ }
369
+ throw err;
370
+ }
277
371
  const executionTime = Date.now() - startedAt;
278
372
  const message = err instanceof Error ? err.message : String(err);
279
373
  emitProgress({
@@ -362,6 +456,37 @@ export class ToolNode {
362
456
  return { role: 'user', content: String(msg) };
363
457
  });
364
458
  }
459
+ async executeSingleWithRetry(req, executionContext, emitProgress, toolset, toolIdentity) {
460
+ const retry = resolveToolRetryConfig(this.retry);
461
+ const maxRetries = retry?.maxRetries ?? 0;
462
+ let last;
463
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
464
+ last = await this.orchestrator.executeSingle(req, executionContext);
465
+ if (last.success)
466
+ return last;
467
+ const willRetry = attempt < maxRetries && isRetryableToolFailure(last);
468
+ if (!willRetry)
469
+ return last;
470
+ emitProgress({
471
+ toolCallId: req.toolCallId,
472
+ toolName: req.toolName,
473
+ ...(toolset ? { toolset } : {}),
474
+ ...(toolIdentity.toolVersion ? { toolVersion: toolIdentity.toolVersion } : {}),
475
+ status: 'error',
476
+ arguments: req.arguments,
477
+ error: last.error ?? 'unknown error',
478
+ willRetry: true,
479
+ });
480
+ await sleepToolRetry(toolRetryDelayMs(attempt, retry.backoff));
481
+ }
482
+ return last ?? {
483
+ toolCallId: req.toolCallId,
484
+ toolName: req.toolName,
485
+ content: '',
486
+ success: false,
487
+ error: 'unknown error',
488
+ };
489
+ }
365
490
  /**
366
491
  * 从 messages 中找到最后一条 assistant 消息
367
492
  */
package/dist/index.d.ts CHANGED
@@ -19,6 +19,8 @@ export type { ITraceCollector, CompleteNodeParams, TraceEvent, TraceSession, Exe
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,7 +1,7 @@
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
- export { createLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
5
+ export { createLocalLlmProvider, createOptionalLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, hasLocalLlmConnection, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
6
6
  export type { LocalLlmConnection, LocalLlmConnectionRef, LocalLlmConnectionRefs, LocalLlmConnections, } from './local-llm-provider.js';
7
7
  export type { LLMToolDefinition } from '../graph/nodes/llm-node.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
- export { createLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
5
+ export { createLocalLlmProvider, createOptionalLocalLlmProvider, requireLocalLlmConnections, resolveLocalLlmConnection, parseLocalLlmConnectionRefs, hydrateLocalLlmConnections, hasLocalLlmConnection, LLM_CONNECTIONS_ENV, } from './local-llm-provider.js';
@@ -29,6 +29,12 @@ export interface LocalLlmConnections {
29
29
  export declare const LLM_CONNECTIONS_ENV = "EAP_LLM_CONNECTIONS";
30
30
  export declare function parseLocalLlmConnectionRefs(env?: NodeJS.ProcessEnv): LocalLlmConnectionRefs;
31
31
  export declare function hydrateLocalLlmConnections(refs: LocalLlmConnectionRefs, env?: NodeJS.ProcessEnv): LocalLlmConnections;
32
+ export declare function hasLocalLlmConnection(connections: LocalLlmConnections): boolean;
32
33
  export declare function requireLocalLlmConnections(env?: NodeJS.ProcessEnv): LocalLlmConnections;
34
+ /**
35
+ * Routing 的可选 fallback:有可用本地连接才返回 Provider。
36
+ * 连接表 JSON 损坏仍抛错;解不出密钥则视为没有本地网关。
37
+ */
38
+ export declare function createOptionalLocalLlmProvider(turnLog?: ILlmTurnLog, env?: NodeJS.ProcessEnv): ILLMProvider | undefined;
33
39
  export declare function resolveLocalLlmConnection(connections: LocalLlmConnections, model?: string): LocalLlmConnection;
34
40
  export declare function createLocalLlmProvider(connections?: LocalLlmConnections, turnLog?: ILlmTurnLog): ILLMProvider;
@@ -63,10 +63,13 @@ export function hydrateLocalLlmConnections(refs, env = process.env) {
63
63
  }
64
64
  return { default: hydrate(refs.default), byModel };
65
65
  }
66
+ export function hasLocalLlmConnection(connections) {
67
+ return Boolean(connections.default?.apiKey) || Object.keys(connections.byModel ?? {}).length > 0;
68
+ }
66
69
  export function requireLocalLlmConnections(env = process.env) {
67
70
  const refs = parseLocalLlmConnectionRefs(env);
68
71
  const connections = hydrateLocalLlmConnections(refs, env);
69
- if (!connections.default && Object.keys(connections.byModel ?? {}).length === 0) {
72
+ if (!hasLocalLlmConnection(connections)) {
70
73
  const wanted = [
71
74
  refs.default?.apiKeyEnv,
72
75
  ...Object.values(refs.byModel ?? {}).map((ref) => ref.apiKeyEnv),
@@ -75,13 +78,34 @@ export function requireLocalLlmConnections(env = process.env) {
75
78
  }
76
79
  return connections;
77
80
  }
81
+ /**
82
+ * Routing 的可选 fallback:有可用本地连接才返回 Provider。
83
+ * 连接表 JSON 损坏仍抛错;解不出密钥则视为没有本地网关。
84
+ */
85
+ export function createOptionalLocalLlmProvider(turnLog, env = process.env) {
86
+ const refs = parseLocalLlmConnectionRefs(env);
87
+ const connections = hydrateLocalLlmConnections(refs, env);
88
+ if (!hasLocalLlmConnection(connections))
89
+ return undefined;
90
+ return createLocalLlmProvider(connections, turnLog);
91
+ }
92
+ function namedLocalModel(model) {
93
+ const trimmed = model?.trim();
94
+ if (!trimmed || trimmed.toLowerCase() === 'default')
95
+ return undefined;
96
+ return trimmed;
97
+ }
78
98
  export function resolveLocalLlmConnection(connections, model) {
79
- const named = model ? connections.byModel?.[model] : undefined;
99
+ const wanted = namedLocalModel(model);
100
+ const named = wanted ? connections.byModel?.[wanted] : undefined;
80
101
  const connection = named ?? connections.default;
81
102
  if (!connection?.apiKey) {
82
- throw new Error(model
83
- ? `No local LLM gateway for model ${model}. Set runtime.llm.models["${model}"] in eap.config.json.`
84
- : 'No local LLM gateway. Set runtime.llm in eap.config.json.');
103
+ const available = Object.keys(connections.byModel ?? {}).sort();
104
+ const availableHint = available.length > 0 ? ` Available: ${available.join(', ')}.` : '';
105
+ if (wanted) {
106
+ throw new Error(`No local LLM gateway for model ${wanted}. Set runtime.llm.models["${wanted}"] in eap.config.json.${availableHint}`);
107
+ }
108
+ throw new Error(`No local LLM gateway. Set runtime.llm.models.default or runtime.llm.baseUrl in eap.config.json.${availableHint}`);
85
109
  }
86
110
  return connection;
87
111
  }
@@ -7,13 +7,17 @@
7
7
  * 3. 用返回的连接配置创建 HttpLLMProvider,发送请求
8
8
  *
9
9
  * 设计原则:
10
- * - Runtime 不存任何 LLM 凭证,全部从 Foundation 获取
10
+ * - 生产路径:Foundation `/models/select` 返回连接,本机 HTTP
11
+ * - select 失败或没有连接:走构造时注入的 fallback(dev 为 LocalLlmProvider / `resolveLocalLlmConnection`)
11
12
  * - 支持任意 OpenAI 兼容 API(OpenAI / DeepSeek / 本地 Ollama 等)
12
- * - 连接配置由 Foundation 统一管理,Runtime 无感知
13
13
  */
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
  }