@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
@@ -2,6 +2,16 @@ export interface SkillDirRef {
2
2
  skillId: string;
3
3
  bundlePath: string;
4
4
  }
5
+ /** 技能包落点:Agent 预装 / 用户装配 / 开发态 overlay。 */
6
+ export type SkillSpace = 'agent' | 'user' | 'local';
7
+ /**
8
+ * 按实际 bundle 路径判定空间。有 agentDir 时相对预装根与 users/ 判定;
9
+ * 否则看路径段 `preinstall/skills` / `users/<id>/skills`。
10
+ */
11
+ export declare function classifySkillSpace(bundleOrFilePath: string, opts?: {
12
+ agentDir?: string;
13
+ userId?: string;
14
+ }): SkillSpace | undefined;
5
15
  /**
6
16
  * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
7
17
  */
@@ -33,6 +33,39 @@ function skillIdFromSkillsSegment(norm) {
33
33
  const match = norm.match(/(?:^|\/)(?:preinstall\/)?skills\/([^/]+)(?:\/|$)/i);
34
34
  return match?.[1] ?? '';
35
35
  }
36
+ /**
37
+ * 按实际 bundle 路径判定空间。有 agentDir 时相对预装根与 users/ 判定;
38
+ * 否则看路径段 `preinstall/skills` / `users/<id>/skills`。
39
+ */
40
+ export function classifySkillSpace(bundleOrFilePath, opts) {
41
+ if (!bundleOrFilePath)
42
+ return undefined;
43
+ const target = normalizeForMatch(bundleOrFilePath);
44
+ const agentDir = opts?.agentDir?.trim();
45
+ if (agentDir) {
46
+ const root = normalizeForMatch(agentDir);
47
+ const pre = `${root}/preinstall/skills`;
48
+ if (target === pre || target.startsWith(`${pre}/`))
49
+ return 'agent';
50
+ const uid = opts?.userId?.trim();
51
+ if (uid) {
52
+ const user = `${root}/users/${uid}/skills`;
53
+ if (target === user || target.startsWith(`${user}/`))
54
+ return 'user';
55
+ }
56
+ if (new RegExp(`^${escapeRegExp(root)}/users/[^/]+/skills(/|$)`).test(target))
57
+ return 'user';
58
+ return 'local';
59
+ }
60
+ if (/(^|\/)preinstall\/skills(\/|$)/i.test(target))
61
+ return 'agent';
62
+ if (/(^|\/)users\/[^/]+\/skills(\/|$)/i.test(target))
63
+ return 'user';
64
+ return undefined;
65
+ }
66
+ function escapeRegExp(value) {
67
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
68
+ }
36
69
  /**
37
70
  * 判断 harness 读操作是否命中技能包,并用 bundlePath 最长前缀解析 skillId。
38
71
  */
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * 将 Skill DSL 编译为子图节点函数,嵌入父 Graph 执行。
5
5
  * 子图作为独立 StateGraph 编译,父图通过节点函数调用子图并合并输出。
6
+ * Skill 子图不得再含 type=subgraph(G-3 / B14)。
6
7
  *
7
8
  * @see design.md §1.4 子图嵌套: Skill作为子图嵌入主Graph
8
9
  * @see requirements.md Requirement 1 AC2 — 子图嵌套(Skill作为子图)
@@ -24,16 +25,20 @@ type LangGraphState = Record<string, unknown>;
24
25
  * 4. 执行时,子图内部运行完整的 graph invoke 流程,输出合并回父图 state
25
26
  */
26
27
  export declare class SubGraphCompiler {
28
+ private readonly auditReporter?;
27
29
  private readonly adapter;
28
30
  private readonly governanceClient?;
29
31
  private readonly skillAuditRecorder?;
30
- constructor(adapter?: LangGraphAdapter, governanceClient?: IGovernanceClient, skillAuditRecorder?: SkillAuditRecorder);
32
+ constructor(adapter?: LangGraphAdapter, governanceClient?: IGovernanceClient, skillAuditRecorder?: SkillAuditRecorder, auditReporter?: import("../../governance/audit-reporter.js").IAuditReporter | undefined);
31
33
  /**
32
34
  * 将 Skill DSL 编译为可嵌入父图的 NodeFunction
33
35
  *
34
36
  * @param dsl - Skill 的 GraphDSL 定义
35
37
  * @param skillId - Skill 资源 ID(用于 checkPermission skill:use)
36
38
  */
37
- compileSubGraph(dsl: GraphDSL, skillId?: string): NodeFunction<LangGraphState>;
39
+ compileSubGraph(dsl: GraphDSL, skillId?: string, options?: {
40
+ isolateContext?: boolean;
41
+ parentNodeId?: string;
42
+ }): NodeFunction<LangGraphState>;
38
43
  }
