@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
@@ -11,9 +11,22 @@
11
11
  * - 支持任意 OpenAI 兼容 API(OpenAI / DeepSeek / 本地 Ollama 等)
12
12
  * - 连接配置由 Foundation 统一管理,Runtime 无感知
13
13
  */
14
- import { mergeModelCallParams } from '@gt-fe/eap-contracts';
14
+ import { AppError, ErrorCodes, mergeModelCallParams } from '@gt-fe/eap-contracts';
15
15
  import { EchoLLMProvider } from './echo-llm-provider.js';
16
16
  import { DEFAULT_LLM_HTTP_TIMEOUT_MS, HttpLLMProvider } from './http-llm-provider.js';
17
+ export const MODELS_INVOKE_STREAM_MESSAGE = 'EAP_RUNTIME_MODELS_INVOKE 实验口只支持同步 invoke(POST /models/invoke),不支持 stream。关掉该开关,或改走 Foundation /models/select + 本机 HTTP。';
18
+ export function isFoundationModelsInvokeEnabled(env = process.env) {
19
+ const raw = env.EAP_RUNTIME_MODELS_INVOKE;
20
+ return raw === '1' || raw === 'true';
21
+ }
22
+ /** `/models/invoke` 无真流式;开了该开关时 RoutingLLMProvider.stream 必须失败。 */
23
+ export function assertFoundationModelsInvokeAllowsStream(env = process.env) {
24
+ if (!isFoundationModelsInvokeEnabled(env))
25
+ return;
26
+ throw new AppError(ErrorCodes.GRAPH_EXECUTION_ERROR, MODELS_INVOKE_STREAM_MESSAGE, {
27
+ exposeMessage: true,
28
+ });
29
+ }
17
30
  export class RoutingLLMProvider {
18
31
  foundationUrl;
19
32
  fallback;
@@ -29,6 +42,7 @@ export class RoutingLLMProvider {
29
42
  return provider.invoke(messages, this.withCallDefaults(config, selected));
30
43
  }
31
44
  async *stream(messages, config) {
45
+ assertFoundationModelsInvokeAllowsStream();
32
46
  const selected = await this.selectModel(config);
33
47
  const provider = this.createProviderForModel(selected);
34
48
  yield* provider.stream(messages, this.withCallDefaults(config, selected));
@@ -72,7 +86,13 @@ export class RoutingLLMProvider {
72
86
  }
73
87
  return { name: config.model ?? 'echo', provider: 'local', connection: null };
74
88
  }
89
+ useFoundationInvoke() {
90
+ return isFoundationModelsInvokeEnabled();
91
+ }
75
92
  createProviderForModel(selected) {
93
+ if (this.useFoundationInvoke()) {
94
+ return new FoundationModelsInvokeProvider(this.foundationUrl, selected.name, this.turnLog);
95
+ }
76
96
  // 优先使用 Foundation 返回的连接配置
77
97
  if (selected.connection) {
78
98
  return new HttpLLMProvider({
@@ -96,3 +116,48 @@ export class RoutingLLMProvider {
96
116
  return this.fallback;
97
117
  }
98
118
  }
119
+ /**
120
+ * Foundation `/models/invoke` 实验口。默认关。
121
+ * 只实现同步 invoke;stream 拒绝(无真流式、无鉴权头)。
122
+ * Foundation 未提供该接口时不要开 EAP_RUNTIME_MODELS_INVOKE。
123
+ */
124
+ class FoundationModelsInvokeProvider {
125
+ foundationUrl;
126
+ model;
127
+ turnLog;
128
+ constructor(foundationUrl, model, turnLog) {
129
+ this.foundationUrl = foundationUrl;
130
+ this.model = model;
131
+ this.turnLog = turnLog;
132
+ }
133
+ async invoke(messages, config) {
134
+ const res = await fetch(`${this.foundationUrl}/api/v1/models/invoke`, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({
138
+ model: config.model ?? this.model,
139
+ messages,
140
+ temperature: config.temperature,
141
+ maxTokens: config.maxTokens,
142
+ }),
143
+ });
144
+ if (!res.ok) {
145
+ throw new Error(`Foundation /models/invoke HTTP ${res.status}`);
146
+ }
147
+ const body = (await res.json());
148
+ const content = body.data?.content ?? body.content ?? '';
149
+ void this.turnLog;
150
+ return {
151
+ content,
152
+ inputTokens: body.data?.inputTokens ?? 0,
153
+ outputTokens: body.data?.outputTokens ?? 0,
154
+ model: body.data?.model ?? config.model ?? this.model,
155
+ };
156
+ }
157
+ async *stream(_messages, _config) {
158
+ assertFoundationModelsInvokeAllowsStream();
159
+ throw new AppError(ErrorCodes.GRAPH_EXECUTION_ERROR, MODELS_INVOKE_STREAM_MESSAGE, {
160
+ exposeMessage: true,
161
+ });
162
+ }
163
+ }
@@ -16,6 +16,7 @@ import type { SessionContextService } from '../session/session-context.service.j
16
16
  import type { MessageService } from '../session/message.service.js';
17
17
  import type { SessionService, SessionRecord, MessageRecord } from '../session/session.service.js';
18
18
  import { type ILlmTurnLog, type LlmTurnIndexEntry, type LlmTurnLogKind, type LlmTurnRecord } from '../debug/llm-turn-log.js';
19
+ import { type SessionCompressionOptions } from '../session/apply-session-compression.js';
19
20
  import type { SSEEvent } from '../sse/contracts/sse-emitter.contract.js';
20
21
  import type { TaskService } from '../task/task.service.js';
21
22
  import type { ExecuteParams, ExecuteResult, ForkParams, ForkOverrides, ForkResult, HitlResponse, ResumeHitlResult, SeedSessionMessagesParams, SeedSessionMessagesResult } from './types.js';
@@ -46,6 +47,10 @@ export interface AgentRuntimeDeps {
46
47
  */
47
48
  traceCollector?: ITraceCollector;
48
49
  recordForkRelation?: RecordForkRelation;
50
+ /** 历史压缩;默认关。见 EAP_RUNTIME_COMPRESSION */
51
+ sessionCompression?: SessionCompressionOptions;
52
+ /** 长期记忆注入;默认关。见 EAP_RUNTIME_MEMORY */
53
+ sessionMemory?: import('../session/apply-session-memory.js').SessionMemoryOptions;
49
54
  }
50
55
  /** 一次对话详情:消息 + 每步 LLM 入参/响应。File / 内存 / Postgres 同一套结构。 */
51
56
  export interface ConversationDetail {
@@ -12,11 +12,15 @@ import { resolveAgentSystemPromptFromLoader } from '../registry/graph-loader.js'
12
12
  import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
13
13
  import { assembleLoadedTools } from '../tool/harness-primitives.js';
14
14
  import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
15
- import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscriptFromStreamEvents, extractHitlThreadId, extractStreamReasoning, extractStreamTextDelta, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, } from '../session/persist-turn-messages.js';
15
+ import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscriptFromStreamEvents, extractHitlThreadId, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, appendTerminalToolResult, } from '../session/persist-turn-messages.js';
16
+ import { iterSessionCompression, } from '../session/apply-session-compression.js';
17
+ import { applySessionMemory } from '../session/apply-session-memory.js';
18
+ import { resolveTokenDetail, shouldEmitTokenSse } from '../token/token-detail.js';
16
19
  import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
17
20
  import { resolveExecutionContext } from '../governance/governance-client.js';
18
21
  import { resolveAgentCode } from '../common/agent-code.js';
19
22
  import { resolveTraceId } from '../common/trace-id.js';
23
+ import { collectStreamNodeTokenUsage, recordTaskTokenUsages, tokenUsagesFromLlmChannel, } from '../token/record-task-token-usage.js';
20
24
  import { resolveSkillInputFromInvokeInput } from '../registry/skill-io-guard.js';
21
25
  const HITL_TERMINAL_STATUSES = [
22
26
  'success',
@@ -207,6 +211,8 @@ export class AgentRuntime {
207
211
  executionContext: ctx,
208
212
  userId,
209
213
  traceId: resolveTraceId(params),
214
+ ragScope: params.ragScope,
215
+ ragSources: params.ragSources,
210
216
  }, userId);
211
217
  return response.data;
212
218
  }
@@ -273,7 +279,31 @@ export class AgentRuntime {
273
279
  ? (await this.deps.graphLoader.resolveAgentDir(agentCode, params.agentVersion)) ?? undefined
274
280
  : undefined;
275
281
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, params.agentVersion);
276
- const history = messageRecordsToGraphMessages(sessionContext.messages);
282
+ let history = messageRecordsToGraphMessages(sessionContext.messages);
283
+ for await (const step of iterSessionCompression({
284
+ history,
285
+ sessionId,
286
+ userId,
287
+ taskId: task.id,
288
+ options: {
289
+ ...this.deps.sessionCompression,
290
+ turnLog: this.deps.sessionCompression?.turnLog ?? this.deps.turnLog,
291
+ },
292
+ })) {
293
+ if (step.kind === 'history') {
294
+ history = step.messages;
295
+ continue;
296
+ }
297
+ yield this.toSSE(step.kind === 'started' ? 'session.compression.started' : 'session.compression.completed', correlationId, task.id, { ...step.payload });
298
+ }
299
+ history = await applySessionMemory({
300
+ history,
301
+ userId,
302
+ agentCode,
303
+ query: typeof params.input === 'string' ? params.input : undefined,
304
+ options: this.deps.sessionMemory,
305
+ });
306
+ const tokenDetail = resolveTokenDetail(params.config?.tokenDetail);
277
307
  const messages = await buildTurnGraphMessages({
278
308
  history,
279
309
  input: params.input,
@@ -281,11 +311,12 @@ export class AgentRuntime {
281
311
  threadId: sessionId,
282
312
  });
283
313
  const startTime = Date.now();
284
- let assistantText = '';
314
+ const assistantBuckets = createStreamAssistantBuckets();
285
315
  let toolCallReasoningContent;
286
316
  let assistantReasoningContent;
287
317
  const pendingToolCalls = [];
288
318
  const toolResults = [];
319
+ const nodeTokenUsages = [];
289
320
  try {
290
321
  for await (const runtimeEvent of this.deps.graphEngine.stream(graph, {
291
322
  messages,
@@ -298,6 +329,8 @@ export class AgentRuntime {
298
329
  sessionId,
299
330
  executionContext: ctx,
300
331
  forceToolServer: params.config?.forceToolServer,
332
+ ragScope: params.ragScope,
333
+ ragSources: params.ragSources,
301
334
  loadedTools,
302
335
  loadedSkills,
303
336
  skillDefinitions,
@@ -321,32 +354,18 @@ export class AgentRuntime {
321
354
  toolCallReasoningContent = p.reasoningContent;
322
355
  }
323
356
  }
324
- if (runtimeEvent.type === 'tool.result') {
325
- const p = runtimeEvent.payload;
326
- if (p.toolCallId && p.toolName) {
327
- toolResults.push({
328
- toolCallId: p.toolCallId,
329
- toolName: p.toolName,
330
- content: p.content,
331
- success: p.success !== false,
332
- });
333
- }
334
- }
357
+ // 终态 tool.result / tool.error 回填历史;willRetry 的 error 不进 transcript
358
+ appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
335
359
  const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
336
360
  if (streamedReasoning)
337
361
  assistantReasoningContent = streamedReasoning;
338
- const delta = extractStreamTextDelta(runtimeEvent.type, runtimeEvent.payload);
339
- if (delta) {
340
- if (runtimeEvent.type === 'response.final') {
341
- if (delta.trim())
342
- assistantText = delta;
343
- }
344
- else {
345
- assistantText += delta;
346
- }
362
+ applyStreamTextDelta(assistantBuckets, runtimeEvent.type, runtimeEvent.payload);
363
+ collectStreamNodeTokenUsage(runtimeEvent, nodeTokenUsages);
364
+ if (shouldEmitTokenSse(tokenDetail, runtimeEvent.type)) {
365
+ yield this.runtimeEventToSSE(runtimeEvent, correlationId, task.id);
347
366
  }
348
- yield this.runtimeEventToSSE(runtimeEvent, correlationId, task.id);
349
367
  }
368
+ await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
350
369
  await this.deps.taskService.updateStatus(task.id, 'success');
351
370
  await persistTurnTranscript(this.deps.messageService, {
352
371
  sessionId,
@@ -355,7 +374,9 @@ export class AgentRuntime {
355
374
  messages: buildTurnTranscriptFromStreamEvents({
356
375
  pendingToolCalls,
357
376
  toolResults,
358
- assistantText,
377
+ assistantTexts: streamAssistantTexts(assistantBuckets),
378
+ assistantText: visibleStreamAssistantText(assistantBuckets),
379
+ finishPoints: graph.finishPoints,
359
380
  toolCallReasoningContent,
360
381
  assistantReasoningContent,
361
382
  }),
@@ -368,6 +389,7 @@ export class AgentRuntime {
368
389
  catch (err) {
369
390
  const hitlPaused = unwrapHitlPaused(err);
370
391
  if (hitlPaused) {
392
+ await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
371
393
  await this.deps.taskService.updateStatus(task.id, 'waiting_hitl', buildHitlWaitingTaskOutput({
372
394
  threadId: hitlPaused.threadId,
373
395
  question: hitlPaused.question,
@@ -385,6 +407,7 @@ export class AgentRuntime {
385
407
  return;
386
408
  }
387
409
  const message = err instanceof Error ? err.message : 'Unknown error';
410
+ await recordTaskTokenUsages(this.deps.taskService, task.id, nodeTokenUsages);
388
411
  await this.deps.taskService.updateStatus(task.id, 'failed', message);
389
412
  yield this.toSSE('task.error', correlationId, task.id, {
390
413
  code: 'runtime.graph.execution_error',
@@ -404,7 +427,7 @@ export class AgentRuntime {
404
427
  let output = '';
405
428
  let waiting;
406
429
  let sawWait = false;
407
- let assistantText = '';
430
+ const assistantBuckets = createStreamAssistantBuckets();
408
431
  for await (const event of this.streamResumeHitl(taskId, response)) {
409
432
  if (event.type === 'hitl.waited') {
410
433
  sawWait = true;
@@ -416,21 +439,12 @@ export class AgentRuntime {
416
439
  };
417
440
  output = waiting.question;
418
441
  }
419
- const delta = extractStreamTextDelta(event.type, event.data);
420
- if (delta) {
421
- if (event.type === 'response.final') {
422
- if (delta.trim())
423
- assistantText = delta;
424
- }
425
- else {
426
- assistantText += delta;
427
- }
428
- }
442
+ applyStreamTextDelta(assistantBuckets, event.type, event.data);
429
443
  }
430
444
  if (sawWait) {
431
445
  return { taskId, status: 'waiting_hitl', output, hitl: waiting };
432
446
  }
433
- return { taskId, status: 'success', output: assistantText || output };
447
+ return { taskId, status: 'success', output: visibleStreamAssistantText(assistantBuckets) || output };
434
448
  }
435
449
  /**
436
450
  * HITL 恢复的流式入口:新开一条 SSE(第一条 stream 在 `hitl.waited` 后已结束)。
@@ -460,6 +474,7 @@ export class AgentRuntime {
460
474
  if (!claimed) {
461
475
  throw new AppError(ErrorCodes.TASK_HITL_INVALID_STATE, `Task ${taskId} is no longer waiting for HITL`, { exposeMessage: true });
462
476
  }
477
+ const tokenDetail = resolveTokenDetail(response.tokenDetail);
463
478
  yield this.toSSE('hitl.resumed', correlationId, taskId, {
464
479
  choice: response.choice,
465
480
  userId: response.userId,
@@ -511,11 +526,12 @@ export class AgentRuntime {
511
526
  traceCollector: this.deps.traceCollector,
512
527
  };
513
528
  const startTime = Date.now();
514
- let assistantText = '';
529
+ const assistantBuckets = createStreamAssistantBuckets();
515
530
  let toolCallReasoningContent;
516
531
  let assistantReasoningContent;
517
532
  const pendingToolCalls = [];
518
533
  const toolResults = [];
534
+ const nodeTokenUsages = [];
519
535
  try {
520
536
  for await (const runtimeEvent of this.deps.graphEngine.stream(graph, continueState)) {
521
537
  if (runtimeEvent.type === 'tool.called') {
@@ -531,33 +547,18 @@ export class AgentRuntime {
531
547
  toolCallReasoningContent = p.reasoningContent;
532
548
  }
533
549
  }
534
- if (runtimeEvent.type === 'tool.result' || runtimeEvent.type === 'tool.error') {
535
- const p = runtimeEvent.payload;
536
- if (p.toolCallId && p.toolName) {
537
- toolResults.push({
538
- toolCallId: p.toolCallId,
539
- toolName: p.toolName,
540
- content: p.content ?? p.error,
541
- success: runtimeEvent.type === 'tool.result' && p.success !== false,
542
- });
543
- }
544
- }
550
+ appendTerminalToolResult(runtimeEvent.type, runtimeEvent.payload, toolResults);
545
551
  const streamedReasoning = extractStreamReasoning(runtimeEvent.type, runtimeEvent.payload);
546
552
  if (streamedReasoning)
547
553
  assistantReasoningContent = streamedReasoning;
548
- const delta = extractStreamTextDelta(runtimeEvent.type, runtimeEvent.payload);
549
- if (delta) {
550
- if (runtimeEvent.type === 'response.final') {
551
- if (delta.trim())
552
- assistantText = delta;
553
- }
554
- else {
555
- assistantText += delta;
556
- }
554
+ applyStreamTextDelta(assistantBuckets, runtimeEvent.type, runtimeEvent.payload);
555
+ collectStreamNodeTokenUsage(runtimeEvent, nodeTokenUsages);
556
+ if (shouldEmitTokenSse(tokenDetail, runtimeEvent.type)) {
557
+ yield this.runtimeEventToSSE(runtimeEvent, correlationId, taskId);
557
558
  }
558
- yield this.runtimeEventToSSE(runtimeEvent, correlationId, taskId);
559
559
  }
560
- await this.deps.taskService.updateStatus(taskId, 'success', assistantText || undefined);
560
+ await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
561
+ await this.deps.taskService.updateStatus(taskId, 'success', visibleStreamAssistantText(assistantBuckets) || undefined);
561
562
  if (sessionId) {
562
563
  await persistTurnTranscript(this.deps.messageService, {
563
564
  sessionId,
@@ -566,7 +567,9 @@ export class AgentRuntime {
566
567
  messages: buildTurnTranscriptFromStreamEvents({
567
568
  pendingToolCalls,
568
569
  toolResults,
569
- assistantText,
570
+ assistantTexts: streamAssistantTexts(assistantBuckets),
571
+ assistantText: visibleStreamAssistantText(assistantBuckets),
572
+ finishPoints: graph.finishPoints,
570
573
  toolCallReasoningContent,
571
574
  assistantReasoningContent,
572
575
  }),
@@ -580,6 +583,7 @@ export class AgentRuntime {
580
583
  catch (err) {
581
584
  const hitlPaused = unwrapHitlPaused(err);
582
585
  if (hitlPaused) {
586
+ await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
583
587
  await this.deps.taskService.updateStatus(taskId, 'waiting_hitl', buildHitlWaitingTaskOutput({
584
588
  threadId: hitlPaused.threadId,
585
589
  question: hitlPaused.question,
@@ -600,6 +604,7 @@ export class AgentRuntime {
600
604
  }
601
605
  const appErr = err instanceof AppError ? err : AppError.fromUnknown(err);
602
606
  const failMsg = appErr.exposeMessage ? appErr.message : '系统内部错误';
607
+ await recordTaskTokenUsages(this.deps.taskService, taskId, nodeTokenUsages);
603
608
  await this.deps.taskService.updateStatus(taskId, 'failed', failMsg);
604
609
  yield this.toSSE('task.error', correlationId, taskId, {
605
610
  code: appErr.code,
@@ -662,8 +667,14 @@ export class AgentRuntime {
662
667
  ? forkedCheckpoint.state.agentSystemPrompt
663
668
  : undefined);
664
669
  try {
670
+ const forkState = { ...forkedCheckpoint.state };
671
+ delete forkState.toolSpanId;
672
+ delete forkState.hitlToolCallId;
665
673
  const result = await this.deps.graphEngine.invoke(graph, {
666
- ...forkedCheckpoint.state,
674
+ messages: Array.isArray(forkState.messages)
675
+ ? forkState.messages
676
+ : [],
677
+ ...forkState,
667
678
  agentVersion,
668
679
  agentDir,
669
680
  agentSystemPrompt,
@@ -676,6 +687,19 @@ export class AgentRuntime {
676
687
  traceId,
677
688
  traceCollector: this.deps.traceCollector,
678
689
  });
690
+ if (result.tokenUsage.perNode && result.tokenUsage.perNode.length > 0) {
691
+ await recordTaskTokenUsages(this.deps.taskService, task.id, result.tokenUsage.perNode);
692
+ }
693
+ else if (result.tokenUsage.total > 0) {
694
+ await recordTaskTokenUsages(this.deps.taskService, task.id, [
695
+ {
696
+ nodeId: 'graph',
697
+ model: 'unknown',
698
+ inputTokens: result.tokenUsage.input,
699
+ outputTokens: result.tokenUsage.output,
700
+ },
701
+ ]);
702
+ }
679
703
  await this.deps.taskService.updateStatus(task.id, 'success', result.output);
680
704
  if (this.deps.recordForkRelation) {
681
705
  try {
@@ -707,6 +731,20 @@ export class AgentRuntime {
707
731
  }
708
732
  catch (err) {
709
733
  const message = err instanceof Error ? err.message : 'Unknown error';
734
+ const sessionId = typeof forkedCheckpoint.state.sessionId === 'string'
735
+ ? forkedCheckpoint.state.sessionId
736
+ : undefined;
737
+ const threadId = resolveCheckpointThreadId({
738
+ sessionId,
739
+ taskId: task.id,
740
+ });
741
+ const ck = await this.deps.checkpointSaver.load(threadId);
742
+ const baseline = tokenUsagesFromLlmChannel(forkedCheckpoint.state);
743
+ const latest = tokenUsagesFromLlmChannel(ck?.state ?? forkedCheckpoint.state);
744
+ const usages = latest.length >= baseline.length ? latest.slice(baseline.length) : latest;
745
+ await recordTaskTokenUsages(this.deps.taskService, task.id, usages);
746
+ const input_tokens = usages.reduce((sum, row) => sum + row.inputTokens, 0);
747
+ const output_tokens = usages.reduce((sum, row) => sum + row.outputTokens, 0);
710
748
  await this.deps.taskService.updateStatus(task.id, 'failed', message);
711
749
  return {
712
750
  newTraceId: traceId,
@@ -715,7 +753,11 @@ export class AgentRuntime {
715
753
  checkpointId,
716
754
  status: 'failed',
717
755
  output: message,
718
- tokenUsage: { input_tokens: 0, output_tokens: 0, total_tokens: 0 },
756
+ tokenUsage: {
757
+ input_tokens,
758
+ output_tokens,
759
+ total_tokens: input_tokens + output_tokens,
760
+ },
719
761
  };
720
762
  }
721
763
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * dev 装配的 rag 依赖 — createDevRuntime 检测显式配置后自动注入
3
+ *
4
+ * 启用条件:EAP_EMBED_API_KEY 存在(缺省不注入,行为与无 rag 的 master 一致)。
5
+ * retriever 惰性初始化:vector-engine(better-sqlite3+sqlite-vec,native 依赖)
6
+ * 只在首次检索时动态 import——装配期零 native 加载,服务端部署不受影响。
7
+ * import 目标可用 EAP_VECTOR_ENGINE_PATH 指定绝对路径(默认解析 @eap/vector-engine;
8
+ * eap-cli 发版链等无该包的环境走绝对路径注入)。
9
+ *
10
+ * 环境变量(与 .local 演示脚本同一套):
11
+ * - EAP_EMBED_API_KEY / EAP_EMBED_BASE_URL / EAP_EMBED_MODEL / EAP_EMBED_DIMS:embedding 网关
12
+ * - RAG_DB:向量库路径(缺省 ~/.eap/rag/agent.db)
13
+ */
14
+ import type { IRetriever, RagNodeDeps } from '../graph/nodes/rag/index.js';
15
+ /**
16
+ * dev rag 装配:EAP_EMBED_API_KEY 显式配置才启用,否则 undefined(不注入)。
17
+ * llmProvider 复用 dev 装配已构造的实例(与 llm 节点同一体系)。
18
+ * EAP_RAG_TRACE=1 时接 console 观测 rag.* 事件流(轮次决策/各源检索/降噪/收束),
19
+ * 供多源融合行为观察;缺省静默。
20
+ */
21
+ export declare function createDevRagNodeDeps(llmProvider: RagNodeDeps['llmProvider'], loadFn?: () => Promise<IRetriever>): RagNodeDeps | undefined;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * dev 装配的 rag 依赖 — createDevRuntime 检测显式配置后自动注入
3
+ *
4
+ * 启用条件:EAP_EMBED_API_KEY 存在(缺省不注入,行为与无 rag 的 master 一致)。
5
+ * retriever 惰性初始化:vector-engine(better-sqlite3+sqlite-vec,native 依赖)
6
+ * 只在首次检索时动态 import——装配期零 native 加载,服务端部署不受影响。
7
+ * import 目标可用 EAP_VECTOR_ENGINE_PATH 指定绝对路径(默认解析 @eap/vector-engine;
8
+ * eap-cli 发版链等无该包的环境走绝对路径注入)。
9
+ *
10
+ * 环境变量(与 .local 演示脚本同一套):
11
+ * - EAP_EMBED_API_KEY / EAP_EMBED_BASE_URL / EAP_EMBED_MODEL / EAP_EMBED_DIMS:embedding 网关
12
+ * - RAG_DB:向量库路径(缺省 ~/.eap/rag/agent.db)
13
+ */
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+ import { existsSync, mkdirSync, readFileSync, watchFile } from 'node:fs';
17
+ import { pathToFileURL } from 'node:url';
18
+ import { appendFileSync } from 'node:fs';
19
+ // 惰性读 env:模块顶层读不到(runtime 经 sdk import 链早于 CLI 灌 .env.local 执行)
20
+ const trace = (line) => {
21
+ console.log(line);
22
+ const f = process.env.EAP_RAG_TRACE_FILE;
23
+ if (f)
24
+ appendFileSync(f, line + '\n', 'utf-8');
25
+ };
26
+ /** 入库日志环形缓冲(globalThis 桥接:Playground 服务同进程读,GET /api/rag-ingest-log) */
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ const ingestLogBuf = (globalThis.__ragIngestLog ??= []);
29
+ const ingestLog = (line) => {
30
+ const ts = new Date().toISOString().slice(11, 19);
31
+ ingestLogBuf.push(`[${ts}] ${line}`);
32
+ if (ingestLogBuf.length > 200)
33
+ ingestLogBuf.splice(0, ingestLogBuf.length - 200);
34
+ };
35
+ import { HttpEmbedder, LocalSqliteRetriever } from '../graph/nodes/rag/index.js';
36
+ /** 惰性 IRetriever:首次调用才加载真实现;加载失败不缓存,允许下轮重试 */
37
+ class LazyLocalRetriever {
38
+ load;
39
+ sourceId = 'local';
40
+ implPromise;
41
+ constructor(load) {
42
+ this.load = load;
43
+ }
44
+ ensure() {
45
+ this.implPromise ??= this.load().catch((err) => {
46
+ this.implPromise = undefined; // 失败不缓存,下次调用重试加载
47
+ throw err;
48
+ });
49
+ return this.implPromise;
50
+ }
51
+ async retrieve(query, opts) {
52
+ return (await this.ensure()).retrieve(query, opts);
53
+ }
54
+ async retrieveByKeyword(query, opts) {
55
+ const impl = await this.ensure();
56
+ if (!impl.retrieveByKeyword)
57
+ return [];
58
+ return impl.retrieveByKeyword(query, opts);
59
+ }
60
+ }
61
+ /** 默认加载器:HttpEmbedder + vector-engine + LocalSqliteRetriever(单库:RAG_DB) */
62
+ async function loadLocalRetriever() {
63
+ const dims = Number(process.env.EAP_EMBED_DIMS ?? 1024);
64
+ const embedder = new HttpEmbedder({
65
+ baseUrl: String(process.env.EAP_EMBED_BASE_URL ?? 'https://gtaiapi.gtcloud.cn'),
66
+ model: String(process.env.EAP_EMBED_MODEL ?? 'Qwen3-Embedding-8B'),
67
+ apiKey: String(process.env.EAP_EMBED_API_KEY ?? ''),
68
+ dims,
69
+ dimensions: dims,
70
+ });
71
+ const moduleName = process.env.EAP_VECTOR_ENGINE_PATH ?? '@eap/vector-engine';
72
+ // Windows 绝对路径须转 file:// URL(盘符字母否则被解析为 URL scheme)
73
+ const spec = /^[a-zA-Z]:[\\/]/.test(moduleName) ? pathToFileURL(moduleName).href : moduleName;
74
+ const mod = (await import(spec));
75
+ const dbPath = process.env.RAG_DB ?? join(homedir(), '.eap', 'rag', 'agent.db');
76
+ mkdirSync(dirname(dbPath), { recursive: true });
77
+ const engine = await mod.createAsyncEngine({ dbPath, dims });
78
+ // 自动向量化:EAP_RAG_AUTO_INGEST=1 时常驻调度器扫授权目录,文件放入即入库
79
+ // (写入侧归宿主/桌面语义;dev 演示用轮询,生产为授权事件触发)。
80
+ // RAG_DOCS_DIR 支持 `;` 分隔多目录——"授权一个新文件夹" = 追加一个路径。
81
+ //
82
+ // 授权清单热更新(客户端授权动作的 dev 模拟):RAG_AUTHORIZED_DIRS_FILE(json
83
+ // {"dirs":[...]},缺省 <库目录>/authorized-dirs.json)存在则覆盖 RAG_DOCS_DIR,
84
+ // 文件变更 → scheduler.updateDirs 即时生效(新目录入库/移除软删),全程不重启。
85
+ if (process.env.EAP_RAG_AUTO_INGEST) {
86
+ const envDirs = (process.env.RAG_DOCS_DIR ?? join(dirname(dbPath), 'docs'))
87
+ .split(';')
88
+ .map((s) => s.trim())
89
+ .filter(Boolean);
90
+ const authFile = process.env.RAG_AUTHORIZED_DIRS_FILE ?? join(dirname(dbPath), 'authorized-dirs.json');
91
+ const readAuthDirs = () => {
92
+ try {
93
+ const parsed = JSON.parse(readFileSync(authFile, 'utf-8'));
94
+ if (Array.isArray(parsed.dirs) && parsed.dirs.every((d) => typeof d === 'string')) {
95
+ const dirs = parsed.dirs.map((d) => d.trim()).filter(Boolean);
96
+ return dirs.length ? dirs : undefined; // 空清单视为无效,防误清库
97
+ }
98
+ }
99
+ catch {
100
+ // 文件不存在/解析失败:保持现清单
101
+ }
102
+ return undefined;
103
+ };
104
+ let currentDirs = readAuthDirs() ?? envDirs;
105
+ const scheduler = new mod.IngestScheduler({
106
+ engine,
107
+ embedder,
108
+ userId: process.env.RAG_USER ?? 'anonymous',
109
+ dirs: currentDirs,
110
+ intervalMs: Number(process.env.EAP_RAG_SCAN_INTERVAL_MS ?? 30_000),
111
+ watch: process.env.EAP_RAG_SCAN_MODE !== 'interval',
112
+ onEvent: (e) => {
113
+ if (e.type === 'file.ingested') {
114
+ const line = `[rag-ingest] 📥 ${String(e['filePath']).split('\\').pop()} → ${e['chunks']} 片`;
115
+ console.log(line);
116
+ ingestLog(line);
117
+ }
118
+ else if (e.type === 'file.failed') {
119
+ const line = `[rag-ingest] ✗ ${String(e['filePath']).split('\\').pop()}: ${e['error']}`;
120
+ console.log(line);
121
+ ingestLog(line);
122
+ }
123
+ },
124
+ });
125
+ scheduler.start();
126
+ ingestLog(`🚀 自动向量化已启动:${currentDirs.length} 个授权目录`);
127
+ console.log(`[rag-ingest] 自动向量化已启动:目录 ${currentDirs.join('; ')}(${Number(process.env.EAP_RAG_SCAN_INTERVAL_MS ?? 30_000) / 1000}s 轮询${existsSync(authFile) ? `,授权清单 ${authFile} 热更新` : ''})`);
128
+ // 授权清单文件变更 → updateDirs(编辑器保存/前端写入均触发;stat 轮询最稳)
129
+ watchFile(authFile, { interval: 2000 }, () => {
130
+ const next = readAuthDirs();
131
+ if (next && next.join(';') !== currentDirs.join(';')) {
132
+ currentDirs = next;
133
+ scheduler.updateDirs(next);
134
+ const line = `[rag-ingest] 🔑 授权目录更新(免重启生效):${next.length} 个目录`;
135
+ console.log(line + `(${next.join('; ')})`);
136
+ ingestLog(line);
137
+ }
138
+ });
139
+ }
140
+ return new LocalSqliteRetriever({ engine, embedder });
141
+ }
142
+ /**
143
+ * dev rag 装配:EAP_EMBED_API_KEY 显式配置才启用,否则 undefined(不注入)。
144
+ * llmProvider 复用 dev 装配已构造的实例(与 llm 节点同一体系)。
145
+ * EAP_RAG_TRACE=1 时接 console 观测 rag.* 事件流(轮次决策/各源检索/降噪/收束),
146
+ * 供多源融合行为观察;缺省静默。
147
+ */
148
+ export function createDevRagNodeDeps(llmProvider, loadFn = loadLocalRetriever) {
149
+ if (!process.env.EAP_EMBED_API_KEY)
150
+ return undefined;
151
+ const fmt = (v, n = 40) => String(v ?? '').replace(/\s+/g, ' ').slice(0, n);
152
+ const emitEvent = process.env.EAP_RAG_TRACE
153
+ ? (type, p) => {
154
+ if (type === 'rag.round') {
155
+ const calls = (p['toolCalls'] ?? [])
156
+ .map((t) => `${t.name}(${JSON.stringify(t.arguments)})`).join(' ');
157
+ trace(`[rag] 第${p['round']}轮: ${calls}`);
158
+ }
159
+ else if (type === 'rag.search') {
160
+ const head = p['kind'] === 'kb' ? `kb:${p['toolName']}` : `local:"${fmt(p['query'], 20)}"`;
161
+ trace(`[rag] search ${head} topK=${p['topK']} → hits=${p['hits']} ${p['durationMs']}ms${p['success'] ? '' : ` ✗${p['error']}`}${p['scope'] ? ` scope=${p['scope']}` : ''}`);
162
+ }
163
+ else if (type === 'rag.denoise') {
164
+ trace(`[rag] 降噪 候选${p['candidates']} → 保留${p['kept']}`);
165
+ for (const f of p['filtered'] ?? [])
166
+ trace(`[rag] 滤 ${fmt(f.rawId, 30)}(${fmt(f.reason, 30)})`);
167
+ }
168
+ else if (type === 'rag.rewrite') {
169
+ trace(`[rag] 空结果重写 "${fmt(p['from'], 20)}" → "${fmt(p['to'], 20)}"`);
170
+ }
171
+ else if (type === 'rag.finish') {
172
+ trace(`[rag] 收束: ${p['rounds']}轮 最终${p['docs']}条${p['empty'] ? '(空)' : ''}`);
173
+ }
174
+ }
175
+ : undefined;
176
+ // 预热:自动向量化开启时立即后台装配一次——调度器/授权热更新/入库日志不等到首次
177
+ // local_search 才启动(演示"先授权后提问"场景需要授权即生效)。失败静默,检索时重试。
178
+ if (process.env.EAP_RAG_AUTO_INGEST) {
179
+ void Promise.resolve(loadFn()).catch(() => undefined);
180
+ }
181
+ return {
182
+ llmProvider,
183
+ retrievers: [new LazyLocalRetriever(loadFn)],
184
+ emitEvent,
185
+ };
186
+ }