@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,1303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangGraph Adapter — IStateGraphEngine 的 v1 实现
|
|
3
|
+
*
|
|
4
|
+
* 将平台 StateGraph Contract 映射到 LangGraph.js API,
|
|
5
|
+
* 实现 Graph 的构建、流式执行、同步调用、Checkpoint 和恢复。
|
|
6
|
+
*
|
|
7
|
+
* @see design.md §1.4 StateGraph Contract 与 LangGraph.js 集成
|
|
8
|
+
* @see requirements.md Requirement 1 AC5
|
|
9
|
+
*/
|
|
10
|
+
import { StateGraph, START, END, } from '@langchain/langgraph';
|
|
11
|
+
import { resolveCheckpointThreadId } from '../../session/persist-turn-messages.js';
|
|
12
|
+
import { LangGraphCheckpointer } from './langgraph-checkpointer.js';
|
|
13
|
+
import { SubGraphCompiler } from './subgraph-compiler.js';
|
|
14
|
+
import { LLMNode } from '../nodes/llm-node.js';
|
|
15
|
+
import { ToolNode } from '../nodes/tool-node.js';
|
|
16
|
+
import { ToolOrchestrator } from '../../tool/tool-orchestrator.js';
|
|
17
|
+
import { ToolResultInjector } from '../../tool/tool-result-injector.js';
|
|
18
|
+
import { fileArtifactFromToolOutput } from '../../tool/file-artifact.js';
|
|
19
|
+
import { EchoLLMProvider } from '../../providers/echo-llm-provider.js';
|
|
20
|
+
import { ConditionEvaluator } from '../condition-evaluator.js';
|
|
21
|
+
import { SystemPromptBuilder, loadedToolsToApiTools, filterLoadedToolsForSkill } from '../../tool/system-prompt-builder.js';
|
|
22
|
+
import { bindSelectedSkill } from '../nodes/skill-bind-node.js';
|
|
23
|
+
import { resolveGraphPrompt } from '../resolve-graph-prompt.js';
|
|
24
|
+
import { SkillAuditRecorder } from '../skill-audit-recorder.js';
|
|
25
|
+
import { preloadSkillNotFound } from '../../registry/preload-errors.js';
|
|
26
|
+
import { AppError, ErrorCodes } from '@gt-fe/eap-contracts';
|
|
27
|
+
import { isHitlDecision, lastAssistantHasUnansweredToolCalls } from '../hitl/hitl-handler.js';
|
|
28
|
+
// ─────────────────────────────────────────────
|
|
29
|
+
// LangGraphAdapter
|
|
30
|
+
// ─────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* LangGraphAdapter — 基于 @langchain/langgraph 的 IStateGraphEngine 实现
|
|
33
|
+
*
|
|
34
|
+
* 职责:
|
|
35
|
+
* 1. 解析 GraphDSL → 注册节点、添加边/条件路由 → 编译为可执行图
|
|
36
|
+
* 2. 调用 LangGraph 的 stream/invoke 驱动执行
|
|
37
|
+
* 3. 提供 Checkpoint 提取与恢复能力
|
|
38
|
+
*/
|
|
39
|
+
export class LangGraphAdapter {
|
|
40
|
+
checkpointer;
|
|
41
|
+
subGraphCompiler;
|
|
42
|
+
llmProvider;
|
|
43
|
+
toolExecutor;
|
|
44
|
+
toolOrchestrator;
|
|
45
|
+
toolResultInjector;
|
|
46
|
+
conditionEvaluator;
|
|
47
|
+
systemPromptBuilder;
|
|
48
|
+
skillAuditRecorder;
|
|
49
|
+
/** 流式旁路:LLM token / Tool 执行事件在节点执行中实时入队 */
|
|
50
|
+
streamSink = null;
|
|
51
|
+
streamEventContext = {};
|
|
52
|
+
streamChunkIndex = 0;
|
|
53
|
+
streamIteration = 0;
|
|
54
|
+
reasoningStartedThisTurn = false;
|
|
55
|
+
streamedTextThisTurn = false;
|
|
56
|
+
/**
|
|
57
|
+
* @param checkpointSaver - 可选的 ICheckpointSaver
|
|
58
|
+
* @param llmProvider - LLM 调用提供者,默认 EchoLLMProvider
|
|
59
|
+
* @param toolExecutor - Tool 执行器(HttpToolExecutor 等)
|
|
60
|
+
*/
|
|
61
|
+
governanceClient;
|
|
62
|
+
hitlHandler;
|
|
63
|
+
constructor(checkpointSaver, llmProvider, toolExecutor, governanceClient, hitlHandler) {
|
|
64
|
+
this.governanceClient = governanceClient;
|
|
65
|
+
this.hitlHandler = hitlHandler;
|
|
66
|
+
this.checkpointer = checkpointSaver
|
|
67
|
+
? new LangGraphCheckpointer(checkpointSaver)
|
|
68
|
+
: undefined;
|
|
69
|
+
this.llmProvider = llmProvider ?? new EchoLLMProvider();
|
|
70
|
+
this.toolExecutor = toolExecutor;
|
|
71
|
+
this.toolOrchestrator = toolExecutor
|
|
72
|
+
? new ToolOrchestrator(toolExecutor, governanceClient)
|
|
73
|
+
: undefined;
|
|
74
|
+
this.toolResultInjector = new ToolResultInjector();
|
|
75
|
+
this.conditionEvaluator = new ConditionEvaluator();
|
|
76
|
+
this.systemPromptBuilder = new SystemPromptBuilder();
|
|
77
|
+
this.skillAuditRecorder = new SkillAuditRecorder();
|
|
78
|
+
// ── 内置 ReAct 标准条件函数(shouldContinue)────────────────────────
|
|
79
|
+
// 判断最后一条 assistant 消息是否有 tool_calls:
|
|
80
|
+
// 有 → "call_tools"(进 tools 节点执行)
|
|
81
|
+
// 无 → "finish"(进 __end__,对话结束)
|
|
82
|
+
// 该函数会被所有 dsl 中 conditionFn="shouldContinue" 的 conditionalEdge 使用。
|
|
83
|
+
this.conditionEvaluator.register('shouldContinue', (state) => {
|
|
84
|
+
const messages = state['messages'];
|
|
85
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
86
|
+
return 'finish';
|
|
87
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
88
|
+
const msg = messages[i];
|
|
89
|
+
if (!msg || msg.role !== 'assistant')
|
|
90
|
+
continue;
|
|
91
|
+
const toolCalls = (msg.toolCalls ?? msg.tool_calls);
|
|
92
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0)
|
|
93
|
+
return 'call_tools';
|
|
94
|
+
return 'finish';
|
|
95
|
+
}
|
|
96
|
+
return 'finish';
|
|
97
|
+
});
|
|
98
|
+
this.conditionEvaluator.register('afterSkillRoute', (state) => {
|
|
99
|
+
const route = state['skillRoute'];
|
|
100
|
+
if (route === 'clarify')
|
|
101
|
+
return 'clarify';
|
|
102
|
+
if (route === 'skill') {
|
|
103
|
+
const skillId = state['activatedSkill'];
|
|
104
|
+
if (typeof skillId === 'string' && skillId.trim())
|
|
105
|
+
return skillId.trim();
|
|
106
|
+
return 'skill';
|
|
107
|
+
}
|
|
108
|
+
return 'none';
|
|
109
|
+
});
|
|
110
|
+
// 必须在其他依赖初始化后再创建 SubGraphCompiler(它持有 adapter 引用)
|
|
111
|
+
this.subGraphCompiler = new SubGraphCompiler(this, governanceClient, this.skillAuditRecorder);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 注册自定义条件评估函数
|
|
115
|
+
*/
|
|
116
|
+
registerConditionEvaluator(name, fn) {
|
|
117
|
+
this.conditionEvaluator.register(name, fn);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 节点旁路推送 RuntimeEvent(子图 Skill 生命周期、Tool 执行)。
|
|
121
|
+
* 无 streamSink 时为空操作(纯 invoke)。
|
|
122
|
+
*/
|
|
123
|
+
emitStreamEvent(event) {
|
|
124
|
+
if (!this.streamSink)
|
|
125
|
+
return;
|
|
126
|
+
this.streamSink({
|
|
127
|
+
...event,
|
|
128
|
+
taskId: event.taskId ?? this.streamEventContext.taskId,
|
|
129
|
+
correlationId: event.correlationId ?? this.streamEventContext.correlationId,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 子图 LLM 可清零流式标志;结束后保留「已经 stream 过」以免父图 convertUpdates 再发一遍正文。
|
|
134
|
+
*/
|
|
135
|
+
async runIsolatedLlmStreamFlags(work) {
|
|
136
|
+
const savedStreamed = this.streamedTextThisTurn;
|
|
137
|
+
const savedReasoning = this.reasoningStartedThisTurn;
|
|
138
|
+
this.reasoningStartedThisTurn = false;
|
|
139
|
+
this.streamedTextThisTurn = false;
|
|
140
|
+
try {
|
|
141
|
+
return await work();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.streamedTextThisTurn = savedStreamed || this.streamedTextThisTurn;
|
|
145
|
+
this.reasoningStartedThisTurn = savedReasoning || this.reasoningStartedThisTurn;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Tool 节点执行旁路:started → tool.called + tool.progress;
|
|
150
|
+
* completed/error → 对应终态 + tool.progress。同一 toolCallId。
|
|
151
|
+
*/
|
|
152
|
+
emitToolExecutionEvents(progress) {
|
|
153
|
+
if (!this.streamSink)
|
|
154
|
+
return;
|
|
155
|
+
const timestamp = new Date().toISOString();
|
|
156
|
+
const { taskId, correlationId } = this.streamEventContext;
|
|
157
|
+
const loc = {
|
|
158
|
+
...(progress.node ? { node: progress.node } : {}),
|
|
159
|
+
...(progress.skillId ? { skillId: progress.skillId } : {}),
|
|
160
|
+
};
|
|
161
|
+
if (progress.status === 'started') {
|
|
162
|
+
this.streamSink({
|
|
163
|
+
type: 'tool.called',
|
|
164
|
+
taskId,
|
|
165
|
+
correlationId,
|
|
166
|
+
timestamp,
|
|
167
|
+
payload: {
|
|
168
|
+
toolCallId: progress.toolCallId,
|
|
169
|
+
toolName: progress.toolName,
|
|
170
|
+
arguments: progress.arguments ?? {},
|
|
171
|
+
...loc,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
this.streamSink({
|
|
176
|
+
type: 'tool.progress',
|
|
177
|
+
taskId,
|
|
178
|
+
correlationId,
|
|
179
|
+
timestamp,
|
|
180
|
+
payload: { ...progress },
|
|
181
|
+
});
|
|
182
|
+
if (progress.status === 'completed') {
|
|
183
|
+
this.streamSink({
|
|
184
|
+
type: 'tool.result',
|
|
185
|
+
taskId,
|
|
186
|
+
correlationId,
|
|
187
|
+
timestamp,
|
|
188
|
+
payload: {
|
|
189
|
+
toolCallId: progress.toolCallId,
|
|
190
|
+
toolName: progress.toolName,
|
|
191
|
+
content: progress.content,
|
|
192
|
+
success: true,
|
|
193
|
+
...loc,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (progress.status === 'error') {
|
|
198
|
+
this.streamSink({
|
|
199
|
+
type: 'tool.error',
|
|
200
|
+
taskId,
|
|
201
|
+
correlationId,
|
|
202
|
+
timestamp,
|
|
203
|
+
payload: {
|
|
204
|
+
toolCallId: progress.toolCallId,
|
|
205
|
+
toolName: progress.toolName,
|
|
206
|
+
error: progress.error ?? 'Unknown error',
|
|
207
|
+
willRetry: false,
|
|
208
|
+
...loc,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 解析 GraphDSL 并构建可执行图
|
|
215
|
+
*
|
|
216
|
+
* **Request Isolation**: 每次调用都创建全新的 StateGraph 实例并编译,
|
|
217
|
+
* 确保并发请求之间无共享可变状态。Adapter 自身仅持有 readonly 配置(checkpointer、subGraphCompiler),
|
|
218
|
+
* 不缓存任何编译产物或运行时状态。
|
|
219
|
+
*
|
|
220
|
+
* 流程:
|
|
221
|
+
* 1. 创建 StateGraph 实例(配置 channels)
|
|
222
|
+
* 2. 遍历 DSL nodes → addNode 注册节点函数
|
|
223
|
+
* 3. 遍历 DSL edges → addEdge 添加普通边
|
|
224
|
+
* 4. 遍历 DSL conditionalEdges → addConditionalEdges 添加条件路由
|
|
225
|
+
* 5. 设置入口/终止节点
|
|
226
|
+
* 6. compile() 编译
|
|
227
|
+
*/
|
|
228
|
+
async buildGraph(dsl) {
|
|
229
|
+
this.assertShouldContinueTargetsTool(dsl);
|
|
230
|
+
const silentNodeIds = new Set(dsl.nodes
|
|
231
|
+
.filter((node) => node.type === 'llm' && node.config?.['silent'] === true)
|
|
232
|
+
.map((node) => node.id));
|
|
233
|
+
const subgraphNodeIds = new Set(dsl.nodes.filter((node) => node.type === 'subgraph').map((node) => node.id));
|
|
234
|
+
// 创建 StateGraph,使用简单的 messages channel
|
|
235
|
+
// 辅助 channel 采用 ephemeral 语义(value=最后写入者胜出),保证 loadedTools/executionContext
|
|
236
|
+
// 等运行期上下文在节点间透传,不会被 LangGraph 按 schema 过滤掉。
|
|
237
|
+
const ephemeral = (defaultValue) => ({
|
|
238
|
+
value: (_current, update) => (update === undefined ? _current : update),
|
|
239
|
+
default: () => defaultValue,
|
|
240
|
+
});
|
|
241
|
+
const stateGraph = new StateGraph({
|
|
242
|
+
channels: {
|
|
243
|
+
messages: {
|
|
244
|
+
value: (current, update) => {
|
|
245
|
+
if (update && typeof update === 'object' && !Array.isArray(update)) {
|
|
246
|
+
const replace = update.__set;
|
|
247
|
+
if (Array.isArray(replace))
|
|
248
|
+
return replace;
|
|
249
|
+
}
|
|
250
|
+
return Array.isArray(current) && Array.isArray(update)
|
|
251
|
+
? [...current, ...update]
|
|
252
|
+
: update ?? current;
|
|
253
|
+
},
|
|
254
|
+
default: () => [],
|
|
255
|
+
},
|
|
256
|
+
currentNode: {
|
|
257
|
+
value: (_current, update) => update,
|
|
258
|
+
default: () => '',
|
|
259
|
+
},
|
|
260
|
+
// ── 运行期上下文(ephemeral):由 invoke/stream 入口注入,节点只读 ──
|
|
261
|
+
userId: ephemeral(''),
|
|
262
|
+
// agentId 已废弃:channel 保留仅为旧 Checkpoint 反序列化兼容,新代码只写 agentCode
|
|
263
|
+
agentId: ephemeral(''),
|
|
264
|
+
agentCode: ephemeral(''),
|
|
265
|
+
agentVersion: ephemeral(undefined),
|
|
266
|
+
delegationToken: ephemeral(''),
|
|
267
|
+
correlationId: ephemeral(''),
|
|
268
|
+
taskId: ephemeral(''),
|
|
269
|
+
sessionId: ephemeral(''),
|
|
270
|
+
loadedTools: ephemeral(undefined),
|
|
271
|
+
loadedSkills: ephemeral(undefined),
|
|
272
|
+
skillDefinitions: ephemeral(undefined),
|
|
273
|
+
skillInput: ephemeral(undefined),
|
|
274
|
+
activeSkillPrompt: ephemeral(undefined),
|
|
275
|
+
activatedSkill: ephemeral(undefined),
|
|
276
|
+
skillRoute: ephemeral(undefined),
|
|
277
|
+
skillSelectRaw: ephemeral(undefined),
|
|
278
|
+
silentOutput: ephemeral(undefined),
|
|
279
|
+
promptRoots: ephemeral(undefined),
|
|
280
|
+
executionContext: ephemeral(undefined),
|
|
281
|
+
forceToolServer: ephemeral(undefined),
|
|
282
|
+
traceCollector: ephemeral(undefined),
|
|
283
|
+
traceId: ephemeral(undefined),
|
|
284
|
+
skillSpanId: ephemeral(undefined),
|
|
285
|
+
hitlChoice: ephemeral(undefined),
|
|
286
|
+
// ── LLM Token 使用量:数组累加器 reducer,每次 LLM 节点追加一条使用明细
|
|
287
|
+
__llmTokenUsage: {
|
|
288
|
+
value: (current, update) => {
|
|
289
|
+
const cur = Array.isArray(current) ? current : [];
|
|
290
|
+
const upd = Array.isArray(update) ? update : [];
|
|
291
|
+
return [...cur, ...upd];
|
|
292
|
+
},
|
|
293
|
+
default: () => [],
|
|
294
|
+
},
|
|
295
|
+
__parallelResults: ephemeral(undefined),
|
|
296
|
+
__codeResult: ephemeral(undefined),
|
|
297
|
+
__codeError: ephemeral(undefined),
|
|
298
|
+
__conditionResult: ephemeral(undefined),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
// 注册节点
|
|
302
|
+
for (const node of dsl.nodes) {
|
|
303
|
+
const nodeFn = this.createNodeFunction(node);
|
|
304
|
+
stateGraph.addNode(node.id, nodeFn);
|
|
305
|
+
}
|
|
306
|
+
// 入口:优先 edges 里的 __start__;若同时配置了 entryPoint,只保留一种,避免 LangGraph
|
|
307
|
+
// “EphemeralValue can only receive one value per step”(__start__ 被加两次)
|
|
308
|
+
const entryPoint = dsl.entryPoint ?? dsl.nodes[0]?.id;
|
|
309
|
+
const hasStartEdge = dsl.edges.some((e) => e.from === '__start__');
|
|
310
|
+
// 添加普通边
|
|
311
|
+
for (const edge of dsl.edges) {
|
|
312
|
+
if (edge.from === '__start__' && entryPoint && entryPoint !== '__start__' && hasStartEdge) {
|
|
313
|
+
// 用 setEntryPoint(entryPoint) 统一入口,跳过显式 __start__ 边
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const from = edge.from === '__start__' ? START : edge.from;
|
|
317
|
+
const to = edge.to === '__end__' ? END : edge.to;
|
|
318
|
+
stateGraph.addEdge(from, to);
|
|
319
|
+
}
|
|
320
|
+
// 添加条件路由边
|
|
321
|
+
if (dsl.conditionalEdges) {
|
|
322
|
+
for (const condEdge of dsl.conditionalEdges) {
|
|
323
|
+
this.addConditionalEdge(stateGraph, condEdge);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// 设置入口节点(仅当 edges 未声明 __start__ 时)
|
|
327
|
+
if (entryPoint && entryPoint !== '__start__') {
|
|
328
|
+
stateGraph.setEntryPoint(entryPoint);
|
|
329
|
+
}
|
|
330
|
+
// 设置终止节点
|
|
331
|
+
if (dsl.finishPoints) {
|
|
332
|
+
for (const fp of dsl.finishPoints) {
|
|
333
|
+
if (fp !== '__end__') {
|
|
334
|
+
stateGraph.setFinishPoint(fp);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 编译(若有 checkpointer 则注入,启用 Checkpoint 持久化)
|
|
339
|
+
const compileOptions = {};
|
|
340
|
+
if (this.checkpointer) {
|
|
341
|
+
compileOptions.checkpointer = this.checkpointer;
|
|
342
|
+
}
|
|
343
|
+
const compiled = stateGraph.compile(compileOptions);
|
|
344
|
+
// 构造 ExecutableGraph
|
|
345
|
+
const executableGraph = {
|
|
346
|
+
id: `graph-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
347
|
+
name: dsl.name ?? 'unnamed',
|
|
348
|
+
status: 'idle',
|
|
349
|
+
nodeIds: dsl.nodes.map((n) => n.id),
|
|
350
|
+
entryPoint: entryPoint ?? '__start__',
|
|
351
|
+
finishPoints: dsl.finishPoints ?? ['__end__'],
|
|
352
|
+
dslVersion: dsl.version ?? '0.0.0',
|
|
353
|
+
compiledAt: new Date().toISOString(),
|
|
354
|
+
_engineInstance: { compiled, dsl, silentNodeIds, subgraphNodeIds },
|
|
355
|
+
};
|
|
356
|
+
return executableGraph;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 流式驱动图执行,逐步 yield RuntimeEvent
|
|
360
|
+
*
|
|
361
|
+
* - LangGraph `streamMode: "updates"`:节点完成后产出 response.chunk / stats 等
|
|
362
|
+
* - LLM token / Tool 执行事件:经 streamSink 在节点执行中实时旁路入队
|
|
363
|
+
* (当前 @langchain/langgraph@0.2 无 messages mode,故用 LLMNode.onChunk 模拟)
|
|
364
|
+
*/
|
|
365
|
+
async *stream(graph, state) {
|
|
366
|
+
const ref = this.getCompiledRef(graph);
|
|
367
|
+
const input = this.graphStateToLangGraphInput(state);
|
|
368
|
+
const queue = [];
|
|
369
|
+
const waiter = { fn: null };
|
|
370
|
+
let finished = false;
|
|
371
|
+
let failure = null;
|
|
372
|
+
const signal = () => {
|
|
373
|
+
const fn = waiter.fn;
|
|
374
|
+
waiter.fn = null;
|
|
375
|
+
if (fn)
|
|
376
|
+
fn();
|
|
377
|
+
};
|
|
378
|
+
const enqueue = (event) => {
|
|
379
|
+
queue.push({
|
|
380
|
+
...event,
|
|
381
|
+
taskId: event.taskId ?? state.taskId,
|
|
382
|
+
correlationId: event.correlationId ?? state.correlationId,
|
|
383
|
+
});
|
|
384
|
+
signal();
|
|
385
|
+
};
|
|
386
|
+
this.streamSink = enqueue;
|
|
387
|
+
this.streamEventContext = { taskId: state.taskId, correlationId: state.correlationId };
|
|
388
|
+
this.streamChunkIndex = 0;
|
|
389
|
+
this.streamIteration = 0;
|
|
390
|
+
this.reasoningStartedThisTurn = false;
|
|
391
|
+
this.streamedTextThisTurn = false;
|
|
392
|
+
const producer = (async () => {
|
|
393
|
+
try {
|
|
394
|
+
const streamIterator = await ref.compiled.stream(input, {
|
|
395
|
+
configurable: {
|
|
396
|
+
thread_id: resolveCheckpointThreadId({
|
|
397
|
+
sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
|
|
398
|
+
taskId: state.taskId,
|
|
399
|
+
graphId: graph.id,
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
streamMode: 'updates',
|
|
403
|
+
});
|
|
404
|
+
for await (const update of streamIterator) {
|
|
405
|
+
for (const event of this.convertUpdatesToEvents(update, graph.id, () => this.streamChunkIndex++, ref.silentNodeIds, ref.subgraphNodeIds)) {
|
|
406
|
+
enqueue(event);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
failure = err;
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
finished = true;
|
|
415
|
+
this.streamSink = null;
|
|
416
|
+
this.streamEventContext = {};
|
|
417
|
+
signal();
|
|
418
|
+
}
|
|
419
|
+
})();
|
|
420
|
+
while (!finished || queue.length > 0) {
|
|
421
|
+
if (queue.length === 0) {
|
|
422
|
+
await new Promise((resolve) => {
|
|
423
|
+
waiter.fn = resolve;
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
yield queue.shift();
|
|
428
|
+
}
|
|
429
|
+
await producer;
|
|
430
|
+
if (failure)
|
|
431
|
+
throw failure;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* 同步调用图执行,返回完整结果
|
|
435
|
+
*/
|
|
436
|
+
async invoke(graph, state) {
|
|
437
|
+
const ref = this.getCompiledRef(graph);
|
|
438
|
+
const input = this.graphStateToLangGraphInput(state);
|
|
439
|
+
const startTime = Date.now();
|
|
440
|
+
const result = await ref.compiled.invoke(input, {
|
|
441
|
+
configurable: {
|
|
442
|
+
thread_id: resolveCheckpointThreadId({
|
|
443
|
+
sessionId: typeof state.sessionId === 'string' ? state.sessionId : undefined,
|
|
444
|
+
taskId: state.taskId,
|
|
445
|
+
graphId: graph.id,
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
const duration = Date.now() - startTime;
|
|
450
|
+
const messages = Array.isArray(result['messages']) ? result['messages'] : [];
|
|
451
|
+
const lastMessage = messages[messages.length - 1];
|
|
452
|
+
const output = typeof lastMessage === 'object' && lastMessage !== null && 'content' in lastMessage
|
|
453
|
+
? String(lastMessage.content)
|
|
454
|
+
: '';
|
|
455
|
+
// ── Token 累计:从 __llmTokenUsage 数组累加器读取每轮 LLM 明细 + 汇总
|
|
456
|
+
// 数组由 channel reducer 自动累加(每次 LLM 节点追加一条)。
|
|
457
|
+
const llmUsageArray = Array.isArray(result['__llmTokenUsage'])
|
|
458
|
+
? result['__llmTokenUsage']
|
|
459
|
+
: [];
|
|
460
|
+
const perNode = llmUsageArray.map((u) => ({
|
|
461
|
+
nodeId: u.nodeId ?? 'unknown',
|
|
462
|
+
model: u.model ?? 'unknown',
|
|
463
|
+
inputTokens: u.inputTokens ?? 0,
|
|
464
|
+
outputTokens: u.outputTokens ?? 0,
|
|
465
|
+
}));
|
|
466
|
+
const inputTokens = perNode.reduce((s, u) => s + u.inputTokens, 0);
|
|
467
|
+
const outputTokens = perNode.reduce((s, u) => s + u.outputTokens, 0);
|
|
468
|
+
return {
|
|
469
|
+
output,
|
|
470
|
+
finalState: {
|
|
471
|
+
...result,
|
|
472
|
+
...state,
|
|
473
|
+
messages: Array.isArray(result['messages'])
|
|
474
|
+
? result['messages']
|
|
475
|
+
: state.messages,
|
|
476
|
+
userId: state.userId,
|
|
477
|
+
loadedTools: state.loadedTools ?? result.loadedTools,
|
|
478
|
+
forceToolServer: state.forceToolServer ?? result.forceToolServer,
|
|
479
|
+
executionContext: state.executionContext ?? result.executionContext,
|
|
480
|
+
},
|
|
481
|
+
tokenUsage: {
|
|
482
|
+
input: inputTokens,
|
|
483
|
+
output: outputTokens,
|
|
484
|
+
total: inputTokens + outputTokens,
|
|
485
|
+
perNode,
|
|
486
|
+
},
|
|
487
|
+
duration,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 从运行中的图提取 Checkpoint
|
|
492
|
+
*
|
|
493
|
+
* 提取当前执行状态为可持久化的 CheckpointData。
|
|
494
|
+
*/
|
|
495
|
+
checkpoint(graph) {
|
|
496
|
+
const now = new Date().toISOString();
|
|
497
|
+
return {
|
|
498
|
+
checkpointId: `cp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
499
|
+
threadId: resolveCheckpointThreadId({ graphId: graph.id }),
|
|
500
|
+
currentNodeId: graph.entryPoint,
|
|
501
|
+
historyPath: [],
|
|
502
|
+
state: {
|
|
503
|
+
messages: [],
|
|
504
|
+
userId: '',
|
|
505
|
+
},
|
|
506
|
+
createdBy: 'system',
|
|
507
|
+
createdAt: now,
|
|
508
|
+
updatedBy: 'system',
|
|
509
|
+
updatedAt: now,
|
|
510
|
+
isDeleted: false,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* 从 CheckpointData 恢复可执行图
|
|
515
|
+
*
|
|
516
|
+
* HITL / Fork 续跑请走 `buildGraph(dsl)` + 同一 `thread_id` 的 state。
|
|
517
|
+
* 本方法无 DSL,无法编译出可用 `_engineInstance`。
|
|
518
|
+
*/
|
|
519
|
+
async restore(checkpointData) {
|
|
520
|
+
// 恢复场景下,需要从 checkpointData 中重建图
|
|
521
|
+
// 实际使用时,DSL 应从外部存储(如 Agent Registry)获取
|
|
522
|
+
// 此处构建一个占位的 ExecutableGraph
|
|
523
|
+
const restoredGraph = {
|
|
524
|
+
id: `graph-restored-${Date.now()}`,
|
|
525
|
+
name: `restored-${checkpointData.threadId}`,
|
|
526
|
+
status: 'paused',
|
|
527
|
+
nodeIds: [],
|
|
528
|
+
entryPoint: checkpointData.currentNodeId,
|
|
529
|
+
finishPoints: ['__end__'],
|
|
530
|
+
dslVersion: '0.0.0',
|
|
531
|
+
compiledAt: new Date().toISOString(),
|
|
532
|
+
_engineInstance: undefined,
|
|
533
|
+
};
|
|
534
|
+
return restoredGraph;
|
|
535
|
+
}
|
|
536
|
+
// ─────────────────────────────────────────────
|
|
537
|
+
// Private helpers
|
|
538
|
+
// ─────────────────────────────────────────────
|
|
539
|
+
/**
|
|
540
|
+
* 根据节点类型创建对应的执行函数
|
|
541
|
+
*/
|
|
542
|
+
createNodeFunction(node) {
|
|
543
|
+
switch (node.type) {
|
|
544
|
+
case 'llm':
|
|
545
|
+
return this.createLLMNodeFunction(node);
|
|
546
|
+
case 'tool':
|
|
547
|
+
return this.createToolNodeFunction(node);
|
|
548
|
+
case 'condition':
|
|
549
|
+
return async (state) => {
|
|
550
|
+
// 条件节点:根据 config 中的 conditionExpr 评估并写入 __conditionResult
|
|
551
|
+
const conditionExpr = node.config?.['conditionExpr']
|
|
552
|
+
?? node.config?.['conditionFn'];
|
|
553
|
+
if (conditionExpr) {
|
|
554
|
+
const branchValues = node.config?.['branchValues']
|
|
555
|
+
?? this.extractBranchValuesFromDsl(node.id);
|
|
556
|
+
const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, node.config?.['fallback'] ?? '__default__');
|
|
557
|
+
return { ...state, currentNode: node.id, __conditionResult: result };
|
|
558
|
+
}
|
|
559
|
+
// 无表达式时透传(由 addConditionalEdge 自行处理)
|
|
560
|
+
return { ...state, currentNode: node.id };
|
|
561
|
+
};
|
|
562
|
+
case 'subgraph':
|
|
563
|
+
return this.createSubGraphNodeFunction(node);
|
|
564
|
+
case 'human':
|
|
565
|
+
return async (state) => {
|
|
566
|
+
// HITL 节点:暂停等待人工介入(Task 14)
|
|
567
|
+
return { ...state, currentNode: node.id };
|
|
568
|
+
};
|
|
569
|
+
case 'parallel':
|
|
570
|
+
return this.createParallelNodeFunction(node);
|
|
571
|
+
case 'code':
|
|
572
|
+
if (node.config?.['bindSkill'] === true) {
|
|
573
|
+
return this.createSkillBindNodeFunction(node);
|
|
574
|
+
}
|
|
575
|
+
return this.createCodeNodeFunction(node);
|
|
576
|
+
default:
|
|
577
|
+
if (node.config?.['bindSkill'] === true) {
|
|
578
|
+
return this.createSkillBindNodeFunction(node);
|
|
579
|
+
}
|
|
580
|
+
return async (state) => {
|
|
581
|
+
return { ...state, currentNode: node.id };
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
createSkillBindNodeFunction(node) {
|
|
586
|
+
return async (state) => {
|
|
587
|
+
const patch = bindSelectedSkill(state);
|
|
588
|
+
if (patch.skillRoute === 'skill' && patch.activatedSkill) {
|
|
589
|
+
this.emitStreamEvent({
|
|
590
|
+
type: 'skill.matched',
|
|
591
|
+
taskId: undefined,
|
|
592
|
+
correlationId: undefined,
|
|
593
|
+
timestamp: new Date().toISOString(),
|
|
594
|
+
payload: {
|
|
595
|
+
skillName: patch.skillName || patch.activatedSkill,
|
|
596
|
+
skillId: patch.activatedSkill,
|
|
597
|
+
confidence: 1,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
else if (patch.unmatched) {
|
|
602
|
+
this.emitStreamEvent({
|
|
603
|
+
type: 'skill.unmatched',
|
|
604
|
+
taskId: undefined,
|
|
605
|
+
correlationId: undefined,
|
|
606
|
+
timestamp: new Date().toISOString(),
|
|
607
|
+
payload: {
|
|
608
|
+
userInput: '',
|
|
609
|
+
userIntent: 'none',
|
|
610
|
+
keywords: [],
|
|
611
|
+
reason: 'skill not in assembled catalog',
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (patch.skillRoute === 'clarify' && this.streamSink) {
|
|
616
|
+
const question = patch.messages.at(-1)?.content;
|
|
617
|
+
if (typeof question === 'string' && question.trim()) {
|
|
618
|
+
this.streamSink({
|
|
619
|
+
type: 'response.chunk',
|
|
620
|
+
taskId: undefined,
|
|
621
|
+
correlationId: undefined,
|
|
622
|
+
timestamp: new Date().toISOString(),
|
|
623
|
+
payload: { content: question, index: this.streamChunkIndex++, node: node.id },
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
currentNode: node.id,
|
|
629
|
+
skillRoute: patch.skillRoute,
|
|
630
|
+
activatedSkill: patch.activatedSkill,
|
|
631
|
+
activeSkillPrompt: patch.activeSkillPrompt,
|
|
632
|
+
skillInput: patch.skillInput,
|
|
633
|
+
messages: { __set: patch.messages },
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
buildSkillCatalog(defs) {
|
|
638
|
+
if (!defs)
|
|
639
|
+
return undefined;
|
|
640
|
+
const rows = Object.values(defs).map((skill) => {
|
|
641
|
+
const desc = (skill.description ?? '').replace(/\s+/g, ' ').trim();
|
|
642
|
+
return `- ${skill.skillId}${skill.name && skill.name !== skill.skillId ? `(${skill.name})` : ''}${desc ? `:${desc}` : ''}`;
|
|
643
|
+
});
|
|
644
|
+
if (rows.length === 0)
|
|
645
|
+
return undefined;
|
|
646
|
+
return `# 可用技能\n下列技能已装配,引用时使用 skillId 或 name。\n${rows.join('\n')}`;
|
|
647
|
+
}
|
|
648
|
+
createToolNodeFunction(node) {
|
|
649
|
+
const toolName = node.config?.['tool'] ?? node.id;
|
|
650
|
+
const defaultArgs = node.config?.['arguments'] ?? {};
|
|
651
|
+
if (this.toolOrchestrator) {
|
|
652
|
+
const toolNode = new ToolNode(this.toolOrchestrator, this.toolResultInjector, {
|
|
653
|
+
userId: 'system',
|
|
654
|
+
delegationToken: '',
|
|
655
|
+
agentCode: '',
|
|
656
|
+
}, (progress) => this.emitToolExecutionEvents(progress), this.hitlHandler, node.id);
|
|
657
|
+
return (state) => toolNode.execute(state);
|
|
658
|
+
}
|
|
659
|
+
if (this.toolExecutor) {
|
|
660
|
+
return async (state) => {
|
|
661
|
+
const userId = String(state['userId'] ?? 'system');
|
|
662
|
+
const delegationToken = String(state['delegationToken'] ?? '');
|
|
663
|
+
const agentCode = String(state['agentCode'] ?? state['agentId'] ?? userId);
|
|
664
|
+
const correlationId = String(state['correlationId'] ?? `tc-${node.id}`);
|
|
665
|
+
const taskId = String(state['taskId'] ?? state['correlationId'] ?? `tc-${node.id}`);
|
|
666
|
+
const toolCallId = `tc-${node.id}`;
|
|
667
|
+
const startedAt = Date.now();
|
|
668
|
+
this.emitToolExecutionEvents({
|
|
669
|
+
toolCallId,
|
|
670
|
+
toolName,
|
|
671
|
+
status: 'started',
|
|
672
|
+
arguments: defaultArgs,
|
|
673
|
+
node: node.id,
|
|
674
|
+
});
|
|
675
|
+
const result = await this.toolExecutor.execute({
|
|
676
|
+
toolCallId,
|
|
677
|
+
toolName,
|
|
678
|
+
arguments: defaultArgs,
|
|
679
|
+
authContext: { userId, delegationToken, agentCode },
|
|
680
|
+
correlationId,
|
|
681
|
+
taskId,
|
|
682
|
+
});
|
|
683
|
+
const executionTime = Date.now() - startedAt;
|
|
684
|
+
this.emitToolExecutionEvents({
|
|
685
|
+
toolCallId: result.toolCallId,
|
|
686
|
+
toolName: result.toolName,
|
|
687
|
+
status: result.success ? 'completed' : 'error',
|
|
688
|
+
content: result.content,
|
|
689
|
+
error: result.error,
|
|
690
|
+
executionTime,
|
|
691
|
+
node: node.id,
|
|
692
|
+
});
|
|
693
|
+
// 重要:messages channel 的 reducer 是 append 语义,只返回新增消息
|
|
694
|
+
return {
|
|
695
|
+
messages: [
|
|
696
|
+
{
|
|
697
|
+
role: 'tool',
|
|
698
|
+
content: result.content || JSON.stringify(result),
|
|
699
|
+
toolCallId,
|
|
700
|
+
toolName,
|
|
701
|
+
success: result.success ?? true,
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
currentNode: node.id,
|
|
705
|
+
};
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
// 无 toolOrchestrator / toolExecutor 时:兑底生成 skipped tool message
|
|
709
|
+
return async (_state) => {
|
|
710
|
+
return {
|
|
711
|
+
messages: [
|
|
712
|
+
{
|
|
713
|
+
role: 'tool',
|
|
714
|
+
content: JSON.stringify({ toolName, skipped: true }),
|
|
715
|
+
toolCallId: `tc-${node.id}`,
|
|
716
|
+
toolName,
|
|
717
|
+
success: false,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
currentNode: node.id,
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
createLLMNodeFunction(node) {
|
|
725
|
+
const rawPrompt = (typeof node.config?.['systemPromptFile'] === 'string' && node.config['systemPromptFile'])
|
|
726
|
+
|| (typeof node.config?.['systemPrompt'] === 'string' ? node.config['systemPrompt'] : undefined);
|
|
727
|
+
const baseConfig = {
|
|
728
|
+
model: node.config?.['model'] ?? 'default',
|
|
729
|
+
systemPrompt: typeof rawPrompt === 'string' ? rawPrompt : undefined,
|
|
730
|
+
temperature: node.config?.['temperature'],
|
|
731
|
+
maxTokens: node.config?.['maxTokens'],
|
|
732
|
+
tools: node.config?.['tools'],
|
|
733
|
+
toolChoice: node.config?.['toolChoice'],
|
|
734
|
+
};
|
|
735
|
+
const disableTools = node.config?.['disableTools'] === true;
|
|
736
|
+
const includeSkillCatalog = node.config?.['includeSkillCatalog'] === true;
|
|
737
|
+
const silent = node.config?.['silent'] === true;
|
|
738
|
+
return async (state) => {
|
|
739
|
+
// HITL 续跑:已有未闭合 toolCalls + hitlChoice 时不要再调 LLM(否则会换新 toolCallId)
|
|
740
|
+
const hitlChoice = state['hitlChoice'];
|
|
741
|
+
const rawMessages = Array.isArray(state['messages']) ? state['messages'] : [];
|
|
742
|
+
if (!disableTools &&
|
|
743
|
+
!silent &&
|
|
744
|
+
isHitlDecision(hitlChoice) &&
|
|
745
|
+
lastAssistantHasUnansweredToolCalls(rawMessages)) {
|
|
746
|
+
return { currentNode: node.id };
|
|
747
|
+
}
|
|
748
|
+
const promptRoots = Array.isArray(state['promptRoots']) ? state['promptRoots'] : [];
|
|
749
|
+
const resolvedPrompt = resolveGraphPrompt(baseConfig.systemPrompt, promptRoots);
|
|
750
|
+
const loadedTools = state['loadedTools'];
|
|
751
|
+
// 旧 Checkpoint 兼容:agentId 仅作读兜底
|
|
752
|
+
const agentCode = (state['agentCode'] ?? state['agentId']);
|
|
753
|
+
const activeSkillPrompt = state['activeSkillPrompt'];
|
|
754
|
+
const skillDefs = state['skillDefinitions'];
|
|
755
|
+
const activatedSkillId = state['activatedSkill'];
|
|
756
|
+
// [DEBUG] 打印 loadedTools 实际状态
|
|
757
|
+
if (process.env.EAP_LLM_DEBUG === '1') {
|
|
758
|
+
const toolNames = loadedTools ? Object.keys(loadedTools) : [];
|
|
759
|
+
console.log(`[LLM-DEBUG] node=${node.id} loadedTools=${toolNames.length}:`, toolNames);
|
|
760
|
+
}
|
|
761
|
+
const catalog = includeSkillCatalog ? this.buildSkillCatalog(skillDefs) : undefined;
|
|
762
|
+
const developerPrompt = [catalog, activeSkillPrompt, resolvedPrompt]
|
|
763
|
+
.filter((s) => Boolean(s && s.trim()))
|
|
764
|
+
.join('\n\n');
|
|
765
|
+
const execCtx = state['executionContext'];
|
|
766
|
+
const userId = (typeof execCtx?.userId === 'string' && execCtx.userId) || state['userId'];
|
|
767
|
+
// 1. 构建 system prompt(平台规则 + 技能提示 + 开发者提示词)
|
|
768
|
+
const builtPrompt = this.systemPromptBuilder.build({
|
|
769
|
+
developerPrompt: developerPrompt || undefined,
|
|
770
|
+
agentCode,
|
|
771
|
+
userId,
|
|
772
|
+
});
|
|
773
|
+
// 2. 工具走 API 的 tools 参数,不塞进 system prompt
|
|
774
|
+
const scopedTools = activatedSkillId && skillDefs
|
|
775
|
+
? filterLoadedToolsForSkill(loadedTools, skillDefs[activatedSkillId])
|
|
776
|
+
: loadedTools;
|
|
777
|
+
const apiTools = disableTools ? [] : loadedToolsToApiTools(scopedTools);
|
|
778
|
+
const finalConfig = {
|
|
779
|
+
...baseConfig,
|
|
780
|
+
systemPrompt: builtPrompt,
|
|
781
|
+
tools: disableTools ? undefined : (apiTools.length > 0 ? apiTools : baseConfig.tools),
|
|
782
|
+
toolChoice: disableTools ? 'none' : baseConfig.toolChoice,
|
|
783
|
+
};
|
|
784
|
+
// 流式旁路:有 streamSink 时启用 LLM token 推送;silent 节点不推给用户
|
|
785
|
+
const enableStream = Boolean(this.streamSink) && !silent;
|
|
786
|
+
this.reasoningStartedThisTurn = false;
|
|
787
|
+
this.streamedTextThisTurn = false;
|
|
788
|
+
const llmNode = new LLMNode(node.id, finalConfig, this.llmProvider, enableStream, enableStream
|
|
789
|
+
? (text) => {
|
|
790
|
+
if (!this.streamSink || !text)
|
|
791
|
+
return;
|
|
792
|
+
const timestamp = new Date().toISOString();
|
|
793
|
+
if (!this.reasoningStartedThisTurn) {
|
|
794
|
+
this.reasoningStartedThisTurn = true;
|
|
795
|
+
this.streamIteration += 1;
|
|
796
|
+
this.streamSink({
|
|
797
|
+
type: 'reasoning.started',
|
|
798
|
+
taskId: undefined,
|
|
799
|
+
correlationId: undefined,
|
|
800
|
+
timestamp,
|
|
801
|
+
payload: {
|
|
802
|
+
iterationCount: this.streamIteration,
|
|
803
|
+
model: finalConfig.model,
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
this.streamedTextThisTurn = true;
|
|
808
|
+
this.streamSink({
|
|
809
|
+
type: 'response.chunk',
|
|
810
|
+
taskId: undefined,
|
|
811
|
+
correlationId: undefined,
|
|
812
|
+
timestamp,
|
|
813
|
+
payload: {
|
|
814
|
+
content: text,
|
|
815
|
+
index: this.streamChunkIndex++,
|
|
816
|
+
node: node.id,
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
: undefined);
|
|
821
|
+
const next = await llmNode.execute(state);
|
|
822
|
+
if (!silent)
|
|
823
|
+
return next;
|
|
824
|
+
const added = Array.isArray(next.messages) ? next.messages[0] : undefined;
|
|
825
|
+
const raw = added && typeof added === 'object' && typeof added.content === 'string'
|
|
826
|
+
? added.content
|
|
827
|
+
: '';
|
|
828
|
+
return {
|
|
829
|
+
...next,
|
|
830
|
+
messages: [],
|
|
831
|
+
silentOutput: raw,
|
|
832
|
+
skillSelectRaw: raw,
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* 子图节点:优先 config.subgraphDsl;否则按 skillId 从 state.skillDefinitions.workflow 解析。
|
|
838
|
+
* 技能 prompt 由 SubGraphCompiler 写入 activeSkillPrompt,供入口 LLM 拼接。
|
|
839
|
+
*/
|
|
840
|
+
createSubGraphNodeFunction(node) {
|
|
841
|
+
const embeddedDsl = node.config?.['subgraphDsl'];
|
|
842
|
+
const configuredSkillId = node.subgraphId ??
|
|
843
|
+
node.config?.['skillId'] ??
|
|
844
|
+
embeddedDsl?.name;
|
|
845
|
+
if (embeddedDsl) {
|
|
846
|
+
return this.subGraphCompiler.compileSubGraph(embeddedDsl, configuredSkillId);
|
|
847
|
+
}
|
|
848
|
+
return async (state) => {
|
|
849
|
+
const skillId = configuredSkillId ??
|
|
850
|
+
(typeof node.config?.['skillCode'] === 'string' ? node.config['skillCode'] : undefined);
|
|
851
|
+
const defs = state['skillDefinitions'];
|
|
852
|
+
const def = skillId && defs ? defs[skillId] : undefined;
|
|
853
|
+
const workflow = def?.workflow;
|
|
854
|
+
if (!workflow?.nodes?.length) {
|
|
855
|
+
throw preloadSkillNotFound(`子图节点无法解析 Skill workflow: node=${node.id}, skillId=${skillId ?? '(none)'}`, { nodeId: node.id, skillId });
|
|
856
|
+
}
|
|
857
|
+
const fn = this.subGraphCompiler.compileSubGraph(workflow, skillId ?? def?.skillId);
|
|
858
|
+
return fn(state);
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* 并行节点:并行执行多个子任务分支
|
|
863
|
+
*
|
|
864
|
+
* 支持两种并行模式:
|
|
865
|
+
* 1. branches 模式:node.branches 列出要并行执行的节点 ID,每个节点的执行函数由 DSL 定义
|
|
866
|
+
* 2. tasks 模式:node.config.tasks 列出要并行执行的异步任务函数
|
|
867
|
+
*
|
|
868
|
+
* 执行结果合并到 state.__parallelResults 中。
|
|
869
|
+
*/
|
|
870
|
+
createParallelNodeFunction(node) {
|
|
871
|
+
return async (state) => {
|
|
872
|
+
const branchNodeIds = node.branches ?? node.config?.['branches'] ?? [];
|
|
873
|
+
const tasks = node.config?.['tasks'];
|
|
874
|
+
const results = [];
|
|
875
|
+
if (tasks?.length) {
|
|
876
|
+
// tasks 模式:并行执行配置的任务
|
|
877
|
+
const promises = tasks.map(async (task) => {
|
|
878
|
+
try {
|
|
879
|
+
// 任务结果由外部注入的 task executor 处理
|
|
880
|
+
// 此处仅记录任务 ID,实际执行依赖外部编排
|
|
881
|
+
return { branchId: task.id, success: true, result: { taskId: task.id, status: 'dispatched' } };
|
|
882
|
+
}
|
|
883
|
+
catch (err) {
|
|
884
|
+
return {
|
|
885
|
+
branchId: task.id,
|
|
886
|
+
success: false,
|
|
887
|
+
error: err instanceof Error ? err.message : String(err),
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
results.push(...await Promise.all(promises));
|
|
892
|
+
}
|
|
893
|
+
else if (branchNodeIds.length) {
|
|
894
|
+
// branches 模式:并行执行分支节点
|
|
895
|
+
const promises = branchNodeIds.map(async (branchId) => {
|
|
896
|
+
try {
|
|
897
|
+
// 每个分支节点的实际执行由 LangGraph 编排层驱动
|
|
898
|
+
// 并行节点仅负责触发并汇总状态
|
|
899
|
+
return { branchId, success: true, result: { dispatched: true } };
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
return {
|
|
903
|
+
branchId,
|
|
904
|
+
success: false,
|
|
905
|
+
error: err instanceof Error ? err.message : String(err),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
results.push(...await Promise.all(promises));
|
|
910
|
+
}
|
|
911
|
+
const allSuccess = results.every((r) => r.success);
|
|
912
|
+
return {
|
|
913
|
+
...state,
|
|
914
|
+
currentNode: node.id,
|
|
915
|
+
__parallelResults: results,
|
|
916
|
+
__parallelAllSuccess: allSuccess,
|
|
917
|
+
__conditionResult: allSuccess ? 'success' : 'partial_failure',
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* 代码执行节点:在受限沙箱中执行用户代码
|
|
923
|
+
*
|
|
924
|
+
* 支持两种模式:
|
|
925
|
+
* 1. inline:node.config.code 为内联 JavaScript/TypeScript 代码字符串
|
|
926
|
+
* 2. handler:node.config.handler 为外部模块路径 + 函数名
|
|
927
|
+
*
|
|
928
|
+
* 安全措施:
|
|
929
|
+
* - 禁止 require/import 动态加载
|
|
930
|
+
* - 执行超时控制(默认 5000ms)
|
|
931
|
+
* - 限制可访问的 state 字段
|
|
932
|
+
*/
|
|
933
|
+
createCodeNodeFunction(node) {
|
|
934
|
+
const code = node.config?.['code'];
|
|
935
|
+
const handler = node.config?.['handler'];
|
|
936
|
+
const timeoutMs = node.config?.['timeoutMs'] ?? 5000;
|
|
937
|
+
return async (state) => {
|
|
938
|
+
try {
|
|
939
|
+
let result;
|
|
940
|
+
if (code) {
|
|
941
|
+
// inline 模式:在受限沙箱中执行
|
|
942
|
+
result = await this.executeCodeInSandbox(code, state, timeoutMs);
|
|
943
|
+
}
|
|
944
|
+
else if (handler) {
|
|
945
|
+
// handler 模式:记录警告,待外部实现
|
|
946
|
+
result = { handler, status: 'skipped', reason: 'External handler execution not yet supported' };
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
result = { status: 'skipped', reason: 'No code or handler configured' };
|
|
950
|
+
}
|
|
951
|
+
const messages = [
|
|
952
|
+
{ role: 'tool', content: JSON.stringify({ nodeId: node.id, codeResult: result }) },
|
|
953
|
+
];
|
|
954
|
+
if (node.config?.['emitReply'] === true
|
|
955
|
+
&& result && typeof result === 'object'
|
|
956
|
+
&& typeof result.reply === 'string') {
|
|
957
|
+
const reply = result.reply.trim();
|
|
958
|
+
if (reply)
|
|
959
|
+
messages.push({ role: 'assistant', content: reply });
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
currentNode: node.id,
|
|
963
|
+
__codeResult: result,
|
|
964
|
+
messages,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
969
|
+
return {
|
|
970
|
+
currentNode: node.id,
|
|
971
|
+
__codeResult: { error },
|
|
972
|
+
__codeError: error,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* 受限沙箱执行代码
|
|
979
|
+
*
|
|
980
|
+
* 使用 Function 构造器 + 超时控制,禁止动态模块加载。
|
|
981
|
+
* 注意:这不是完整的安全沙箱,生产环境应使用 vm2/isolated-vm 或容器化方案。
|
|
982
|
+
*/
|
|
983
|
+
async executeCodeInSandbox(code, state, timeoutMs) {
|
|
984
|
+
// 安全检查:禁止动态加载
|
|
985
|
+
const forbidden = ['require(', 'import(', 'eval(', 'Function('];
|
|
986
|
+
for (const pattern of forbidden) {
|
|
987
|
+
if (code.includes(pattern)) {
|
|
988
|
+
throw new Error(`Code execution forbidden: ${pattern} is not allowed in sandbox`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return new Promise((resolve, reject) => {
|
|
992
|
+
const timer = setTimeout(() => reject(new Error(`Code execution timeout: ${timeoutMs}ms`)), timeoutMs);
|
|
993
|
+
try {
|
|
994
|
+
// 构造受限执行环境:仅暴露 state 和 console 的简化版本
|
|
995
|
+
const sandboxConsole = {
|
|
996
|
+
log: (...args) => { },
|
|
997
|
+
};
|
|
998
|
+
const fn = new Function('state', 'console', `
|
|
999
|
+
"use strict";
|
|
1000
|
+
${code}
|
|
1001
|
+
`);
|
|
1002
|
+
const result = fn(state, sandboxConsole);
|
|
1003
|
+
clearTimeout(timer);
|
|
1004
|
+
resolve(result instanceof Promise ? result : result);
|
|
1005
|
+
}
|
|
1006
|
+
catch (err) {
|
|
1007
|
+
clearTimeout(timer);
|
|
1008
|
+
reject(err);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* 从 DSL 中提取条件节点关联的分支值
|
|
1014
|
+
*/
|
|
1015
|
+
extractBranchValuesFromDsl(nodeId) {
|
|
1016
|
+
// 从 DSL 的 conditionalEdges 中查找以当前节点为 from 的分支值
|
|
1017
|
+
// 此方法在 buildGraph 时可通过缓存 DSL 优化,当前为运行时查找
|
|
1018
|
+
return [];
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* shouldContinue 的 call_tools 只能进 type:tool。
|
|
1022
|
+
* 禁止指向 subgraph / llm / code:那是路由信号,不是 Tool 执行。
|
|
1023
|
+
* 内嵌 subgraphDsl 同样校验。
|
|
1024
|
+
*/
|
|
1025
|
+
assertShouldContinueTargetsTool(dsl) {
|
|
1026
|
+
const graphName = dsl.name ?? 'unnamed';
|
|
1027
|
+
const byId = new Map(dsl.nodes.map((node) => [node.id, node]));
|
|
1028
|
+
for (const edge of dsl.conditionalEdges ?? []) {
|
|
1029
|
+
if (edge.conditionFn !== 'shouldContinue')
|
|
1030
|
+
continue;
|
|
1031
|
+
for (const branch of edge.branches) {
|
|
1032
|
+
if (branch.value !== 'call_tools')
|
|
1033
|
+
continue;
|
|
1034
|
+
if (branch.to === '__end__') {
|
|
1035
|
+
throw new AppError(ErrorCodes.GRAPH_BUILD_FAILED, `Graph "${graphName}": shouldContinue call_tools cannot target __end__; must be type:tool`, { exposeMessage: true });
|
|
1036
|
+
}
|
|
1037
|
+
const target = byId.get(branch.to);
|
|
1038
|
+
if (!target) {
|
|
1039
|
+
throw new AppError(ErrorCodes.GRAPH_BUILD_FAILED, `Graph "${graphName}": shouldContinue call_tools target "${branch.to}" is not a node`, { exposeMessage: true });
|
|
1040
|
+
}
|
|
1041
|
+
if (target.type !== 'tool') {
|
|
1042
|
+
throw new AppError(ErrorCodes.GRAPH_BUILD_FAILED, `Graph "${graphName}": shouldContinue call_tools must target type:tool, got type:${target.type} node "${branch.to}"`, { exposeMessage: true });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
for (const node of dsl.nodes) {
|
|
1047
|
+
const nested = node.config?.['subgraphDsl'];
|
|
1048
|
+
if (nested?.nodes?.length)
|
|
1049
|
+
this.assertShouldContinueTargetsTool(nested);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* 添加条件路由边到 StateGraph
|
|
1054
|
+
*
|
|
1055
|
+
* 使用 ConditionEvaluator 对 DSL 中的 conditionFn 表达式求值,
|
|
1056
|
+
* 支持 stateField 引用、简单表达式和自定义评估函数。
|
|
1057
|
+
*/
|
|
1058
|
+
addConditionalEdge(stateGraph, condEdge) {
|
|
1059
|
+
const from = condEdge.from === '__start__' ? START : condEdge.from;
|
|
1060
|
+
// 构建路由映射 { branchValue → targetNodeId }
|
|
1061
|
+
const mapping = {};
|
|
1062
|
+
for (const branch of condEdge.branches) {
|
|
1063
|
+
mapping[branch.value] = branch.to === '__end__' ? END : branch.to;
|
|
1064
|
+
}
|
|
1065
|
+
// 默认 fallback
|
|
1066
|
+
const fallbackKey = '__default__';
|
|
1067
|
+
if (condEdge.fallback) {
|
|
1068
|
+
mapping[fallbackKey] = condEdge.fallback === '__end__' ? END : condEdge.fallback;
|
|
1069
|
+
}
|
|
1070
|
+
const branchValues = condEdge.branches.map((b) => b.value);
|
|
1071
|
+
const conditionExpr = condEdge.conditionFn;
|
|
1072
|
+
// 条件函数:使用 ConditionEvaluator 真实求值
|
|
1073
|
+
const conditionFn = async (state) => {
|
|
1074
|
+
const result = await this.conditionEvaluator.evaluate(conditionExpr, state, branchValues, fallbackKey);
|
|
1075
|
+
return result;
|
|
1076
|
+
};
|
|
1077
|
+
stateGraph.addConditionalEdges(from, conditionFn, mapping);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* 获取编译后图的内部引用
|
|
1081
|
+
*/
|
|
1082
|
+
getCompiledRef(graph) {
|
|
1083
|
+
const ref = graph._engineInstance;
|
|
1084
|
+
if (!ref?.compiled) {
|
|
1085
|
+
throw new Error(`Graph "${graph.name}" (${graph.id}) has no compiled engine instance. ` +
|
|
1086
|
+
'Ensure buildGraph() was called or the graph was properly restored.');
|
|
1087
|
+
}
|
|
1088
|
+
return ref;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* 将 GraphState 转换为 LangGraph 输入格式。
|
|
1092
|
+
* 必须原样保留 toolCalls / toolCallId / reasoningContent 等字段,
|
|
1093
|
+
* 否则多轮回放时 DeepSeek thinking 会因缺少 reasoning_content 而 400,
|
|
1094
|
+
* tool 消息也会丢掉 tool_call_id。
|
|
1095
|
+
*/
|
|
1096
|
+
graphStateToLangGraphInput(state) {
|
|
1097
|
+
const mapped = (state.messages ?? []).map((msg) => ({
|
|
1098
|
+
role: msg.role,
|
|
1099
|
+
content: msg.content,
|
|
1100
|
+
...(msg.toolCalls?.length ? { toolCalls: msg.toolCalls } : {}),
|
|
1101
|
+
...(msg.toolCallId ? { toolCallId: msg.toolCallId } : {}),
|
|
1102
|
+
...(msg.toolName ? { toolName: msg.toolName } : {}),
|
|
1103
|
+
...(typeof msg.success === 'boolean' ? { success: msg.success } : {}),
|
|
1104
|
+
...(msg.reasoningContent ? { reasoningContent: msg.reasoningContent } : {}),
|
|
1105
|
+
...(msg.file ? { file: msg.file } : {}),
|
|
1106
|
+
}));
|
|
1107
|
+
return {
|
|
1108
|
+
...state,
|
|
1109
|
+
messages: state.replaceCheckpointMessages ? { __set: mapped } : mapped,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* 将 LangGraph updates 模式输出转为平台 RuntimeEvent 序列
|
|
1114
|
+
*
|
|
1115
|
+
* updates 形态:`{ [nodeId]: partialState }`;partialState.messages 仅为该节点新增消息。
|
|
1116
|
+
*/
|
|
1117
|
+
*convertUpdatesToEvents(update, graphId, nextChunkIndex, silentNodeIds = new Set(), subgraphNodeIds = new Set()) {
|
|
1118
|
+
const timestamp = new Date().toISOString();
|
|
1119
|
+
const entries = this.normalizeStreamUpdates(update);
|
|
1120
|
+
for (const { nodeId, data } of entries) {
|
|
1121
|
+
// LLM 节点 token 明细(写入 __llmTokenUsage)→ stats.node_token_usage
|
|
1122
|
+
const usages = data['__llmTokenUsage'];
|
|
1123
|
+
if (Array.isArray(usages)) {
|
|
1124
|
+
for (const raw of usages) {
|
|
1125
|
+
if (!raw || typeof raw !== 'object')
|
|
1126
|
+
continue;
|
|
1127
|
+
const u = raw;
|
|
1128
|
+
yield {
|
|
1129
|
+
type: 'stats.node_token_usage',
|
|
1130
|
+
taskId: undefined,
|
|
1131
|
+
correlationId: undefined,
|
|
1132
|
+
timestamp,
|
|
1133
|
+
payload: {
|
|
1134
|
+
nodeId: String(u['nodeId'] ?? nodeId),
|
|
1135
|
+
model: String(u['model'] ?? 'unknown'),
|
|
1136
|
+
inputTokens: Number(u['inputTokens'] ?? 0),
|
|
1137
|
+
outputTokens: Number(u['outputTokens'] ?? 0),
|
|
1138
|
+
costEstimate: Number(u['costEstimate'] ?? 0),
|
|
1139
|
+
durationMs: typeof u['durationMs'] === 'number' ? u['durationMs'] : undefined,
|
|
1140
|
+
graphId,
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const messages = this.extractMessagesFromPartial(data);
|
|
1146
|
+
for (const msg of messages) {
|
|
1147
|
+
if (msg.role === 'assistant' && Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0) {
|
|
1148
|
+
// tool.called 由 Tool 节点在真正执行时发出,此处不再把 LLM function-call 当执行开始
|
|
1149
|
+
// 子图节点的 updates 是内层已 stream 过的回写,不要再发空 reasoning chunk
|
|
1150
|
+
if (subgraphNodeIds.has(nodeId) || silentNodeIds.has(nodeId)) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
const hasText = typeof msg.content === 'string' && msg.content.trim().length > 0;
|
|
1154
|
+
if ((hasText || msg.reasoningContent) &&
|
|
1155
|
+
(!this.streamedTextThisTurn || msg.reasoningContent)) {
|
|
1156
|
+
if (this.streamedTextThisTurn && !msg.reasoningContent)
|
|
1157
|
+
continue;
|
|
1158
|
+
yield {
|
|
1159
|
+
type: 'response.chunk',
|
|
1160
|
+
taskId: undefined,
|
|
1161
|
+
correlationId: undefined,
|
|
1162
|
+
timestamp,
|
|
1163
|
+
payload: {
|
|
1164
|
+
content: this.streamedTextThisTurn ? '' : (msg.content ?? ''),
|
|
1165
|
+
index: nextChunkIndex(),
|
|
1166
|
+
node: nodeId,
|
|
1167
|
+
graphId,
|
|
1168
|
+
...(msg.reasoningContent
|
|
1169
|
+
? { reasoningContent: msg.reasoningContent }
|
|
1170
|
+
: {}),
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.length > 0) {
|
|
1177
|
+
if (silentNodeIds.has(nodeId) || subgraphNodeIds.has(nodeId))
|
|
1178
|
+
continue;
|
|
1179
|
+
// 已通过 onChunk 推送过 token,避免整段重复
|
|
1180
|
+
if (this.streamedTextThisTurn) {
|
|
1181
|
+
// token 已流过:仍补发一条带 reasoning 的标记 chunk(空 content),供落库回传
|
|
1182
|
+
if (msg.reasoningContent) {
|
|
1183
|
+
yield {
|
|
1184
|
+
type: 'response.chunk',
|
|
1185
|
+
taskId: undefined,
|
|
1186
|
+
correlationId: undefined,
|
|
1187
|
+
timestamp,
|
|
1188
|
+
payload: {
|
|
1189
|
+
content: '',
|
|
1190
|
+
index: nextChunkIndex(),
|
|
1191
|
+
node: nodeId,
|
|
1192
|
+
graphId,
|
|
1193
|
+
reasoningContent: msg.reasoningContent,
|
|
1194
|
+
},
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
yield {
|
|
1200
|
+
type: 'response.chunk',
|
|
1201
|
+
taskId: undefined,
|
|
1202
|
+
correlationId: undefined,
|
|
1203
|
+
timestamp,
|
|
1204
|
+
payload: {
|
|
1205
|
+
content: msg.content,
|
|
1206
|
+
index: nextChunkIndex(),
|
|
1207
|
+
node: nodeId,
|
|
1208
|
+
graphId,
|
|
1209
|
+
...(msg.reasoningContent
|
|
1210
|
+
? { reasoningContent: msg.reasoningContent }
|
|
1211
|
+
: {}),
|
|
1212
|
+
},
|
|
1213
|
+
};
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (msg.role === 'tool') {
|
|
1217
|
+
// tool.result / tool.error 由 Tool 节点旁路发出,此处只补 file.created
|
|
1218
|
+
const raw = typeof msg.content === 'string' ? msg.content : '';
|
|
1219
|
+
const file = msg.file ?? fileArtifactFromToolOutput(raw);
|
|
1220
|
+
if (file) {
|
|
1221
|
+
yield {
|
|
1222
|
+
type: 'file.created',
|
|
1223
|
+
taskId: undefined,
|
|
1224
|
+
correlationId: undefined,
|
|
1225
|
+
timestamp,
|
|
1226
|
+
payload: {
|
|
1227
|
+
fileName: file.fileName,
|
|
1228
|
+
fileType: file.fileType,
|
|
1229
|
+
size: file.size,
|
|
1230
|
+
url: file.url,
|
|
1231
|
+
toolCallId: msg.toolCallId,
|
|
1232
|
+
toolName: msg.toolName ?? 'tool',
|
|
1233
|
+
},
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/** 规范化 updates / values 两种 stream 输出 */
|
|
1241
|
+
normalizeStreamUpdates(update) {
|
|
1242
|
+
if (!update || typeof update !== 'object')
|
|
1243
|
+
return [];
|
|
1244
|
+
const obj = update;
|
|
1245
|
+
// updates 模式:{ nodeId: partialState }
|
|
1246
|
+
// values 模式兜底:整表 state(无明确 node 键时当作 __values__)
|
|
1247
|
+
const keys = Object.keys(obj);
|
|
1248
|
+
if (keys.length === 0)
|
|
1249
|
+
return [];
|
|
1250
|
+
// 若顶层已有 messages,视为 values 全量快照(仅取末条,避免重复刷屏)
|
|
1251
|
+
if (Array.isArray(obj['messages'])) {
|
|
1252
|
+
const messages = obj['messages'];
|
|
1253
|
+
const last = messages.length > 0 ? messages[messages.length - 1] : undefined;
|
|
1254
|
+
return [
|
|
1255
|
+
{
|
|
1256
|
+
nodeId: String(obj['currentNode'] ?? '__values__'),
|
|
1257
|
+
data: { messages: last !== undefined ? [last] : [] },
|
|
1258
|
+
},
|
|
1259
|
+
];
|
|
1260
|
+
}
|
|
1261
|
+
const out = [];
|
|
1262
|
+
for (const [nodeId, value] of Object.entries(obj)) {
|
|
1263
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1264
|
+
out.push({ nodeId, data: value });
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
extractMessagesFromPartial(data) {
|
|
1270
|
+
const raw = data['messages'];
|
|
1271
|
+
if (!Array.isArray(raw))
|
|
1272
|
+
return [];
|
|
1273
|
+
return raw.map((item) => {
|
|
1274
|
+
if (!item || typeof item !== 'object') {
|
|
1275
|
+
return { role: 'assistant', content: String(item) };
|
|
1276
|
+
}
|
|
1277
|
+
const m = item;
|
|
1278
|
+
return {
|
|
1279
|
+
role: typeof m['role'] === 'string' ? m['role'] : undefined,
|
|
1280
|
+
content: typeof m['content'] === 'string' ? m['content'] : undefined,
|
|
1281
|
+
toolCalls: m['toolCalls'],
|
|
1282
|
+
toolCallId: typeof m['toolCallId'] === 'string' ? m['toolCallId'] : undefined,
|
|
1283
|
+
toolName: typeof m['toolName'] === 'string' ? m['toolName'] : undefined,
|
|
1284
|
+
success: typeof m['success'] === 'boolean' ? m['success'] : undefined,
|
|
1285
|
+
reasoningContent: typeof m['reasoningContent'] === 'string' ? m['reasoningContent'] : undefined,
|
|
1286
|
+
file: readFileArtifact(m['file']),
|
|
1287
|
+
};
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function readFileArtifact(value) {
|
|
1292
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1293
|
+
return undefined;
|
|
1294
|
+
const rec = value;
|
|
1295
|
+
if (typeof rec.fileName !== 'string' || typeof rec.url !== 'string')
|
|
1296
|
+
return undefined;
|
|
1297
|
+
return {
|
|
1298
|
+
fileName: rec.fileName,
|
|
1299
|
+
fileType: typeof rec.fileType === 'string' ? rec.fileType : 'file',
|
|
1300
|
+
size: typeof rec.size === 'number' && Number.isFinite(rec.size) ? rec.size : 0,
|
|
1301
|
+
url: rec.url,
|
|
1302
|
+
};
|
|
1303
|
+
}
|