39
44
  export {};
@@ -3,17 +3,22 @@
3
3
  *
4
4
  * 将 Skill DSL 编译为子图节点函数,嵌入父 Graph 执行。
5
5
  * 子图作为独立 StateGraph 编译,父图通过节点函数调用子图并合并输出。
6
+ * Skill 子图不得再含 type=subgraph(G-3 / B14)。
6
7
  *
7
8
  * @see design.md §1.4 子图嵌套: Skill作为子图嵌入主Graph
8
9
  * @see requirements.md Requirement 1 AC2 — 子图嵌套(Skill作为子图)
9
10
  */
11
+ import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
10
12
  import { assertSkillPermission } from '../../governance/permission-guard.js';
11
13
  import { LangGraphAdapter } from './langgraph.adapter.js';
14
+ import { assertSkillGraphHasNoSubgraphNodes } from './graph-nesting.js';
12
15
  import { resolveExecutionTrace, warnTraceSpanFailure, } from '../execution-trace-context.js';
16
+ import { skillSpanFields, skillTraceIdentity } from './skill-entry-span.js';
13
17
  import { filterLoadedToolsForSkill, } from '../../tool/system-prompt-builder.js';
14
18
  import { unwrapHitlPaused } from '../hitl/hitl-handler.js';
15
19
  import { pickContextFields } from '../../token/context-window.js';
16
20
  import { assertSkillIoIfEnabled, coerceSkillInputFromMessages, } from '../../registry/skill-io-guard.js';
21
+ import { isolatedSkillMessages, isolatedSkillOutput, SKILL_ISOLATE_HITL_MESSAGE, } from './skill-isolate.js';
17
22
  /**
18
23
  * SubGraphCompiler — 子图编译器
19
24
  *
@@ -24,10 +29,12 @@ import { assertSkillIoIfEnabled, coerceSkillInputFromMessages, } from '../../reg
24
29
  * 4. 执行时,子图内部运行完整的 graph invoke 流程,输出合并回父图 state
25
30
  */
