@gt-fe/eap-runtime 0.0.8
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.
- package/README.md +323 -0
- package/dist/api/handlers/index.d.ts +3 -0
- package/dist/api/handlers/index.js +4 -0
- package/dist/api/handlers/invoke.handler.d.ts +77 -0
- package/dist/api/handlers/invoke.handler.js +411 -0
- package/dist/api/handlers/messages.handler.d.ts +53 -0
- package/dist/api/handlers/messages.handler.js +80 -0
- package/dist/api/handlers/stream.handler.d.ts +92 -0
- package/dist/api/handlers/stream.handler.js +432 -0
- package/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +4 -0
- package/dist/api/middleware/auth.middleware.d.ts +96 -0
- package/dist/api/middleware/auth.middleware.js +158 -0
- package/dist/api/middleware/index.d.ts +1 -0
- package/dist/api/middleware/index.js +2 -0
- package/dist/api/routes/agent.routes.d.ts +44 -0
- package/dist/api/routes/agent.routes.js +204 -0
- package/dist/api/routes/index.d.ts +3 -0
- package/dist/api/routes/index.js +4 -0
- package/dist/api/routes/session.routes.d.ts +82 -0
- package/dist/api/routes/session.routes.js +169 -0
- package/dist/api/routes/task.routes.d.ts +75 -0
- package/dist/api/routes/task.routes.js +108 -0
- package/dist/checkpoint/checkpoint-serializer.d.ts +58 -0
- package/dist/checkpoint/checkpoint-serializer.js +81 -0
- package/dist/checkpoint/composite-checkpoint-saver.d.ts +42 -0
- package/dist/checkpoint/composite-checkpoint-saver.js +118 -0
- package/dist/checkpoint/contracts/checkpoint-saver.contract.d.ts +65 -0
- package/dist/checkpoint/contracts/checkpoint-saver.contract.js +12 -0
- package/dist/checkpoint/contracts/index.d.ts +1 -0
- package/dist/checkpoint/contracts/index.js +1 -0
- package/dist/checkpoint/file-checkpoint-saver.d.ts +19 -0
- package/dist/checkpoint/file-checkpoint-saver.js +98 -0
- package/dist/checkpoint/hybrid-checkpoint-saver.d.ts +130 -0
- package/dist/checkpoint/hybrid-checkpoint-saver.js +211 -0
- package/dist/checkpoint/in-memory-checkpoint-saver.d.ts +14 -0
- package/dist/checkpoint/in-memory-checkpoint-saver.js +46 -0
- package/dist/checkpoint/index.d.ts +16 -0
- package/dist/checkpoint/index.js +13 -0
- package/dist/checkpoint/mongo-checkpoint-saver.d.ts +38 -0
- package/dist/checkpoint/mongo-checkpoint-saver.js +56 -0
- package/dist/checkpoint/pg-checkpoint-saver.d.ts +59 -0
- package/dist/checkpoint/pg-checkpoint-saver.js +140 -0
- package/dist/checkpoint/redis-checkpoint-saver.d.ts +68 -0
- package/dist/checkpoint/redis-checkpoint-saver.js +173 -0
- package/dist/checkpoint/restore-checkpoint.d.ts +33 -0
- package/dist/checkpoint/restore-checkpoint.js +33 -0
- package/dist/common/agent-code.d.ts +10 -0
- package/dist/common/agent-code.js +12 -0
- package/dist/common/concurrency.d.ts +79 -0
- package/dist/common/concurrency.js +121 -0
- package/dist/common/health.d.ts +40 -0
- package/dist/common/health.js +37 -0
- package/dist/common/index.d.ts +5 -0
- package/dist/common/index.js +5 -0
- package/dist/common/logger.d.ts +34 -0
- package/dist/common/logger.js +38 -0
- package/dist/common/queue-consumer.d.ts +125 -0
- package/dist/common/queue-consumer.js +159 -0
- package/dist/common/trace-id.d.ts +2 -0
- package/dist/common/trace-id.js +8 -0
- package/dist/common/types.d.ts +104 -0
- package/dist/common/types.js +14 -0
- package/dist/contracts/index.d.ts +11 -0
- package/dist/contracts/index.js +10 -0
- package/dist/contracts/permission-checker.d.ts +10 -0
- package/dist/contracts/permission-checker.js +4 -0
- package/dist/contracts/trace-collector.d.ts +211 -0
- package/dist/contracts/trace-collector.js +13 -0
- package/dist/governance/governance-client.d.ts +38 -0
- package/dist/governance/governance-client.js +95 -0
- package/dist/governance/index.d.ts +7 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/permission-guard.d.ts +19 -0
- package/dist/governance/permission-guard.js +59 -0
- package/dist/graph/adapters/index.d.ts +3 -0
- package/dist/graph/adapters/index.js +3 -0
- package/dist/graph/adapters/langgraph-checkpointer.d.ts +40 -0
- package/dist/graph/adapters/langgraph-checkpointer.js +211 -0
- package/dist/graph/adapters/langgraph.adapter.d.ts +190 -0
- package/dist/graph/adapters/langgraph.adapter.js +1303 -0
- package/dist/graph/adapters/subgraph-compiler.d.ts +39 -0
- package/dist/graph/adapters/subgraph-compiler.js +248 -0
- package/dist/graph/condition-evaluator.d.ts +57 -0
- package/dist/graph/condition-evaluator.js +162 -0
- package/dist/graph/contracts/executable-graph.d.ts +340 -0
- package/dist/graph/contracts/executable-graph.js +10 -0
- package/dist/graph/contracts/index.d.ts +2 -0
- package/dist/graph/contracts/index.js +2 -0
- package/dist/graph/contracts/state-graph.contract.d.ts +76 -0
- package/dist/graph/contracts/state-graph.contract.js +11 -0
- package/dist/graph/hitl/hitl-handler.d.ts +199 -0
- package/dist/graph/hitl/hitl-handler.js +480 -0
- package/dist/graph/hitl/index.d.ts +1 -0
- package/dist/graph/hitl/index.js +1 -0
- package/dist/graph/index.d.ts +7 -0
- package/dist/graph/index.js +7 -0
- package/dist/graph/nodes/index.d.ts +2 -0
- package/dist/graph/nodes/index.js +2 -0
- package/dist/graph/nodes/llm-node.d.ts +182 -0
- package/dist/graph/nodes/llm-node.js +281 -0
- package/dist/graph/nodes/skill-bind-node.d.ts +32 -0
- package/dist/graph/nodes/skill-bind-node.js +116 -0
- package/dist/graph/nodes/tool-node.d.ts +75 -0
- package/dist/graph/nodes/tool-node.js +324 -0
- package/dist/graph/resolve-graph-prompt.d.ts +5 -0
- package/dist/graph/resolve-graph-prompt.js +75 -0
- package/dist/graph/retry/budget-aware-retry.d.ts +91 -0
- package/dist/graph/retry/budget-aware-retry.js +126 -0
- package/dist/graph/retry/index.d.ts +9 -0
- package/dist/graph/retry/index.js +8 -0
- package/dist/graph/retry/node-retry.d.ts +123 -0
- package/dist/graph/retry/node-retry.js +235 -0
- package/dist/graph/skill-audit-recorder.d.ts +126 -0
- package/dist/graph/skill-audit-recorder.js +114 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +32 -0
- package/dist/memory/compression/compression-stats.d.ts +109 -0
- package/dist/memory/compression/compression-stats.js +144 -0
- package/dist/memory/compression/compression.repository.d.ts +62 -0
- package/dist/memory/compression/compression.repository.js +71 -0
- package/dist/memory/compression/index.d.ts +10 -0
- package/dist/memory/compression/index.js +15 -0
- package/dist/memory/compression/llm-summarize.d.ts +58 -0
- package/dist/memory/compression/llm-summarize.js +192 -0
- package/dist/memory/compression/sliding-window.compression.d.ts +60 -0
- package/dist/memory/compression/sliding-window.compression.js +175 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/long-term-memory.service.d.ts +94 -0
- package/dist/memory/long-term-memory.service.js +120 -0
- package/dist/memory/message-compression.service.d.ts +128 -0
- package/dist/memory/message-compression.service.js +69 -0
- package/dist/memory/pg-memory.repository.d.ts +44 -0
- package/dist/memory/pg-memory.repository.js +108 -0
- package/dist/memory/semantic-retrieval.service.d.ts +148 -0
- package/dist/memory/semantic-retrieval.service.js +238 -0
- package/dist/providers/echo-llm-provider.d.ts +10 -0
- package/dist/providers/echo-llm-provider.js +87 -0
- package/dist/providers/http-llm-provider.d.ts +35 -0
- package/dist/providers/http-llm-provider.js +404 -0
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.js +5 -0
- package/dist/providers/local-llm-provider.d.ts +27 -0
- package/dist/providers/local-llm-provider.js +105 -0
- package/dist/providers/resolve-llm-model.d.ts +13 -0
- package/dist/providers/resolve-llm-model.js +30 -0
- package/dist/providers/routing-llm-provider.d.ts +28 -0
- package/dist/providers/routing-llm-provider.js +79 -0
- package/dist/registry/agent-models.d.ts +1 -0
- package/dist/registry/agent-models.js +1 -0
- package/dist/registry/agent-registry-client.d.ts +56 -0
- package/dist/registry/agent-registry-client.js +36 -0
- package/dist/registry/definition-cache.d.ts +37 -0
- package/dist/registry/definition-cache.js +82 -0
- package/dist/registry/deployment-snapshot.d.ts +99 -0
- package/dist/registry/deployment-snapshot.js +542 -0
- package/dist/registry/graph-loader.d.ts +134 -0
- package/dist/registry/graph-loader.js +985 -0
- package/dist/registry/index.d.ts +9 -0
- package/dist/registry/index.js +9 -0
- package/dist/registry/preload-errors.d.ts +13 -0
- package/dist/registry/preload-errors.js +35 -0
- package/dist/registry/preload-flags.d.ts +22 -0
- package/dist/registry/preload-flags.js +44 -0
- package/dist/registry/skill-io-guard.d.ts +25 -0
- package/dist/registry/skill-io-guard.js +98 -0
- package/dist/registry/skill-tree.d.ts +27 -0
- package/dist/registry/skill-tree.js +183 -0
- package/dist/runtime/agent-runtime.d.ts +106 -0
- package/dist/runtime/agent-runtime.js +777 -0
- package/dist/runtime/dev-runtime.d.ts +69 -0
- package/dist/runtime/dev-runtime.js +199 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/storage-profile.d.ts +50 -0
- package/dist/runtime/storage-profile.js +138 -0
- package/dist/runtime/types.d.ts +120 -0
- package/dist/runtime/types.js +9 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +161 -0
- package/dist/session/compression-middleware.d.ts +126 -0
- package/dist/session/compression-middleware.js +230 -0
- package/dist/session/file-message.repository.d.ts +25 -0
- package/dist/session/file-message.repository.js +78 -0
- package/dist/session/file-session.repository.d.ts +72 -0
- package/dist/session/file-session.repository.js +212 -0
- package/dist/session/index.d.ts +13 -0
- package/dist/session/index.js +12 -0
- package/dist/session/message.service.d.ts +80 -0
- package/dist/session/message.service.js +128 -0
- package/dist/session/persist-turn-messages.d.ts +145 -0
- package/dist/session/persist-turn-messages.js +325 -0
- package/dist/session/pg-message.repository.d.ts +49 -0
- package/dist/session/pg-message.repository.js +149 -0
- package/dist/session/pg-session.repository.d.ts +46 -0
- package/dist/session/pg-session.repository.js +117 -0
- package/dist/session/session-context.service.d.ts +49 -0
- package/dist/session/session-context.service.js +66 -0
- package/dist/session/session.service.d.ts +194 -0
- package/dist/session/session.service.js +166 -0
- package/dist/sse/connection-event-logger.d.ts +21 -0
- package/dist/sse/connection-event-logger.js +33 -0
- package/dist/sse/contracts/index.d.ts +1 -0
- package/dist/sse/contracts/index.js +1 -0
- package/dist/sse/contracts/sse-emitter.contract.d.ts +74 -0
- package/dist/sse/contracts/sse-emitter.contract.js +11 -0
- package/dist/sse/event-buffer.d.ts +20 -0
- package/dist/sse/event-buffer.js +45 -0
- package/dist/sse/events/code-events.d.ts +19 -0
- package/dist/sse/events/code-events.js +9 -0
- package/dist/sse/events/collaboration-events.d.ts +37 -0
- package/dist/sse/events/collaboration-events.js +11 -0
- package/dist/sse/events/connection-events.d.ts +13 -0
- package/dist/sse/events/connection-events.js +9 -0
- package/dist/sse/events/file-events.d.ts +29 -0
- package/dist/sse/events/file-events.js +10 -0
- package/dist/sse/events/hitl-events.d.ts +33 -0
- package/dist/sse/events/hitl-events.js +10 -0
- package/dist/sse/events/index.d.ts +107 -0
- package/dist/sse/events/index.js +96 -0
- package/dist/sse/events/plan-events.d.ts +38 -0
- package/dist/sse/events/plan-events.js +11 -0
- package/dist/sse/events/reasoning-events.d.ts +35 -0
- package/dist/sse/events/reasoning-events.js +13 -0
- package/dist/sse/events/response-events.d.ts +25 -0
- package/dist/sse/events/response-events.js +12 -0
- package/dist/sse/events/session-events.d.ts +43 -0
- package/dist/sse/events/session-events.js +12 -0
- package/dist/sse/events/skill-events.d.ts +48 -0
- package/dist/sse/events/skill-events.js +11 -0
- package/dist/sse/events/stats-events.d.ts +44 -0
- package/dist/sse/events/stats-events.js +12 -0
- package/dist/sse/events/tool-events.d.ts +68 -0
- package/dist/sse/events/tool-events.js +14 -0
- package/dist/sse/index.d.ts +8 -0
- package/dist/sse/index.js +8 -0
- package/dist/sse/sse-connection-manager.d.ts +116 -0
- package/dist/sse/sse-connection-manager.js +185 -0
- package/dist/sse/sse-emitter.d.ts +108 -0
- package/dist/sse/sse-emitter.js +210 -0
- package/dist/sse/sse-heartbeat.d.ts +58 -0
- package/dist/sse/sse-heartbeat.js +76 -0
- package/dist/sse/trace-collector.d.ts +60 -0
- package/dist/sse/trace-collector.js +107 -0
- package/dist/task/contracts/index.d.ts +2 -0
- package/dist/task/contracts/index.js +1 -0
- package/dist/task/contracts/task-manager.contract.d.ts +160 -0
- package/dist/task/contracts/task-manager.contract.js +10 -0
- package/dist/task/index.d.ts +7 -0
- package/dist/task/index.js +5 -0
- package/dist/task/pg-task.repository.d.ts +47 -0
- package/dist/task/pg-task.repository.js +131 -0
- package/dist/task/task-progress-tracker.d.ts +67 -0
- package/dist/task/task-progress-tracker.js +80 -0
- package/dist/task/task-timeout.d.ts +70 -0
- package/dist/task/task-timeout.js +116 -0
- package/dist/task/task.service.d.ts +84 -0
- package/dist/task/task.service.js +233 -0
- package/dist/token/compression-budget-integration.d.ts +145 -0
- package/dist/token/compression-budget-integration.js +185 -0
- package/dist/token/context-window-budget.d.ts +113 -0
- package/dist/token/context-window-budget.js +134 -0
- package/dist/token/contracts/index.d.ts +1 -0
- package/dist/token/contracts/index.js +1 -0
- package/dist/token/contracts/token-budget.contract.d.ts +77 -0
- package/dist/token/contracts/token-budget.contract.js +11 -0
- package/dist/token/index.d.ts +12 -0
- package/dist/token/index.js +10 -0
- package/dist/token/pg-token-budget.repository.d.ts +39 -0
- package/dist/token/pg-token-budget.repository.js +116 -0
- package/dist/token/session-budget-checker.d.ts +54 -0
- package/dist/token/session-budget-checker.js +72 -0
- package/dist/token/token-budget-guard.d.ts +66 -0
- package/dist/token/token-budget-guard.js +97 -0
- package/dist/token/token-budget.service.d.ts +86 -0
- package/dist/token/token-budget.service.js +130 -0
- package/dist/token/token-detail-mode.d.ts +73 -0
- package/dist/token/token-detail-mode.js +107 -0
- package/dist/token/token-estimator.d.ts +80 -0
- package/dist/token/token-estimator.js +134 -0
- package/dist/token/token-stats.service.d.ts +88 -0
- package/dist/token/token-stats.service.js +94 -0
- package/dist/tool/create-tool-executor.d.ts +33 -0
- package/dist/tool/create-tool-executor.js +60 -0
- package/dist/tool/dev-http-executor.d.ts +18 -0
- package/dist/tool/dev-http-executor.js +96 -0
- package/dist/tool/dev-mcp-executor.d.ts +29 -0
- package/dist/tool/dev-mcp-executor.js +84 -0
- package/dist/tool/dev-tool-executor.d.ts +23 -0
- package/dist/tool/dev-tool-executor.js +55 -0
- package/dist/tool/file-artifact.d.ts +11 -0
- package/dist/tool/file-artifact.js +90 -0
- package/dist/tool/http-tool-executor.d.ts +17 -0
- package/dist/tool/http-tool-executor.js +158 -0
- package/dist/tool/index.d.ts +18 -0
- package/dist/tool/index.js +17 -0
- package/dist/tool/loaded-tool-definition.d.ts +35 -0
- package/dist/tool/loaded-tool-definition.js +9 -0
- package/dist/tool/local-mcp-client.d.ts +23 -0
- package/dist/tool/local-mcp-client.js +225 -0
- package/dist/tool/local-tool-executor.d.ts +37 -0
- package/dist/tool/local-tool-executor.js +249 -0
- package/dist/tool/mcp-tool-executor.d.ts +23 -0
- package/dist/tool/mcp-tool-executor.js +160 -0
- package/dist/tool/mock-tool-executor.d.ts +28 -0
- package/dist/tool/mock-tool-executor.js +65 -0
- package/dist/tool/system-prompt-builder.d.ts +114 -0
- package/dist/tool/system-prompt-builder.js +213 -0
- package/dist/tool/tool-confirmation.d.ts +29 -0
- package/dist/tool/tool-confirmation.js +81 -0
- package/dist/tool/tool-event-emitter.d.ts +58 -0
- package/dist/tool/tool-event-emitter.js +154 -0
- package/dist/tool/tool-execution-router.d.ts +25 -0
- package/dist/tool/tool-execution-router.js +50 -0
- package/dist/tool/tool-executor.interface.d.ts +85 -0
- package/dist/tool/tool-executor.interface.js +7 -0
- package/dist/tool/tool-orchestrator.d.ts +22 -0
- package/dist/tool/tool-orchestrator.js +104 -0
- package/dist/tool/tool-result-injector.d.ts +64 -0
- package/dist/tool/tool-result-injector.js +65 -0
- package/dist/utils/format-error.d.ts +4 -0
- package/dist/utils/format-error.js +4 -0
- package/dist/utils/http-error.d.ts +11 -0
- package/dist/utils/http-error.js +24 -0
- package/package.json +56 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolNode — Tool 执行节点
|
|
3
|
+
*
|
|
4
|
+
* 在 StateGraph 中作为 node function 注册,负责:
|
|
5
|
+
* 1. 从 state 中提取最后一条 assistant 消息的 toolCalls
|
|
6
|
+
* 2. 通过 ToolOrchestrator.executeAll() 并行执行所有 Tool 调用
|
|
7
|
+
* 3. 通过 ToolResultInjector 将执行结果注入 state
|
|
8
|
+
*
|
|
9
|
+
* @see design.md §1.4 ToolNode: LangGraph的ToolNode封装
|
|
10
|
+
* @see requirements.md Requirement 8 AC1, AC3, AC5
|
|
11
|
+
*/
|
|
12
|
+
import { isHitlDecision, unwrapHitlPaused } from '../hitl/hitl-handler.js';
|
|
13
|
+
import { ToolConfirmationInterceptor } from '../../tool/tool-confirmation.js';
|
|
14
|
+
import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
|
|
15
|
+
import { resolveTraceId } from '../../common/trace-id.js';
|
|
16
|
+
export class ToolNode {
|
|
17
|
+
orchestrator;
|
|
18
|
+
injector;
|
|
19
|
+
defaultAuthContext;
|
|
20
|
+
onProgress;
|
|
21
|
+
hitlHandler;
|
|
22
|
+
nodeId;
|
|
23
|
+
constructor(orchestrator, injector, defaultAuthContext, onProgress, hitlHandler, nodeId) {
|
|
24
|
+
this.orchestrator = orchestrator;
|
|
25
|
+
this.injector = injector;
|
|
26
|
+
this.defaultAuthContext = defaultAuthContext;
|
|
27
|
+
this.onProgress = onProgress;
|
|
28
|
+
this.hitlHandler = hitlHandler;
|
|
29
|
+
this.nodeId = nodeId;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* execute — StateGraph 节点函数
|
|
33
|
+
*
|
|
34
|
+
* 提取 pending tool_calls → 并行执行 → 注入结果 → 返回更新 state
|
|
35
|
+
*
|
|
36
|
+
* @param state - 当前 LangGraph State
|
|
37
|
+
* @returns 包含 Tool 结果消息的更新 state
|
|
38
|
+
*/
|
|
39
|
+
async execute(state) {
|
|
40
|
+
// 1. 从 state 中提取 messages
|
|
41
|
+
const messages = this.extractMessages(state);
|
|
42
|
+
// 2. 找到最后一条 assistant 消息
|
|
43
|
+
const lastAssistantMessage = this.findLastAssistantMessage(messages);
|
|
44
|
+
// 3. 边界情况:无 tool calls → 原样返回
|
|
45
|
+
if (!lastAssistantMessage?.toolCalls || lastAssistantMessage.toolCalls.length === 0) {
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
const loadedTools = state['loadedTools'];
|
|
49
|
+
// 4. 转换为 ToolCallRequest[](附带 manifest 路由字段)
|
|
50
|
+
const requests = lastAssistantMessage.toolCalls.map((tc) => {
|
|
51
|
+
const loaded = loadedTools?.[tc.name];
|
|
52
|
+
const correlationId = String(state['correlationId'] ?? tc.id);
|
|
53
|
+
const taskId = String(state['taskId'] ?? state['correlationId'] ?? tc.id);
|
|
54
|
+
return {
|
|
55
|
+
toolCallId: tc.id,
|
|
56
|
+
toolName: tc.name,
|
|
57
|
+
arguments: tc.arguments,
|
|
58
|
+
authContext: this.buildAuthContext(state),
|
|
59
|
+
forceServer: this.shouldForceServer(state, tc.name),
|
|
60
|
+
toolType: loaded?.manifest.type,
|
|
61
|
+
toolRuntime: loaded?.manifest.toolRuntime,
|
|
62
|
+
dataScope: loaded?.manifest.dataScope,
|
|
63
|
+
// dev 上下文 DevMcpExecutor 直连所需;server 上下文会忽略
|
|
64
|
+
execution: loaded?.manifest.execution,
|
|
65
|
+
correlationId,
|
|
66
|
+
taskId,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
const executionContext = state['executionContext'];
|
|
70
|
+
const traceCollector = state['traceCollector'];
|
|
71
|
+
const traceId = resolveTraceId(state);
|
|
72
|
+
const agentCode = String(state['agentCode'] ?? state['agentId'] ?? '');
|
|
73
|
+
const nodeId = this.nodeId ?? 'tool-node';
|
|
74
|
+
const skillId = typeof state['activatedSkill'] === 'string' && state['activatedSkill']
|
|
75
|
+
? state['activatedSkill']
|
|
76
|
+
: undefined;
|
|
77
|
+
const emitProgress = (data) => {
|
|
78
|
+
this.onProgress?.({ ...data, node: nodeId, ...(skillId ? { skillId } : {}) });
|
|
79
|
+
};
|
|
80
|
+
// 5. 并行执行所有 Tool 调用(附 Trace span)
|
|
81
|
+
const spanIds = await Promise.all(requests.map(async (req) => {
|
|
82
|
+
if (!traceCollector || !traceId)
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
const parentNodeId = typeof state['skillSpanId'] === 'string' && state['skillSpanId']
|
|
86
|
+
? state['skillSpanId']
|
|
87
|
+
: undefined;
|
|
88
|
+
return await traceCollector.createNode({
|
|
89
|
+
traceId,
|
|
90
|
+
name: req.toolName,
|
|
91
|
+
type: 'tool',
|
|
92
|
+
parentNodeId,
|
|
93
|
+
toolRuntime: req.toolRuntime === 'both' ? 'both' : req.toolRuntime,
|
|
94
|
+
agentCode: agentCode || undefined,
|
|
95
|
+
input: req.arguments,
|
|
96
|
+
metadata: {
|
|
97
|
+
toolCallId: req.toolCallId,
|
|
98
|
+
toolType: req.toolType,
|
|
99
|
+
taskId: req.taskId,
|
|
100
|
+
correlationId: req.correlationId,
|
|
101
|
+
skillId,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
// 并行执行,但各自独立计时(避免共用 startedAt 导致时长不准/看起来像“写死”)
|
|
110
|
+
// require_confirmation 工具走 HITL 拦截:改为顺序执行,thread_id 与 Checkpoint 对齐
|
|
111
|
+
const confirmationTools = new Set(Object.entries(loadedTools ?? {})
|
|
112
|
+
.filter(([, def]) => Boolean(def?.manifest.requireConfirmation))
|
|
113
|
+
.map(([name]) => name));
|
|
114
|
+
const needsHitl = Boolean(this.hitlHandler) &&
|
|
115
|
+
requests.some((req) => confirmationTools.has(req.toolName));
|
|
116
|
+
const runOne = async (req, i) => {
|
|
117
|
+
const resumingHitl = isHitlDecision(state['hitlChoice']);
|
|
118
|
+
if (!resumingHitl) {
|
|
119
|
+
emitProgress({
|
|
120
|
+
toolCallId: req.toolCallId,
|
|
121
|
+
toolName: req.toolName,
|
|
122
|
+
status: 'started',
|
|
123
|
+
arguments: req.arguments,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const startedAt = Date.now();
|
|
127
|
+
try {
|
|
128
|
+
let result;
|
|
129
|
+
if (needsHitl && this.hitlHandler && confirmationTools.has(req.toolName)) {
|
|
130
|
+
const interceptor = new ToolConfirmationInterceptor(confirmationTools, this.hitlHandler, {
|
|
131
|
+
execute: (r) => this.orchestrator.executeSingle(r, executionContext),
|
|
132
|
+
});
|
|
133
|
+
const graphState = this.toGraphState(state);
|
|
134
|
+
const threadId = resolveCheckpointThreadId({
|
|
135
|
+
sessionId: typeof state['sessionId'] === 'string' ? state['sessionId'] : undefined,
|
|
136
|
+
taskId: req.taskId,
|
|
137
|
+
});
|
|
138
|
+
const confirmed = await interceptor.executeWithConfirmation(req, graphState, req.taskId ?? String(state['taskId'] ?? req.toolCallId), threadId, {
|
|
139
|
+
toolSpanId: spanIds[i] ?? undefined,
|
|
140
|
+
skillSpanId: typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined,
|
|
141
|
+
});
|
|
142
|
+
result =
|
|
143
|
+
confirmed ??
|
|
144
|
+
{
|
|
145
|
+
toolCallId: req.toolCallId,
|
|
146
|
+
toolName: req.toolName,
|
|
147
|
+
content: `用户拒绝执行工具 ${req.toolName}`,
|
|
148
|
+
success: false,
|
|
149
|
+
error: 'hitl_rejected',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
result = await this.orchestrator.executeSingle(req, executionContext);
|
|
154
|
+
}
|
|
155
|
+
const executionTime = Date.now() - startedAt;
|
|
156
|
+
if (result.success) {
|
|
157
|
+
emitProgress({
|
|
158
|
+
toolCallId: result.toolCallId,
|
|
159
|
+
toolName: result.toolName,
|
|
160
|
+
status: 'completed',
|
|
161
|
+
content: result.content,
|
|
162
|
+
executionTime,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
emitProgress({
|
|
167
|
+
toolCallId: result.toolCallId,
|
|
168
|
+
toolName: result.toolName,
|
|
169
|
+
status: 'error',
|
|
170
|
+
error: result.error ?? 'unknown error',
|
|
171
|
+
executionTime,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const nodeId = spanIds[i];
|
|
175
|
+
if (traceCollector && nodeId) {
|
|
176
|
+
try {
|
|
177
|
+
await traceCollector.completeNode(nodeId, {
|
|
178
|
+
status: result.success ? 'completed' : 'failed',
|
|
179
|
+
output: result.content,
|
|
180
|
+
error: result.error,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Trace 失败不影响主路径
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
// HITL 暂停必须向上抛出,供 invoke/stream 返回 waiting_hitl(不可降级为 tool failure)
|
|
191
|
+
const hitlPaused = unwrapHitlPaused(err);
|
|
192
|
+
if (hitlPaused) {
|
|
193
|
+
hitlPaused.toolSpanId = hitlPaused.toolSpanId ?? spanIds[i] ?? undefined;
|
|
194
|
+
hitlPaused.skillSpanId =
|
|
195
|
+
hitlPaused.skillSpanId ??
|
|
196
|
+
(typeof state['skillSpanId'] === 'string' ? state['skillSpanId'] : undefined);
|
|
197
|
+
throw hitlPaused;
|
|
198
|
+
}
|
|
199
|
+
const executionTime = Date.now() - startedAt;
|
|
200
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
201
|
+
emitProgress({
|
|
202
|
+
toolCallId: req.toolCallId,
|
|
203
|
+
toolName: req.toolName,
|
|
204
|
+
status: 'error',
|
|
205
|
+
error: message,
|
|
206
|
+
executionTime,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
toolCallId: req.toolCallId,
|
|
210
|
+
toolName: req.toolName,
|
|
211
|
+
content: '',
|
|
212
|
+
success: false,
|
|
213
|
+
error: message,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const execResults = needsHitl
|
|
218
|
+
? await (async () => {
|
|
219
|
+
const out = [];
|
|
220
|
+
for (let i = 0; i < requests.length; i++) {
|
|
221
|
+
out.push(await runOne(requests[i], i));
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
})()
|
|
225
|
+
: await Promise.all(requests.map((req, i) => runOne(req, i)));
|
|
226
|
+
// 6. 将 tool results 转换为 ToolMessage[](不再注入到 graphState.messages,避免重复)
|
|
227
|
+
//
|
|
228
|
+
// 重要:messages channel 的 reducer 是 **append 语义**([...current, ...update]),
|
|
229
|
+
// ToolNode 只返回 **新增的 toolMessage 数组**,reducer 自动追加到 current。
|
|
230
|
+
// 若返回 [...currentMessages, ...toolMessages],会导致所有消息被追加两次
|
|
231
|
+
// (这也是早期版本里 user / assistant / tool 消息重复的根因)。
|
|
232
|
+
const toolMessages = execResults.map((result) => ({
|
|
233
|
+
role: 'tool',
|
|
234
|
+
content: result.success
|
|
235
|
+
? result.content
|
|
236
|
+
: `[Tool Error] ${result.error ?? 'unknown error'}`,
|
|
237
|
+
// 以下字段符合 LLM function-calling 协议(ToolMessage 接口)
|
|
238
|
+
toolCallId: result.toolCallId,
|
|
239
|
+
toolName: result.toolName,
|
|
240
|
+
success: result.success,
|
|
241
|
+
...('file' in result && result.file ? { file: result.file } : {}),
|
|
242
|
+
}));
|
|
243
|
+
// 7. 返回更新后的 state(仅含新增 messages + currentNode)
|
|
244
|
+
return {
|
|
245
|
+
messages: toolMessages,
|
|
246
|
+
currentNode: nodeId,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ─────────────────────────────────────────────
|
|
250
|
+
// Private methods
|
|
251
|
+
// ─────────────────────────────────────────────
|
|
252
|
+
/**
|
|
253
|
+
* 从 state 中提取 messages 数组
|
|
254
|
+
*/
|
|
255
|
+
extractMessages(state) {
|
|
256
|
+
const raw = state['messages'];
|
|
257
|
+
if (!Array.isArray(raw)) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
return raw.map((msg) => {
|
|
261
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
262
|
+
const m = msg;
|
|
263
|
+
return {
|
|
264
|
+
role: m['role'] ?? 'user',
|
|
265
|
+
content: m['content'] ?? '',
|
|
266
|
+
toolCalls: m['toolCalls'],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { role: 'user', content: String(msg) };
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 从 messages 中找到最后一条 assistant 消息
|
|
274
|
+
*/
|
|
275
|
+
findLastAssistantMessage(messages) {
|
|
276
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
277
|
+
if (messages[i].role === 'assistant') {
|
|
278
|
+
return messages[i];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
buildAuthContext(state) {
|
|
284
|
+
const executionContext = state['executionContext'];
|
|
285
|
+
// 旧 Checkpoint / 历史调用方兼容:agentId 仅作读兜底
|
|
286
|
+
const agentCode = String(state['agentCode'] ??
|
|
287
|
+
state['agentId'] ??
|
|
288
|
+
this.defaultAuthContext.agentCode ??
|
|
289
|
+
this.defaultAuthContext.agentId);
|
|
290
|
+
// 优先 ExecutionContext(invoke Token.sub),保证所有 Tool/MCP 出站同一身份
|
|
291
|
+
const userId = String(executionContext?.userId ?? state['userId'] ?? this.defaultAuthContext.userId);
|
|
292
|
+
const delegationToken = String(executionContext?.delegationToken ??
|
|
293
|
+
state['delegationToken'] ??
|
|
294
|
+
this.defaultAuthContext.delegationToken);
|
|
295
|
+
return {
|
|
296
|
+
userId,
|
|
297
|
+
delegationToken,
|
|
298
|
+
agentCode,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
shouldForceServer(state, toolName) {
|
|
302
|
+
const forceList = state['forceToolServer'];
|
|
303
|
+
return Array.isArray(forceList) && forceList.includes(toolName);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 将 LangGraphState 转换为 GraphState(供 injector 使用)
|
|
307
|
+
*/
|
|
308
|
+
toGraphState(state) {
|
|
309
|
+
const messages = this.extractMessages(state);
|
|
310
|
+
return {
|
|
311
|
+
...state,
|
|
312
|
+
messages,
|
|
313
|
+
userId: state['userId'] ?? '',
|
|
314
|
+
delegationToken: state['delegationToken'],
|
|
315
|
+
loadedTools: state['loadedTools'],
|
|
316
|
+
forceToolServer: state['forceToolServer'],
|
|
317
|
+
executionContext: state['executionContext'],
|
|
318
|
+
agentCode: (state['agentCode'] ?? state['agentId']),
|
|
319
|
+
correlationId: state['correlationId'],
|
|
320
|
+
taskId: state['taskId'],
|
|
321
|
+
sessionId: state['sessionId'],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface ResolveGraphPromptOptions {
|
|
2
|
+
allowAbsolute?: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function isGraphPromptPath(value: string): boolean;
|
|
5
|
+
export declare function resolveGraphPrompt(raw: string | undefined, roots: string[], options?: ResolveGraphPromptOptions): string | undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图节点提示词:内联正文,或文件路径。
|
|
3
|
+
* - `./prompts/agent.md`:相对 promptRoots(agentDir / 工程根)
|
|
4
|
+
* - `/abs/path/agent.md`:绝对路径;dev/desktop 可读,server 仅当落在 promptRoots 内
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
8
|
+
const REL_RE = /^\.\/[A-Za-z0-9._/-]+\.(md|txt)$/;
|
|
9
|
+
const ABS_RE = /^(?:\/|[A-Za-z]:[\\/])[^\r\n]+\.(md|txt)$/i;
|
|
10
|
+
export function isGraphPromptPath(value) {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return REL_RE.test(trimmed) || ABS_RE.test(trimmed);
|
|
13
|
+
}
|
|
14
|
+
export function resolveGraphPrompt(raw, roots, options = {}) {
|
|
15
|
+
if (typeof raw !== 'string')
|
|
16
|
+
return undefined;
|
|
17
|
+
const trimmed = raw.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (!isGraphPromptPath(trimmed))
|
|
21
|
+
return raw;
|
|
22
|
+
if (REL_RE.test(trimmed)) {
|
|
23
|
+
return readRelative(trimmed, roots);
|
|
24
|
+
}
|
|
25
|
+
return readAbsolute(trimmed, roots, options.allowAbsolute ?? defaultAllowAbsolute());
|
|
26
|
+
}
|
|
27
|
+
function defaultAllowAbsolute() {
|
|
28
|
+
const ctx = (process.env.EAP_DEPLOYMENT_CONTEXT ?? '').toLowerCase();
|
|
29
|
+
return ctx === 'dev' || ctx === 'desktop';
|
|
30
|
+
}
|
|
31
|
+
function readRelative(spec, roots) {
|
|
32
|
+
const relativePath = spec.slice(2).replace(/\\/g, '/');
|
|
33
|
+
if (relativePath.split('/').some((part) => part === '' || part === '.' || part === '..')) {
|
|
34
|
+
throw new Error(`Graph prompt path is not portable: ${spec}`);
|
|
35
|
+
}
|
|
36
|
+
for (const root of roots) {
|
|
37
|
+
if (!root)
|
|
38
|
+
continue;
|
|
39
|
+
const candidate = resolve(root, relativePath);
|
|
40
|
+
if (!isInside(root, candidate))
|
|
41
|
+
continue;
|
|
42
|
+
const text = readIfFile(candidate);
|
|
43
|
+
if (text !== undefined)
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Graph prompt file not found: ${spec}`);
|
|
47
|
+
}
|
|
48
|
+
function readAbsolute(spec, roots, allowAbsolute) {
|
|
49
|
+
const candidate = resolve(spec);
|
|
50
|
+
if (!isAbsolute(candidate) || candidate.includes(`..${sep}`) || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(spec)) {
|
|
51
|
+
throw new Error(`Graph prompt path is not portable: ${spec}`);
|
|
52
|
+
}
|
|
53
|
+
const underRoot = roots.some((root) => root && isInside(root, candidate));
|
|
54
|
+
if (!underRoot && !allowAbsolute) {
|
|
55
|
+
throw new Error(`Absolute graph prompt path is not allowed outside the Agent directory: ${spec}`);
|
|
56
|
+
}
|
|
57
|
+
const text = readIfFile(candidate);
|
|
58
|
+
if (text === undefined)
|
|
59
|
+
throw new Error(`Graph prompt file not found: ${spec}`);
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
function readIfFile(candidate) {
|
|
63
|
+
try {
|
|
64
|
+
if (!statSync(candidate).isFile())
|
|
65
|
+
return undefined;
|
|
66
|
+
return readFileSync(candidate, 'utf8').replace(/^\uFEFF/, '');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function isInside(root, target) {
|
|
73
|
+
const relativePath = relative(resolve(root), resolve(target));
|
|
74
|
+
return relativePath === '' || (!relativePath.startsWith(`..${sep}`) && relativePath !== '..');
|
|
75
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BudgetAwareRetryExecutor — Token Budget 感知的重试执行器
|
|
3
|
+
*
|
|
4
|
+
* 扩展 NodeRetryExecutor,在重试时检查 Token Budget:
|
|
5
|
+
* - 重试产生的 Token 消耗计入配额
|
|
6
|
+
* - 配额耗尽时不再重试
|
|
7
|
+
*
|
|
8
|
+
* @see node-retry.ts 基础重试逻辑
|
|
9
|
+
* @see token/token-budget.service.ts Token Budget 控制
|
|
10
|
+
* @see requirements.md Requirement 12.4
|
|
11
|
+
*/
|
|
12
|
+
import { NodeRetryExecutor } from './node-retry.js';
|
|
13
|
+
import type { NodeRetryConfig } from '../contracts/executable-graph.js';
|
|
14
|
+
/**
|
|
15
|
+
* Token 消耗回调接口
|
|
16
|
+
*
|
|
17
|
+
* 由外部注入,用于报告重试过程中的 Token 消耗。
|
|
18
|
+
*/
|
|
19
|
+
export interface TokenConsumptionCallback {
|
|
20
|
+
/**
|
|
21
|
+
* 报告 Token 消耗
|
|
22
|
+
*
|
|
23
|
+
* @param nodeId - 节点 ID
|
|
24
|
+
* @param tokens - 消耗的 Token 数
|
|
25
|
+
* @returns 是否还有剩余配额(false 表示配额耗尽)
|
|
26
|
+
*/
|
|
27
|
+
reportConsumption(nodeId: string, tokens: number): Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* BudgetAwareRetryConfig — 带预算感知的重试配置
|
|
31
|
+
*/
|
|
32
|
+
export interface BudgetAwareRetryConfig extends NodeRetryConfig {
|
|
33
|
+
/** 每次重试预估的 Token 消耗 */
|
|
34
|
+
estimatedTokensPerRetry?: number;
|
|
35
|
+
/** Token 消耗回调 */
|
|
36
|
+
tokenCallback?: TokenConsumptionCallback;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* BudgetExhaustedError — Token 预算耗尽错误
|
|
40
|
+
*
|
|
41
|
+
* 当重试过程中 Token 预算耗尽时抛出。
|
|
42
|
+
*/
|
|
43
|
+
export declare class BudgetExhaustedError extends Error {
|
|
44
|
+
readonly nodeId: string;
|
|
45
|
+
readonly consumedTokens: number;
|
|
46
|
+
readonly attempt: number;
|
|
47
|
+
constructor(opts: {
|
|
48
|
+
nodeId: string;
|
|
49
|
+
consumedTokens: number;
|
|
50
|
+
attempt: number;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* BudgetAwareRetryExecutor — Token Budget 感知的重试执行器
|
|
55
|
+
*
|
|
56
|
+
* 在重试前检查 Token Budget,配额耗尽时停止重试。
|
|
57
|
+
*/
|
|
58
|
+
export declare class BudgetAwareRetryExecutor extends NodeRetryExecutor {
|
|
59
|
+
private readonly estimatedTokensPerRetry;
|
|
60
|
+
private readonly tokenCallback?;
|
|
61
|
+
private totalConsumedTokens;
|
|
62
|
+
constructor(nodeId: string, config?: BudgetAwareRetryConfig);
|
|
63
|
+
/**
|
|
64
|
+
* 带预算检查的重试执行
|
|
65
|
+
*
|
|
66
|
+
* @param fn - 要执行的异步函数
|
|
67
|
+
* @param config - 可选覆盖重试配置
|
|
68
|
+
* @param abortSignal - 可选 AbortSignal
|
|
69
|
+
* @returns RetryResult 包含结果和观测数据
|
|
70
|
+
* @throws RetryExhaustedError 当所有重试用尽
|
|
71
|
+
* @throws BudgetExhaustedError 当 Token 预算耗尽
|
|
72
|
+
*/
|
|
73
|
+
executeWithBudgetCheck<T>(fn: () => Promise<T>, config?: BudgetAwareRetryConfig, abortSignal?: AbortSignal): Promise<{
|
|
74
|
+
result: T;
|
|
75
|
+
totalConsumedTokens: number;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* 获取已消耗的 Token 数
|
|
79
|
+
*/
|
|
80
|
+
getTotalConsumedTokens(): number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* withBudgetRetry — 将节点函数包装为带预算检查的重试版本
|
|
84
|
+
*
|
|
85
|
+
* @param nodeId - 节点标识
|
|
86
|
+
* @param nodeFn - 原始节点函数
|
|
87
|
+
* @param config - 重试配置(含 Token Budget 回调)
|
|
88
|
+
* @param abortSignal - 可选 AbortSignal
|
|
89
|
+
* @returns 包装后的节点函数
|
|
90
|
+
*/
|
|
91
|
+
export declare function withBudgetRetry(nodeId: string, nodeFn: (state: Record<string, unknown>) => Promise<Record<string, unknown>>, config: BudgetAwareRetryConfig, abortSignal?: AbortSignal): (state: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BudgetAwareRetryExecutor — Token Budget 感知的重试执行器
|
|
3
|
+
*
|
|
4
|
+
* 扩展 NodeRetryExecutor,在重试时检查 Token Budget:
|
|
5
|
+
* - 重试产生的 Token 消耗计入配额
|
|
6
|
+
* - 配额耗尽时不再重试
|
|
7
|
+
*
|
|
8
|
+
* @see node-retry.ts 基础重试逻辑
|
|
9
|
+
* @see token/token-budget.service.ts Token Budget 控制
|
|
10
|
+
* @see requirements.md Requirement 12.4
|
|
11
|
+
*/
|
|
12
|
+
import { NodeRetryExecutor } from './node-retry.js';
|
|
13
|
+
// ─────────────────────────────────────────────
|
|
14
|
+
// BudgetExhaustedError
|
|
15
|
+
// ─────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* BudgetExhaustedError — Token 预算耗尽错误
|
|
18
|
+
*
|
|
19
|
+
* 当重试过程中 Token 预算耗尽时抛出。
|
|
20
|
+
*/
|
|
21
|
+
export class BudgetExhaustedError extends Error {
|
|
22
|
+
nodeId;
|
|
23
|
+
consumedTokens;
|
|
24
|
+
attempt;
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
super(`Node "${opts.nodeId}" retry aborted: Token budget exhausted after ${opts.attempt} attempts (${opts.consumedTokens} tokens consumed)`);
|
|
27
|
+
this.name = 'BudgetExhaustedError';
|
|
28
|
+
this.nodeId = opts.nodeId;
|
|
29
|
+
this.consumedTokens = opts.consumedTokens;
|
|
30
|
+
this.attempt = opts.attempt;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─────────────────────────────────────────────
|
|
34
|
+
// BudgetAwareRetryExecutor
|
|
35
|
+
// ─────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* BudgetAwareRetryExecutor — Token Budget 感知的重试执行器
|
|
38
|
+
*
|
|
39
|
+
* 在重试前检查 Token Budget,配额耗尽时停止重试。
|
|
40
|
+
*/
|
|
41
|
+
export class BudgetAwareRetryExecutor extends NodeRetryExecutor {
|
|
42
|
+
estimatedTokensPerRetry;
|
|
43
|
+
tokenCallback;
|
|
44
|
+
totalConsumedTokens = 0;
|
|
45
|
+
constructor(nodeId, config) {
|
|
46
|
+
super(nodeId, config);
|
|
47
|
+
this.estimatedTokensPerRetry = config?.estimatedTokensPerRetry ?? 1000;
|
|
48
|
+
this.tokenCallback = config?.tokenCallback;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 带预算检查的重试执行
|
|
52
|
+
*
|
|
53
|
+
* @param fn - 要执行的异步函数
|
|
54
|
+
* @param config - 可选覆盖重试配置
|
|
55
|
+
* @param abortSignal - 可选 AbortSignal
|
|
56
|
+
* @returns RetryResult 包含结果和观测数据
|
|
57
|
+
* @throws RetryExhaustedError 当所有重试用尽
|
|
58
|
+
* @throws BudgetExhaustedError 当 Token 预算耗尽
|
|
59
|
+
*/
|
|
60
|
+
async executeWithBudgetCheck(fn, config, abortSignal) {
|
|
61
|
+
const effectiveConfig = {
|
|
62
|
+
maxRetries: config?.maxRetries ?? this.getConfig().maxRetries,
|
|
63
|
+
backoff: config?.backoff ?? this.getConfig().backoff,
|
|
64
|
+
estimatedTokensPerRetry: config?.estimatedTokensPerRetry ?? this.estimatedTokensPerRetry,
|
|
65
|
+
tokenCallback: config?.tokenCallback ?? this.tokenCallback,
|
|
66
|
+
};
|
|
67
|
+
this.totalConsumedTokens = 0;
|
|
68
|
+
try {
|
|
69
|
+
const { result, totalAttempts } = await this.executeWithRetry(async () => {
|
|
70
|
+
// 执行前检查 Token Budget
|
|
71
|
+
if (effectiveConfig.tokenCallback) {
|
|
72
|
+
const hasBudget = await effectiveConfig.tokenCallback.reportConsumption(this.getNodeId(), effectiveConfig.estimatedTokensPerRetry);
|
|
73
|
+
if (!hasBudget) {
|
|
74
|
+
throw new BudgetExhaustedError({
|
|
75
|
+
nodeId: this.getNodeId(),
|
|
76
|
+
consumedTokens: this.totalConsumedTokens,
|
|
77
|
+
attempt: totalAttempts,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 执行函数
|
|
82
|
+
const result = await fn();
|
|
83
|
+
// 记录 Token 消耗
|
|
84
|
+
this.totalConsumedTokens += effectiveConfig.estimatedTokensPerRetry;
|
|
85
|
+
return result;
|
|
86
|
+
}, config, abortSignal);
|
|
87
|
+
return {
|
|
88
|
+
result,
|
|
89
|
+
totalConsumedTokens: this.totalConsumedTokens,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// 如果是 BudgetExhaustedError,直接抛出
|
|
94
|
+
if (error instanceof BudgetExhaustedError) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
// 其他错误重新抛出
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 获取已消耗的 Token 数
|
|
103
|
+
*/
|
|
104
|
+
getTotalConsumedTokens() {
|
|
105
|
+
return this.totalConsumedTokens;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// ─────────────────────────────────────────────
|
|
109
|
+
// withBudgetRetry — 高阶包装函数
|
|
110
|
+
// ─────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* withBudgetRetry — 将节点函数包装为带预算检查的重试版本
|
|
113
|
+
*
|
|
114
|
+
* @param nodeId - 节点标识
|
|
115
|
+
* @param nodeFn - 原始节点函数
|
|
116
|
+
* @param config - 重试配置(含 Token Budget 回调)
|
|
117
|
+
* @param abortSignal - 可选 AbortSignal
|
|
118
|
+
* @returns 包装后的节点函数
|
|
119
|
+
*/
|
|
120
|
+
export function withBudgetRetry(nodeId, nodeFn, config, abortSignal) {
|
|
121
|
+
const executor = new BudgetAwareRetryExecutor(nodeId, config);
|
|
122
|
+
return async (state) => {
|
|
123
|
+
const { result } = await executor.executeWithBudgetCheck(() => nodeFn(state), undefined, abortSignal);
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Module — 节点级重试
|
|
3
|
+
*
|
|
4
|
+
* 导出通用重试执行器、错误类型和高阶包装函数。
|
|
5
|
+
*/
|
|
6
|
+
export { NodeRetryExecutor, RetryExhaustedError, withRetry, DEFAULT_LLM_RETRY_CONFIG, DEFAULT_NODE_RETRY_CONFIG, } from './node-retry.js';
|
|
7
|
+
export type { NodeFunction, RetryAttemptInfo, RetryResult, } from './node-retry.js';
|
|
8
|
+
export { BudgetAwareRetryExecutor, BudgetExhaustedError, withBudgetRetry, } from './budget-aware-retry.js';
|
|
9
|
+
export type { BudgetAwareRetryConfig, TokenConsumptionCallback, } from './budget-aware-retry.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Module — 节点级重试
|
|
3
|
+
*
|
|
4
|
+
* 导出通用重试执行器、错误类型和高阶包装函数。
|
|
5
|
+
*/
|
|
6
|
+
export { NodeRetryExecutor, RetryExhaustedError, withRetry, DEFAULT_LLM_RETRY_CONFIG, DEFAULT_NODE_RETRY_CONFIG, } from './node-retry.js';
|
|
7
|
+
// Token Budget 感知的重试
|
|
8
|
+
export { BudgetAwareRetryExecutor, BudgetExhaustedError, withBudgetRetry, } from './budget-aware-retry.js';
|