26
31
  export class SubGraphCompiler {
32
+ auditReporter;
27
33
  adapter;
28
34
  governanceClient;
29
35
  skillAuditRecorder;
30
- constructor(adapter, governanceClient, skillAuditRecorder) {
36
+ constructor(adapter, governanceClient, skillAuditRecorder, auditReporter) {
37
+ this.auditReporter = auditReporter;
31
38
  this.adapter = adapter ?? new LangGraphAdapter();
32
39
  this.governanceClient = governanceClient;
33
40
  this.skillAuditRecorder = skillAuditRecorder;
@@ -38,14 +45,23 @@ export class SubGraphCompiler {
38
45
  * @param dsl - Skill 的 GraphDSL 定义
39
46
  * @param skillId - Skill 资源 ID(用于 checkPermission skill:use)
40
47
  */
41
- compileSubGraph(dsl, skillId) {
48
+ compileSubGraph(dsl, skillId, options) {
49
+ assertSkillGraphHasNoSubgraphNodes(dsl, skillId ?? dsl.name);
42
50
  const adapter = this.adapter;
51
+ const isolateContext = options?.isolateContext === true;
43
52
  const resolvedSkillId = skillId ?? dsl.name ?? '';
44
53
  const subGraphNodeFn = async (state) => {
45
- const executionContext = state['executionContext'];
46
- if (executionContext) {
47
- await assertSkillPermission(this.governanceClient, executionContext, resolvedSkillId);
48
- }
54
+ const executionContext = state['executionContext'] ?? {
55
+ userId: String(state['userId'] ?? ''),
56
+ callerType: 'user',
57
+ roles: [],
58
+ permissions: [],
59
+ delegationToken: String(state['delegationToken'] ?? ''),
60
+ correlationId: String(state['correlationId'] ?? ''),
61
+ };
62
+ await assertSkillPermission(this.governanceClient, executionContext, resolvedSkillId, {
63
+ auditReporter: this.auditReporter,
64
+ });
49
65
  const existingSpanId = typeof state['skillSpanId'] === 'string' && state['skillSpanId']
50
66
  ? state['skillSpanId']
51
67
  : '';
@@ -63,6 +79,7 @@ export class SubGraphCompiler {
63
79
  // 用父图这次调用上的 collector;内层 invoke 只补 collector/traceId,沿用外层流式袋。
64
80
  const { collector: traceCollector, traceId } = resolveExecutionTrace(state);
65
81
  const skillName = dsl.name || resolvedSkillId;
82
+ const identity = skillTraceIdentity(state, resolvedSkillId);
66
83
  if (!resuming) {
67
84
  adapter.emitStreamEvent({
68
85
  type: 'skill.started',
@@ -71,19 +88,27 @@ export class SubGraphCompiler {
71
88
  timestamp: new Date().toISOString(),
72
89
  payload: {
73
90
  skillId: resolvedSkillId,
74
- skillName,
91
+ skillName: identity.skillName || skillName,
92
+ ...(identity.skillSpace ? { skillSpace: identity.skillSpace } : {}),
93
+ ...(identity.skillVersion ? { skillVersion: identity.skillVersion } : {}),
75
94
  },
76
95
  });
77
96
  }
78
97
  let skillSpanId = existingSpanId || null;
79
98
  if (!resuming && traceCollector && traceId) {
80
99
  try {
100
+ const fields = skillSpanFields(identity, { source: 'subgraph' });
81
101
  skillSpanId = await traceCollector.createNode({
82
102
  traceId,
83
103
  name: resolvedSkillId || dsl.name || 'skill',
84
104
  type: 'skill',
85
105
  agentCode: state['agentCode'] ?? state['agentId'],
86
- metadata: { skillId: resolvedSkillId, taskId: state['taskId'], correlationId: state['correlationId'] },
106
+ ...(fields.version ? { version: fields.version } : {}),
107
+ input: fields.input,
108
+ metadata: {
109
+ taskId: state['taskId'],
110
+ correlationId: state['correlationId'],
111
+ },
87
112
  });
88
113
  }
89
114
  catch (err) {
@@ -109,26 +134,32 @@ export class SubGraphCompiler {
109
134
  if (skillInfo) {
110
135
  assertSkillIoIfEnabled(skillInfo, skillInput);
111
136
  }
137
+ const parentMessages = messages.map((msg) => ({
138
+ role: msg['role'] ?? 'user',
139
+ content: msg['content'] ?? '',
140
+ toolCalls: msg['tool_calls'] ?? msg['toolCalls'],
141
+ ...(typeof msg['toolCallId'] === 'string' ? { toolCallId: msg['toolCallId'] } : {}),
142
+ ...(typeof msg['toolName'] === 'string' ? { toolName: msg['toolName'] } : {}),
143
+ ...(typeof msg['success'] === 'boolean' ? { success: msg['success'] } : {}),
144
+ ...(typeof msg['reasoningContent'] === 'string'
145
+ ? { reasoningContent: msg['reasoningContent'] }
146
+ : {}),
147
+ }));
148
+ const subMessagesIn = isolateContext
149
+ ? isolatedSkillMessages(parentMessages, skillInput)
150
+ : parentMessages;
112
151
  const subGraphState = {
113
- messages: messages.map((msg) => ({
114
- role: msg['role'] ?? 'user',
115
- content: msg['content'] ?? '',
116
- toolCalls: msg['tool_calls'] ?? msg['toolCalls'],
117
- ...(typeof msg['toolCallId'] === 'string' ? { toolCallId: msg['toolCallId'] } : {}),
118
- ...(typeof msg['toolName'] === 'string' ? { toolName: msg['toolName'] } : {}),
119
- ...(typeof msg['success'] === 'boolean' ? { success: msg['success'] } : {}),
120
- ...(typeof msg['reasoningContent'] === 'string'
121
- ? { reasoningContent: msg['reasoningContent'] }
122
- : {}),
123
- })),
124
- // 子图与父图共用 session thread;入口必须 __set,否则 checkpointer 会把父图 messages 再 append 一遍
125
- replaceCheckpointMessages: true,
152
+ messages: subMessagesIn,
153
+ // 默认同父 session thread;隔离时不写父 Checkpoint,避免短历史覆盖父对话
154
+ replaceCheckpointMessages: !isolateContext,
126
155
  userId,
127
156
  delegationToken,
128
157
  agentCode,
129
158
  taskId: state['taskId'],
130
159
  correlationId: state['correlationId'],
131
- sessionId: state['sessionId'],
160
+ sessionId: isolateContext ? undefined : state['sessionId'],
161
+ // ToolNode 用此标志在 interrupt 前拒绝 HITL
162
+ skillIsolateContext: isolateContext || undefined,
132
163
  executionContext: state['executionContext'],
133
164
  loadedTools: scopedTools,
134
165
  loadedSkills: state['loadedSkills'],
@@ -146,7 +177,7 @@ export class SubGraphCompiler {
146
177
  hitlRespondedBy: state['hitlRespondedBy'],
147
178
  };
148
179
  const result = await adapter.runIsolatedLlmStreamFlags(() => adapter.invoke(subGraph, subGraphState));
149
- // RT-6: 记录成功
180
+ // 进程内调试记录;生产记录是上面的 skill Span
150
181
  await auditCtx?.finish({
151
182
  status: 'success',
152
183
  tokenUsage: result.tokenUsage,
@@ -182,19 +213,26 @@ export class SubGraphCompiler {
182
213
  ...(reasoningContent ? { reasoningContent } : {}),
183
214
  },
184
215
  });
185
- const added = subMessages.slice(messages.length);
186
- const outputMessages = added.length > 0
187
- ? added
188
- : result.output
189
- ? [{ role: 'assistant', content: result.output }]
190
- : [];
216
+ const added = subMessages.slice(subMessagesIn.length);
217
+ const outputMessages = isolateContext
218
+ ? isolatedSkillOutput(added.length > 0
219
+ ? added
220
+ : result.output
221
+ ? [{ role: 'assistant', content: result.output }]
222
+ : [])
223
+ : added.length > 0
224
+ ? added
225
+ : result.output
226
+ ? [{ role: 'assistant', content: result.output }]
227
+ : [];
191
228
  const perNode = result.tokenUsage?.perNode;
192
229
  const nestedLastSpanId = (typeof result.finalState?.lastSpanId === 'string' && result.finalState.lastSpanId) ||
193
230
  skillSpanId ||
194
231
  undefined;
195
232
  return {
196
233
  messages: outputMessages,
197
- currentNode: `subgraph:${dsl.name}`,
234
+ currentNode: options?.parentNodeId
235
+ || (dsl.name ? `subgraph:${dsl.name}` : 'subgraph'),
198
236
  ...(nestedLastSpanId ? { lastSpanId: nestedLastSpanId } : {}),
199
237
  ...(Array.isArray(perNode) && perNode.length > 0
200
238
  ? {
@@ -211,18 +249,26 @@ export class SubGraphCompiler {
211
249
  }
212
250
  catch (err) {
213
251
  const hitlPaused = unwrapHitlPaused(err);
214
- if (hitlPaused) {
252
+ // 未隔离:HITL 暂停原样上抛。隔离:转 GRAPH_EXECUTION_ERROR,走 skill.failed,不当 waiting_hitl。
253
+ if (hitlPaused && !isolateContext) {
215
254
  throw err;
216
255
  }
256
+ const failErr = hitlPaused && isolateContext
257
+ ? new AppError(ErrorCodes.GRAPH_EXECUTION_ERROR, SKILL_ISOLATE_HITL_MESSAGE, {
258
+ exposeMessage: true,
259
+ cause: err,
260
+ })
261
+ : err;
262
+ const failMessage = failErr instanceof Error ? failErr.message : String(failErr);
217
263
  await auditCtx?.finish({
218
264
  status: 'failed',
219
- errorMessage: err instanceof Error ? err.message : String(err),
265
+ errorMessage: failMessage,
220
266
  });
221
267
  if (traceCollector && skillSpanId) {
222
268
  try {
223
269
  await traceCollector.completeNode(skillSpanId, {
224
270
  status: 'failed',
225
- error: err instanceof Error ? err.message : String(err),
271
+ error: failMessage,
226
272
  });
227
273
  }
228
274
  catch (completeErr) {
@@ -237,10 +283,10 @@ export class SubGraphCompiler {
237
283
  payload: {
238
284
  skillId: resolvedSkillId,
239
285
  skillName,
240
- error: err instanceof Error ? err.message : String(err),
286
+ error: failMessage,
241
287
  },
242
288
  });
243
- throw err;
289
+ throw failErr;
244
290
  }
245
291
  };
246
292
  return subGraphNodeFn;
@@ -7,8 +7,14 @@
7
7
  * @see design.md §1.4 StateGraph Contract
8
8
  * @see requirements.md Requirement 1 AC1-AC2
9
9
  */
10
- import type { GraphDSL as BaseGraphDSL, GraphNode as BaseGraphNode, GraphEdge as BaseGraphEdge, GraphNodeType, GraphConfig } from '@gt-fe/eap-contracts';
11
- export type { GraphNodeType, GraphConfig };
10
+ import type { GraphDSL as BaseGraphDSL, GraphNode as BaseGraphNode, GraphEdge as BaseGraphEdge, GraphNodeType as ContractsGraphNodeType, GraphConfig } from '@gt-fe/eap-contracts';
11
+ export type { GraphConfig };
12
+ /**
13
+ * GraphNodeType — runtime 侧扩展联合:contracts 官方包的 'rag' 类型尚未合入
14
+ * (客户 MR 跟进中),在此扩展使官方 contracts 零 patch 可用;
15
+ * contracts 合入后此扩展退化为无害超集,可直接删除回归 re-export。
16
+ */
17
+ export type GraphNodeType = ContractsGraphNodeType | 'rag';
12
18
  /**
13
19
  * RuntimeGraphConfig — Runtime 层扩展的 Graph 配置
14
20
  *
@@ -26,7 +32,9 @@ export interface RuntimeGraphConfig extends GraphConfig {
26
32
  * - HITL 标记(interrupt_before / interrupt_after)
27
33
  * - 描述信息(调试/Trace 用)
28
34
  */
29
- export interface GraphNode extends BaseGraphNode {
35
+ export interface GraphNode extends Omit<BaseGraphNode, 'type'> {
36
+ /** 节点类型(contracts 联合 + runtime 扩展 'rag',见 GraphNodeType) */
37
+ type: GraphNodeType;
30
38
  /** 节点显示名称(调试/Trace 用) */
31
39
  label?: string;
32
40
  /** 节点描述 */
@@ -37,8 +45,6 @@ export interface GraphNode extends BaseGraphNode {
37
45
  interrupt?: NodeInterruptConfig;
38
46
  /** 子图引用 ID(type=subgraph 时必填) */
39
47
  subgraphId?: string;
40
- /** 并行分支定义(type=parallel 时使用) */
41
- branches?: string[];
42
48
  }
43
49
  /**
44
50
  * GraphEdge — 基础边定义
@@ -183,6 +189,10 @@ export interface GraphState {
183
189
  executionContext?: PlatformExecutionContext;
184
190
  /** ExecuteConfig.forceToolServer — 单次 invoke 强制走 Gateway 的 toolName */
185
191
  forceToolServer?: string[];
192
+ /** 请求级检索范围(前端圈定 fileIds/dirPaths;rag 节点读取,仅本地检索源生效) */
193
+ ragScope?: import('../../graph/nodes/rag/types.js').RagScope;
194
+ /** 会话级数据源勾选(数据源 id 列表;undefined = 全部启用) */
195
+ ragSources?: string[];
186
196
  /** GraphLoader 注入:toolName → 运行期 manifest 引用 */
187
197
  loadedTools?: import('../../tool/loaded-tool-definition.js').LoadedToolMap;
188
198
  /** GraphLoader 注入:已装配 Skill 列表 */
@@ -212,8 +222,20 @@ export interface GraphState {
212
222
  * 不入 Checkpoint。
213
223
  */
214
224
  traceCollector?: import('../../contracts/trace-collector.js').ITraceCollector;
225
+ /**
226
+ * 当前节点跑在 Skill 隔离子图内。为 true 时禁止 HITL interrupt
227
+ *(隔离不写父 session Checkpoint,确认态会丢)。
228
+ */
229
+ skillIsolateContext?: boolean;
215
230
  /** 当前 Skill 子图 createNode 返回的 span id;子图内 Tool span 的 parentNodeId */
216
231
  skillSpanId?: string;
232
+ /**
233
+ * HITL 续跑:暂停时已 create 的 tool Span id。
234
+ * 按 hitlToolCallId 复用,禁止再 createNode。
235
+ */
236
+ toolSpanId?: string;
237
+ /** 与 toolSpanId 配对的 toolCallId(同一 assistant 多 tool 时只复用这一次调用) */
238
+ hitlToolCallId?: string;
217
239
  /** 最近一次 createNode 的 span id;Checkpoint 引用挂在此 Span 上 */
218
240
  lastSpanId?: string;
219
241
  /**
@@ -384,6 +384,8 @@ export class HitlHandler {
384
384
  ...cleaned,
385
385
  hitlChoice: choice,
386
386
  hitlRespondedBy: userId,
387
+ ...(waiting.toolSpanId ? { toolSpanId: waiting.toolSpanId } : {}),
388
+ ...(waiting.toolCallId ? { hitlToolCallId: waiting.toolCallId } : {}),
387
389
  };
388
390
  const now = new Date().toISOString();
389
391
  await this.checkpointSaver.save(waiting.threadId, stripCheckpointCollectors({
@@ -1,2 +1,3 @@
1
1
  export { LLMNode, type LLMNodeConfig, type LLMResponse, type LLMNodeToolCall, type LLMChunk, type ILLMProvider, type LLMToolDefinition, } from './llm-node.js';
2
2
  export { ToolNode, type ToolNodeProgress } from './tool-node.js';
3
+ export * from './rag/index.js';
@@ -1,2 +1,3 @@
1
1
  export { LLMNode, } from './llm-node.js';
2
2
  export { ToolNode } from './tool-node.js';
3
+ export * from './rag/index.js';
@@ -10,6 +10,7 @@
10
10
  * @see requirements.md Requirement 1 AC6, Requirement 9
11
11
  */
12
12
  import type { NodeRetryConfig, GraphMessage } from '../contracts/executable-graph.js';
13
+ import { type ExecutionInterceptors } from '../../execution/interceptors.js';
13
14
  /**
14
15
  * LLMNodeConfig — LLM 节点配置
15
16
  */
@@ -132,6 +133,7 @@ type LangGraphState = Record<string, unknown>;
132
133
  * 5. 将 LLM 响应追加到 state.messages 并返回
133
134
  */
134
135
  export declare class LLMNode {
136
+ private readonly interceptors?;
135
137
  private readonly config;
136
138
  private readonly provider;
137
139
  private readonly retryConfig;
@@ -145,7 +147,7 @@ export declare class LLMNode {
145
147
  * @param streaming - 是否启用流式输出(默认 false)
146
148
  * @param onChunk - 流式模式下每个文本片段回调(用于 SSE response.chunk)
147
149
  */
148
- constructor(nodeId: string, config: LLMNodeConfig, provider: ILLMProvider, streaming?: boolean, onChunk?: (chunk: string) => void);
150
+ constructor(nodeId: string, config: LLMNodeConfig, provider: ILLMProvider, streaming?: boolean, onChunk?: (chunk: string) => void, interceptors?: Pick<ExecutionInterceptors, "beforeModel" | "afterModel"> | undefined);
149
151
  /**
150
152
  * execute — StateGraph 节点函数
151
153
  *
@@ -12,6 +12,8 @@
12
12
  import { formatUnknownError } from '../../utils/format-error.js';
13
13
  import { buildContextUsage, turnSectionChars } from '../../token/context-window.js';
14
14
  import { isAbortError } from '../../tool/timeout.js';
15
+ import { emitRuntimeLog } from '../../common/logger.js';
16
+ import { decideBeforeModel, } from '../../execution/interceptors.js';
15
17
  // ─────────────────────────────────────────────
16
18
  // Default retry config
17
19
  // ─────────────────────────────────────────────
@@ -38,6 +40,7 @@ const DEFAULT_RETRY_CONFIG = {
38
40
  * 5. 将 LLM 响应追加到 state.messages 并返回
39
41
  */
40
42
  export class LLMNode {
43
+ interceptors;
41
44
  config;
42
45
  provider;
43
46
  retryConfig;
@@ -51,7 +54,8 @@ export class LLMNode {
51
54
  * @param streaming - 是否启用流式输出(默认 false)
52
55
  * @param onChunk - 流式模式下每个文本片段回调(用于 SSE response.chunk)
53
56
  */
54
- constructor(nodeId, config, provider, streaming = false, onChunk) {
57
+ constructor(nodeId, config, provider, streaming = false, onChunk, interceptors) {
58
+ this.interceptors = interceptors;
55
59
  this.nodeId = nodeId;
56
60
  this.config = config;
57
61
  this.provider = provider;
@@ -71,10 +75,30 @@ export class LLMNode {
71
75
  const messages = this.extractMessages(state);
72
76
  // 注入 system prompt
73
77
  const messagesWithSystem = this.prependSystemPrompt(messages);
78
+ const modelInput = {
79
+ messages: messagesWithSystem,
80
+ config: this.config,
81
+ nodeId: this.nodeId,
82
+ };
83
+ const gate = await decideBeforeModel(this.interceptors?.beforeModel, modelInput);
84
+ if (gate.action === 'deny') {
85
+ throw new Error(gate.reason);
86
+ }
87
+ const messagesForModel = gate.messages ?? messagesWithSystem;
74
88
  const startedAt = Date.now();
75
89
  // 带重试的 LLM 调用
76
- const response = await this.invokeWithRetry(messagesWithSystem);
90
+ const response = await this.invokeWithRetry(messagesForModel);
77
91
  const durationMs = Date.now() - startedAt;
92
+ try {
93
+ await this.interceptors?.afterModel?.({ ...modelInput, messages: messagesForModel }, response);
94
+ }
95
+ catch (err) {
96
+ emitRuntimeLog('warn', 'runtime.intercept', 'afterModel failed', {
97
+ event: 'runtime.intercept.after_model_failed',
98
+ nodeId: this.nodeId,
99
+ error: err instanceof Error ? err.message : String(err),
100
+ });
101
+ }
78
102
  // 构建 assistant message(保留 reasoningContent 供后续 HTTP 回传)
79
103
  const assistantMessage = {
80
104
  role: 'assistant',
@@ -0,0 +1,20 @@
1
+ /**
2
+ * dedup — 确定性去重(纯函数,不经 LLM)
3
+ *
4
+ * 精确去重(源内、fuse 前):sourceId:rawId + 归一化 content hash。
5
+ * 约束:入参须先经收集侧(react-loop pushDocs)消除"同 id 不同 content"撞号
6
+ * (重编号 `${rawId}#r${n}`)——本函数 id 命中即弃仅对真重复成立;
7
+ * fuse 的跨源键 sourceId:rawId 同样依赖该唯一性。
8
+ * 近重去重(融合后、降噪判定前,M4):字符 3-gram Jaccard 超 nearDupThreshold
9
+ * 判近重,保留序列靠前者(design-v2 §6.3 ①;跨源候选集几乎不重叠,属低频兜底)。
10
+ */
11
+ import type { RetrievalDoc } from './types.js';
12
+ export declare function dedupExact(docs: RetrievalDoc[]): RetrievalDoc[];
13
+ /**
14
+ * 归一化内容相等(与 dedupExact 内容去重同口径):"重复"判定单一出处——
15
+ * 收集侧(react-loop pushDocs)跳过同 id 真重复时必须与融合前去重用同一口径,
16
+ * 两处口径分叉会导致同 id 同排版差异的文档一处跳过一处保留。
17
+ */
18
+ export declare function sameContent(a: string, b: string): boolean;
19
+ /** 近重去重:字符 n-gram Jaccard 超阈值即判近重,保留序列靠前者 */
20
+ export declare function dedupNearDup(docs: RetrievalDoc[], threshold?: number): RetrievalDoc[];
@@ -0,0 +1,77 @@
1
+ /**
2
+ * dedup — 确定性去重(纯函数,不经 LLM)
3
+ *
4
+ * 精确去重(源内、fuse 前):sourceId:rawId + 归一化 content hash。
5
+ * 约束:入参须先经收集侧(react-loop pushDocs)消除"同 id 不同 content"撞号
6
+ * (重编号 `${rawId}#r${n}`)——本函数 id 命中即弃仅对真重复成立;
7
+ * fuse 的跨源键 sourceId:rawId 同样依赖该唯一性。
8
+ * 近重去重(融合后、降噪判定前,M4):字符 3-gram Jaccard 超 nearDupThreshold
9
+ * 判近重,保留序列靠前者(design-v2 §6.3 ①;跨源候选集几乎不重叠,属低频兜底)。
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ export function dedupExact(docs) {
13
+ const seenIds = new Set();
14
+ const seenContent = new Set();
15
+ const out = [];
16
+ for (const d of docs) {
17
+ const idKey = `${d.sourceId}:${d.rawId}`;
18
+ const contentKey = hashContent(d.content);
19
+ if (seenIds.has(idKey) || seenContent.has(contentKey))
20
+ continue;
21
+ seenIds.add(idKey);
22
+ seenContent.add(contentKey);
23
+ out.push(d);
24
+ }
25
+ return out;
26
+ }
27
+ function hashContent(content) {
28
+ // 归一化:压空白后取 hash,挡"内容相同仅排版不同"
29
+ const normalized = content.replace(/\s+/g, ' ').trim().toLowerCase();
30
+ return createHash('sha1').update(normalized).digest('hex');
31
+ }
32
+ /**
33
+ * 归一化内容相等(与 dedupExact 内容去重同口径):"重复"判定单一出处——
34
+ * 收集侧(react-loop pushDocs)跳过同 id 真重复时必须与融合前去重用同一口径,
35
+ * 两处口径分叉会导致同 id 同排版差异的文档一处跳过一处保留。
36
+ */
37
+ export function sameContent(a, b) {
38
+ return hashContent(a) === hashContent(b);
39
+ }
40
+ /** 近重去重:字符 n-gram Jaccard 超阈值即判近重,保留序列靠前者 */
41
+ export function dedupNearDup(docs, threshold = 0.8) {
42
+ const kept = [];
43
+ const keptGrams = [];
44
+ for (const d of docs) {
45
+ const grams = charGrams(d.content);
46
+ let near = false;
47
+ for (const g of keptGrams) {
48
+ if (jaccard(grams, g) > threshold) {
49
+ near = true;
50
+ break;
51
+ }
52
+ }
53
+ if (near)
54
+ continue;
55
+ kept.push(d);
56
+ keptGrams.push(grams);
57
+ }
58
+ return kept;
59
+ }
60
+ const NGRAM_SIZE = 3;
61
+ function charGrams(content) {
62
+ const norm = content.replace(/\s+/g, ' ').trim();
63
+ const grams = new Set();
64
+ for (let i = 0; i + NGRAM_SIZE <= norm.length; i++) {
65
+ grams.add(norm.slice(i, i + NGRAM_SIZE));
66
+ }
67
+ return grams;
68
+ }
69
+ function jaccard(a, b) {
70
+ if (a.size === 0 || b.size === 0)
71
+ return 0;
72
+ let inter = 0;
73
+ for (const x of a)
74
+ if (b.has(x))
75
+ inter++;
76
+ return inter / (a.size + b.size - inter);
77
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * denoise — 降噪管线(design-v2 §6.3,M4)
3
+ *
4
+ * 数万 token 候选压成数百 token,LLM 只做判定与选择、绝不产出正文:
5
+ * ① 近重去重(字符 n-gram Jaccard,纯函数,确定性优先)
6
+ * ② 一次结构化 LLM 判定(主力模型,temp 0)→ DenoiseVerdict[]
7
+ * { rawId, relevant, reason, keepSentences }——rawId 为复合键 sourceId:rawId
8
+ * (rawId 仅源内唯一;payload 标签、verdict 匹配、filtered 项三处必须同用复合键,
9
+ * 裸 id 跨源碰撞会让两份文档共享同一 verdict:错滤另一源、按别家句子编号截正文)
10
+ * ③ 代码按句子编号拼装原文(零改写:返回文本逐字出自原文句子)
11
+ *
12
+ * 防御策略(§6.5 误滤宁保守):
13
+ * verdict 调用或解析失败 / 文档缺 verdict 项 / keepSentences 无有效编号 → 整篇保留;
14
+ * 判定存活集为空 → react-loop 走 QUERY_REWRITE 重写兜底(本模块只报 verdictEmpty)。
15
+ */
16
+ import type { ILLMProvider } from '../llm-node.js';
17
+ import type { DenoiseVerdict, RagTokenUsage, RetrievalDoc } from './types.js';
18
+ export declare const RAG_EMPTY_MARKER = "[rag] \u672A\u627E\u5230\u9AD8\u76F8\u5173\u7ED3\u679C";
19
+ /** 格式化(降噪后的)文档集为最终上下文 */
20
+ export declare function formatContext(query: string, docs: RetrievalDoc[]): string;
21
+ export interface DenoiseInput {
22
+ query: string;
23
+ /** 融合后的候选文档集(可能数万 token) */
24
+ docs: RetrievalDoc[];
25
+ llmProvider: ILLMProvider;
26
+ /** 主力模型(与决策同款,不分档) */
27
+ model: string;
28
+ nodeId: string;
29
+ /** 近重判定阈值,缺省 0.8 */
30
+ nearDupThreshold?: number;
31
+ tokenUsage: RagTokenUsage[];
32
+ }
33
+ export interface DenoiseResult {
34
+ /** 存活文档(正文已按 keepSentences 截取) */
35
+ docs: RetrievalDoc[];
36
+ contextText: string;
37
+ /** 判定后存活集是否为空(react-loop 据此触发重写兜底) */
38
+ verdictEmpty: boolean;
39
+ /** 被滤文档与原因(空结果重写定向 / rag.denoise 事件用);rawId 为复合键 sourceId:rawId(rewrite 归因整键匹配用) */
40
+ filtered: Array<{
41
+ rawId: string;
42
+ reason: string;
43
+ }>;
44
+ }
45
+ export declare function denoise(input: DenoiseInput): Promise<DenoiseResult>;
46
+ export declare function rewriteQuery(input: {
47
+ query: string;
48
+ reasons: string[];
49
+ llmProvider: ILLMProvider;
50
+ model: string;
51
+ nodeId: string;
52
+ tokenUsage: RagTokenUsage[];
53
+ }): Promise<string | undefined>;
54
+ /** 确定性分句:按中英文句终符与换行切,句符随句保留(零改写:句子为原文子串) */
55
+ export declare function splitSentences(content: string): string[];
56
+ /** 解析判定输出:剥代码围栏、截取首尾方括号;解析失败返回 undefined(保守放行全部)。
57
+ * rawId 不校验复合键形态——LLM 抄错(裸 id/截断)时 find 不中,该文档无 verdict 即整篇保留,仍走保守路径 */
58
+ export declare function parseVerdicts(text: string): DenoiseVerdict[] | undefined;