@gt-fe/eap-runtime 0.0.8 → 0.0.10
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 +12 -17
- package/dist/api/handlers/invoke.handler.js +3 -3
- package/dist/api/handlers/messages.handler.js +0 -1
- package/dist/api/handlers/stream.handler.js +3 -3
- package/dist/api/routes/agent.routes.js +2 -2
- package/dist/checkpoint/checkpoint-ref-notify.d.ts +10 -0
- package/dist/checkpoint/checkpoint-ref-notify.js +59 -0
- package/dist/checkpoint/index.d.ts +1 -0
- package/dist/checkpoint/index.js +1 -0
- package/dist/checkpoint/pg-checkpoint-saver.js +25 -2
- package/dist/common/agent-code.d.ts +2 -4
- package/dist/common/agent-code.js +4 -5
- package/dist/contracts/trace-collector.d.ts +15 -1
- package/dist/debug/llm-turn-log.d.ts +100 -0
- package/dist/debug/llm-turn-log.js +130 -0
- package/dist/debug/pg-llm-turn-log.d.ts +22 -0
- package/dist/debug/pg-llm-turn-log.js +57 -0
- package/dist/graph/adapters/langgraph-checkpointer.js +3 -1
- package/dist/graph/adapters/langgraph.adapter.d.ts +1 -0
- package/dist/graph/adapters/langgraph.adapter.js +147 -91
- package/dist/graph/adapters/skill-read.d.ts +12 -0
- package/dist/graph/adapters/skill-read.js +63 -0
- package/dist/graph/adapters/subgraph-compiler.js +5 -0
- package/dist/graph/contracts/executable-graph.d.ts +3 -3
- package/dist/graph/hitl/hitl-handler.d.ts +7 -0
- package/dist/graph/hitl/hitl-handler.js +47 -6
- package/dist/graph/nodes/llm-node.d.ts +14 -0
- package/dist/graph/nodes/skill-bind-node.js +1 -2
- package/dist/graph/nodes/tool-node.d.ts +2 -0
- package/dist/graph/nodes/tool-node.js +24 -9
- package/dist/graph/trace-span.d.ts +22 -0
- package/dist/graph/trace-span.js +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/memory/compression/compression.repository.d.ts +1 -15
- package/dist/providers/echo-llm-provider.d.ts +1 -1
- package/dist/providers/echo-llm-provider.js +2 -1
- package/dist/providers/http-llm-provider.d.ts +4 -0
- package/dist/providers/http-llm-provider.js +129 -4
- package/dist/providers/local-llm-provider.d.ts +5 -1
- package/dist/providers/local-llm-provider.js +14 -6
- package/dist/providers/routing-llm-provider.d.ts +4 -0
- package/dist/providers/routing-llm-provider.js +12 -2
- package/dist/registry/agent-models.d.ts +1 -1
- package/dist/registry/agent-models.js +1 -1
- package/dist/registry/agent-registry-client.d.ts +2 -2
- package/dist/registry/deployment-snapshot.d.ts +4 -4
- package/dist/registry/deployment-snapshot.js +50 -28
- package/dist/registry/graph-loader.d.ts +1 -1
- package/dist/registry/graph-loader.js +39 -76
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/skill-tree.d.ts +3 -0
- package/dist/registry/skill-tree.js +41 -3
- package/dist/runtime/agent-runtime.d.ts +42 -3
- package/dist/runtime/agent-runtime.js +110 -34
- package/dist/runtime/dev-runtime.d.ts +6 -3
- package/dist/runtime/dev-runtime.js +21 -14
- package/dist/runtime/index.d.ts +3 -3
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/storage-profile.d.ts +19 -10
- package/dist/runtime/storage-profile.js +42 -29
- package/dist/runtime/types.d.ts +15 -17
- package/dist/server.js +1 -1
- package/dist/session/session.service.d.ts +5 -0
- package/dist/session/session.service.js +12 -0
- package/dist/sse/events/index.d.ts +5 -3
- package/dist/sse/events/index.js +2 -0
- package/dist/sse/events/skill-events.d.ts +11 -0
- package/dist/sse/trace-collector.d.ts +10 -0
- package/dist/sse/trace-collector.js +1 -0
- package/dist/token/token-budget.service.d.ts +2 -2
- package/dist/token/token-budget.service.js +1 -1
- package/dist/tool/create-tool-executor.js +3 -2
- package/dist/tool/harness-primitives.d.ts +32 -0
- package/dist/tool/harness-primitives.js +385 -0
- package/dist/tool/http-tool-executor.js +1 -1
- package/dist/tool/index.d.ts +2 -1
- package/dist/tool/index.js +2 -1
- package/dist/tool/local-tool-executor.d.ts +3 -3
- package/dist/tool/local-tool-executor.js +5 -7
- package/dist/tool/mcp-tool-executor.js +9 -1
- package/dist/tool/system-prompt-builder.d.ts +61 -25
- package/dist/tool/system-prompt-builder.js +195 -92
- package/dist/tool/tool-execution-router.js +1 -1
- package/dist/tool/tool-executor.interface.d.ts +12 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Enterprise Agent Platform **执行引擎**(LangGraph.js):Agent Graph 编
|
|
|
8
8
|
|
|
9
9
|
| 日期 | 摘要 |
|
|
10
10
|
|------|------|
|
|
11
|
+
| 2026-08-24 | 0.0.10:官方 Skill / Harness、`ForkParams`、对话详情、存储两档。依赖 `@eap/contracts@0.0.9`。详见 [CHANGELOG.md](./CHANGELOG.md) |
|
|
12
|
+
| 2026-08-21 | 0.0.9:Gateway `/mcp` JSON-RPC 补 `Accept`;`tools/call` 透传工具版本。详见 [CHANGELOG.md](./CHANGELOG.md) |
|
|
11
13
|
| 2026-08-19 | 0.0.8:Skill 子图 SSE 闭环;`ExecuteParams.traceId`;`tool.called` 改为 Tool 节点开始执行。依赖 `@eap/contracts@0.0.8`。详见 [CHANGELOG.md](./CHANGELOG.md) |
|
|
12
14
|
| 2026-08-18 | 0.0.7:`DevHttpExecutor`、`mode=mcp` 缺配置失败、HTTP 出站语义与 Gateway 共用 `@eap/contracts@0.0.7` |
|
|
13
15
|
| 2026-07-29 | 补全 `.env.example` 全量环境变量(部署上下文 / Tool / 预装 / 存储装配 / 调试分区);§5 预装开关表补 `EAP_PRELOAD_DEFINITION_CACHE` |
|
|
@@ -21,6 +23,7 @@ Enterprise Agent Platform **执行引擎**(LangGraph.js):Agent Graph 编
|
|
|
21
23
|
| 权威文档 | 路径 |
|
|
22
24
|
|----------|------|
|
|
23
25
|
| 模块规格 | [`.kiro/specs/04-runtime-core/`](../.kiro/specs/04-runtime-core/) |
|
|
26
|
+
| Runtime DDL | [`docs/ddl.md`](./docs/ddl.md) |
|
|
24
27
|
| SDK 集成手册 | [`docs/contracts/runtime-sdk-integration-final.md`](../docs/contracts/runtime-sdk-integration-final.md) |
|
|
25
28
|
| 环境变量总表 | [`docs/governance/governance-env-config.md`](../docs/governance/governance-env-config.md) |
|
|
26
29
|
| 预装联调契约 | [`docs/contracts/preload-hosting-runtime-contract.md`](../docs/contracts/preload-hosting-runtime-contract.md) |
|
|
@@ -82,7 +85,7 @@ for await (const ev of agentRuntime.stream({ agentCode, input, sessionId, taskId
|
|
|
82
85
|
await agentRuntime.seedSessionMessages({ agentCode, messages: [ /* role: user | assistant */ ] });
|
|
83
86
|
await agentRuntime.resumeHitl(taskId, { choice, userId, /* … */ });
|
|
84
87
|
for await (const ev of agentRuntime.streamResumeHitl(taskId, { choice, userId })) { /* HITL 续跑事件 */ }
|
|
85
|
-
await agentRuntime.fork(checkpointId, { /*
|
|
88
|
+
await agentRuntime.fork({ checkpointId, taskId, traceId, overrides: { /* … */ } });
|
|
86
89
|
```
|
|
87
90
|
|
|
88
91
|
细则与字段表见 [`docs/contracts/runtime-sdk-integration-final.md`](../docs/contracts/runtime-sdk-integration-final.md)。
|
|
@@ -175,26 +178,18 @@ node ../fixtures/eap-preinstall-sample/scripts/seal-snapshot.mjs
|
|
|
175
178
|
|
|
176
179
|
## 6. 持久化与存储 Profile
|
|
177
180
|
|
|
178
|
-
|
|
181
|
+
`assembleRuntimeStorage()` 按 profile 与环境变量选择介质。表结构见 [`docs/ddl.md`](./docs/ddl.md)。
|
|
179
182
|
|
|
180
|
-
|
|
|
181
|
-
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `integration` | Redis + PG | (Hosting Session) | PG | Hosting 内嵌联调 |
|
|
185
|
-
| `production` | Redis + PG | (Hosting Session) | PG | 多副本生产 |
|
|
183
|
+
| 条件 | Checkpoint | Message / LLM 回合 | Token 配额 |
|
|
184
|
+
|------|------------|-------------------|------------|
|
|
185
|
+
| `EAP_RUNTIME_STORAGE_PROFILE=dev`,或未设 profile 且未配置数据库与 Redis | 文件(默认 `.eap/checkpoints`) | 文件 | 进程内,不落库 |
|
|
186
|
+
| `test` / `integration` / `production`,或未设 profile 但已配置 `DATABASE_URL`/`DB_*` 或 `REDIS_*` | Redis + PostgreSQL | PostgreSQL | PostgreSQL `token_budgets` |
|
|
186
187
|
|
|
187
|
-
|
|
188
|
+
后一路径缺少数据库或 Redis 时抛错。显式 `dev` 时即使已配置数据库仍使用文件。
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|------|------|
|
|
191
|
-
| `EAP_CHECKPOINT_DIR` | File Checkpoint |
|
|
192
|
-
| `EAP_SESSION_DIR` | File Session |
|
|
193
|
-
| `EAP_MESSAGE_DIR` | File Message |
|
|
194
|
-
|
|
195
|
-
完整策略与库表约定:[`docs/governance/governance-env-config.md` §9](../docs/governance/governance-env-config.md#9-runtime-存储装配策略)、[`docs/architecture/session-message-checkpoint-alignment.md`](../docs/architecture/session-message-checkpoint-alignment.md)、[`docs/verification/db-storage-verification.md`](../docs/verification/db-storage-verification.md)。
|
|
190
|
+
仅文件路径使用的目录变量:`EAP_CHECKPOINT_DIR`、`EAP_SESSION_DIR`、`EAP_MESSAGE_DIR`(可用 `EAP_DATA_DIR` 作为根目录)。
|
|
196
191
|
|
|
197
|
-
|
|
192
|
+
环境变量总表:[governance-env-config.md §9](../docs/governance/governance-env-config.md#9-runtime-存储装配策略)。
|
|
198
193
|
|
|
199
194
|
---
|
|
200
195
|
|
|
@@ -21,7 +21,8 @@ import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
|
|
|
21
21
|
import { resolveExecutionContext } from '../../governance/governance-client.js';
|
|
22
22
|
import { resolveSkillInputFromInvokeInput } from '../../registry/skill-io-guard.js';
|
|
23
23
|
import { toLoadedToolMap } from '../../tool/loaded-tool-definition.js';
|
|
24
|
-
import { toLoadedSkillMap
|
|
24
|
+
import { toLoadedSkillMap } from '../../tool/system-prompt-builder.js';
|
|
25
|
+
import { assembleLoadedTools } from '../../tool/harness-primitives.js';
|
|
25
26
|
import { resolveAgentCode } from '../../common/agent-code.js';
|
|
26
27
|
import { unwrapHitlPaused } from '../../graph/hitl/hitl-handler.js';
|
|
27
28
|
import { resolveTraceId } from '../../common/trace-id.js';
|
|
@@ -173,8 +174,7 @@ export class InvokeOrchestrator {
|
|
|
173
174
|
})
|
|
174
175
|
: undefined;
|
|
175
176
|
const skillDefinitions = toLoadedSkillMap(loadedSkills);
|
|
176
|
-
|
|
177
|
-
const loadedTools = mergeSkillScriptTools(loadedToolsBase, loadedSkills);
|
|
177
|
+
const loadedTools = assembleLoadedTools(loadedToolsBase);
|
|
178
178
|
const promptRoots = this.graphLoader
|
|
179
179
|
? await this.graphLoader.resolvePromptRoots(agentCode, req.agentVersion)
|
|
180
180
|
: [];
|
|
@@ -47,7 +47,6 @@ export class MessagesOrchestrator {
|
|
|
47
47
|
const agentVersion = req.agentVersion ?? metaVersion;
|
|
48
48
|
// ─── Step 2: 委托 InvokeOrchestrator(user/assistant Message 由 invoke 主路径写回)───
|
|
49
49
|
const invokeReq = {
|
|
50
|
-
agentId: session.agentId,
|
|
51
50
|
agentCode: session.agentId,
|
|
52
51
|
input,
|
|
53
52
|
sessionId,
|
|
@@ -28,7 +28,8 @@ import { resolveSkillInputFromInvokeInput } from '../../registry/skill-io-guard.
|
|
|
28
28
|
import { ErrorCodes } from '@gt-fe/eap-contracts';
|
|
29
29
|
import { classifyRuntimeError } from '../../utils/http-error.js';
|
|
30
30
|
import { toLoadedToolMap } from '../../tool/loaded-tool-definition.js';
|
|
31
|
-
import { toLoadedSkillMap
|
|
31
|
+
import { toLoadedSkillMap } from '../../tool/system-prompt-builder.js';
|
|
32
|
+
import { assembleLoadedTools } from '../../tool/harness-primitives.js';
|
|
32
33
|
import { resolveAgentCode } from '../../common/agent-code.js';
|
|
33
34
|
import { unwrapHitlPaused } from './invoke.handler.js';
|
|
34
35
|
import { buildHitlWaitedPayload } from '../../graph/hitl/hitl-handler.js';
|
|
@@ -195,8 +196,7 @@ export class StreamOrchestrator {
|
|
|
195
196
|
? await this.graphLoader.loadSkillDefinitions(agentCode, { ...loadOpts, userId })
|
|
196
197
|
: undefined;
|
|
197
198
|
const skillDefinitions = toLoadedSkillMap(loadedSkills);
|
|
198
|
-
|
|
199
|
-
const loadedTools = mergeSkillScriptTools(loadedToolsBase, loadedSkills);
|
|
199
|
+
const loadedTools = assembleLoadedTools(loadedToolsBase);
|
|
200
200
|
const promptRoots = this.graphLoader
|
|
201
201
|
? await this.graphLoader.resolvePromptRoots(agentCode, req.agentVersion)
|
|
202
202
|
: [];
|
|
@@ -21,7 +21,7 @@ export function createAgentRouteHandlers(deps) {
|
|
|
21
21
|
*/
|
|
22
22
|
async function invokeHandler(req) {
|
|
23
23
|
const { input, sessionId, config, userId } = req;
|
|
24
|
-
const agentCode = req.agentCode
|
|
24
|
+
const agentCode = req.agentCode;
|
|
25
25
|
if (!agentCode) {
|
|
26
26
|
throw new Error('agentCode is required');
|
|
27
27
|
}
|
|
@@ -109,7 +109,7 @@ export function createAgentRouteHandlers(deps) {
|
|
|
109
109
|
*/
|
|
110
110
|
async function streamHandler(req, res) {
|
|
111
111
|
const { input, sessionId, config, userId } = req;
|
|
112
|
-
const agentCode = req.agentCode
|
|
112
|
+
const agentCode = req.agentCode;
|
|
113
113
|
if (!agentCode) {
|
|
114
114
|
res.write(`event: error\ndata: ${JSON.stringify({ error: 'agentCode is required' })}\n\n`);
|
|
115
115
|
res.end();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint 落盘后通知上层写 checkpoint_refs。
|
|
3
|
+
* 无 traceId 或 spanId 则跳过;回调失败不抛。
|
|
4
|
+
* collector 不得写入持久化正文。
|
|
5
|
+
*/
|
|
6
|
+
import type { ITraceCollector } from '../contracts/trace-collector.js';
|
|
7
|
+
import type { CheckpointData } from '../graph/contracts/executable-graph.js';
|
|
8
|
+
/** 去掉不可序列化的 collector,再交给 Saver。 */
|
|
9
|
+
export declare function stripCheckpointCollectors(data: CheckpointData): CheckpointData;
|
|
10
|
+
export declare function notifyCheckpointRef(data: CheckpointData, collectorOverride?: ITraceCollector): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint 落盘后通知上层写 checkpoint_refs。
|
|
3
|
+
* 无 traceId 或 spanId 则跳过;回调失败不抛。
|
|
4
|
+
* collector 不得写入持久化正文。
|
|
5
|
+
*/
|
|
6
|
+
function pickString(...values) {
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
if (typeof value === 'string' && value.trim())
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function omitTraceCollector(record) {
|
|
14
|
+
const { traceCollector: _drop, ...rest } = record;
|
|
15
|
+
return rest;
|
|
16
|
+
}
|
|
17
|
+
/** 去掉不可序列化的 collector,再交给 Saver。 */
|
|
18
|
+
export function stripCheckpointCollectors(data) {
|
|
19
|
+
const state = omitTraceCollector({ ...data.state });
|
|
20
|
+
const snap = data.engineSnapshot ? { ...data.engineSnapshot } : undefined;
|
|
21
|
+
if (snap?.channel_values && typeof snap.channel_values === 'object') {
|
|
22
|
+
snap.channel_values = omitTraceCollector({
|
|
23
|
+
...snap.channel_values,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
...data,
|
|
28
|
+
state: state,
|
|
29
|
+
...(snap ? { engineSnapshot: snap } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function notifyCheckpointRef(data, collectorOverride) {
|
|
33
|
+
const channels = data.engineSnapshot && typeof data.engineSnapshot.channel_values === 'object'
|
|
34
|
+
? data.engineSnapshot.channel_values
|
|
35
|
+
: {};
|
|
36
|
+
const state = (data.state ?? {});
|
|
37
|
+
const collector = (collectorOverride ??
|
|
38
|
+
channels['traceCollector'] ??
|
|
39
|
+
state['traceCollector']);
|
|
40
|
+
const traceId = pickString(state['traceId'], channels['traceId']);
|
|
41
|
+
const spanId = pickString(state['lastSpanId'], channels['lastSpanId']);
|
|
42
|
+
if (!collector?.recordCheckpointRef || !traceId || !spanId)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
await collector.recordCheckpointRef({
|
|
46
|
+
checkpointId: data.checkpointId,
|
|
47
|
+
traceId,
|
|
48
|
+
spanId,
|
|
49
|
+
summary: {
|
|
50
|
+
nodeId: data.currentNodeId || pickString(channels['currentNode']),
|
|
51
|
+
threadId: data.threadId,
|
|
52
|
+
step: Array.isArray(data.historyPath) ? data.historyPath.length : undefined,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// 采集失败不阻断 Graph / HITL
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -3,6 +3,7 @@ export { RedisCheckpointSaver } from './redis-checkpoint-saver.js';
|
|
|
3
3
|
export type { RedisCheckpointSaverOptions } from './redis-checkpoint-saver.js';
|
|
4
4
|
export { CompositeCheckpointSaver } from './composite-checkpoint-saver.js';
|
|
5
5
|
export { restoreCheckpoint } from './restore-checkpoint.js';
|
|
6
|
+
export { notifyCheckpointRef, stripCheckpointCollectors } from './checkpoint-ref-notify.js';
|
|
6
7
|
export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, } from './checkpoint-serializer.js';
|
|
7
8
|
export type { CreateCheckpointParams } from './checkpoint-serializer.js';
|
|
8
9
|
export { FileCheckpointSaver } from './file-checkpoint-saver.js';
|
package/dist/checkpoint/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './contracts/index.js';
|
|
|
3
3
|
export { RedisCheckpointSaver } from './redis-checkpoint-saver.js';
|
|
4
4
|
export { CompositeCheckpointSaver } from './composite-checkpoint-saver.js';
|
|
5
5
|
export { restoreCheckpoint } from './restore-checkpoint.js';
|
|
6
|
+
export { notifyCheckpointRef, stripCheckpointCollectors } from './checkpoint-ref-notify.js';
|
|
6
7
|
export { createCheckpointData, serializeCheckpoint, deserializeCheckpoint, } from './checkpoint-serializer.js';
|
|
7
8
|
export { FileCheckpointSaver } from './file-checkpoint-saver.js';
|
|
8
9
|
export { InMemoryCheckpointSaver } from './in-memory-checkpoint-saver.js';
|
|
@@ -20,12 +20,16 @@ export class PostgresCheckpointSaver {
|
|
|
20
20
|
*/
|
|
21
21
|
async save(threadId, data) {
|
|
22
22
|
const sql = `
|
|
23
|
-
INSERT INTO checkpoints (
|
|
24
|
-
|
|
23
|
+
INSERT INTO checkpoints (
|
|
24
|
+
id, thread_id, checkpoint_id, current_node_id, history_path, state, engine_snapshot,
|
|
25
|
+
created_by, created_at, updated_by, updated_at
|
|
26
|
+
)
|
|
27
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
25
28
|
ON CONFLICT (thread_id, checkpoint_id) DO UPDATE SET
|
|
26
29
|
current_node_id = EXCLUDED.current_node_id,
|
|
27
30
|
history_path = EXCLUDED.history_path,
|
|
28
31
|
state = EXCLUDED.state,
|
|
32
|
+
engine_snapshot = EXCLUDED.engine_snapshot,
|
|
29
33
|
updated_by = EXCLUDED.updated_by,
|
|
30
34
|
updated_at = EXCLUDED.updated_at
|
|
31
35
|
`;
|
|
@@ -36,6 +40,7 @@ export class PostgresCheckpointSaver {
|
|
|
36
40
|
data.currentNodeId,
|
|
37
41
|
JSON.stringify(data.historyPath),
|
|
38
42
|
JSON.stringify(data.state),
|
|
43
|
+
data.engineSnapshot == null ? null : JSON.stringify(data.engineSnapshot),
|
|
39
44
|
data.createdBy || 'system',
|
|
40
45
|
data.createdAt,
|
|
41
46
|
data.updatedBy || 'system',
|
|
@@ -105,6 +110,7 @@ export class PostgresCheckpointSaver {
|
|
|
105
110
|
currentNodeId: source.currentNodeId,
|
|
106
111
|
historyPath: source.historyPath,
|
|
107
112
|
state: overrides ? { ...source.state, ...overrides } : source.state,
|
|
113
|
+
engineSnapshot: source.engineSnapshot,
|
|
108
114
|
createdBy: 'fork',
|
|
109
115
|
createdAt: now,
|
|
110
116
|
updatedBy: 'fork',
|
|
@@ -128,6 +134,7 @@ export class PostgresCheckpointSaver {
|
|
|
128
134
|
currentNodeId: row.current_node_id,
|
|
129
135
|
historyPath: typeof row.history_path === 'string' ? JSON.parse(row.history_path) : row.history_path,
|
|
130
136
|
state: typeof row.state === 'string' ? JSON.parse(row.state) : row.state,
|
|
137
|
+
engineSnapshot: parseJsonObject(row.engine_snapshot),
|
|
131
138
|
createdBy: row.created_by,
|
|
132
139
|
createdAt: row.created_at.toISOString(),
|
|
133
140
|
updatedBy: row.updated_by,
|
|
@@ -138,3 +145,19 @@ export class PostgresCheckpointSaver {
|
|
|
138
145
|
};
|
|
139
146
|
}
|
|
140
147
|
}
|
|
148
|
+
function parseJsonObject(value) {
|
|
149
|
+
if (value == null || value === '')
|
|
150
|
+
return undefined;
|
|
151
|
+
if (typeof value === 'object')
|
|
152
|
+
return value;
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(value);
|
|
155
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent
|
|
3
|
-
*
|
|
4
|
-
* 正式字段为 agentCode;历史 agentId 仅作同义兜底。
|
|
2
|
+
* Agent 对外主键。只认 agentCode。
|
|
3
|
+
* SessionRecord.agentId 是存储列名(值为 agentCode),不是本函数的入参。
|
|
5
4
|
*/
|
|
6
5
|
export declare function resolveAgentCode(input: {
|
|
7
6
|
agentCode?: string | null;
|
|
8
|
-
agentId?: string | null;
|
|
9
7
|
fallback?: string;
|
|
10
8
|
}): string;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent
|
|
3
|
-
*
|
|
4
|
-
* 正式字段为 agentCode;历史 agentId 仅作同义兜底。
|
|
2
|
+
* Agent 对外主键。只认 agentCode。
|
|
3
|
+
* SessionRecord.agentId 是存储列名(值为 agentCode),不是本函数的入参。
|
|
5
4
|
*/
|
|
6
5
|
export function resolveAgentCode(input) {
|
|
7
|
-
const code = input.agentCode ?? input.
|
|
6
|
+
const code = input.agentCode ?? input.fallback;
|
|
8
7
|
if (code == null || code === '') {
|
|
9
|
-
throw new Error('agentCode is required
|
|
8
|
+
throw new Error('agentCode is required');
|
|
10
9
|
}
|
|
11
10
|
return String(code);
|
|
12
11
|
}
|
|
@@ -82,7 +82,7 @@ export interface ExecutionNodeEvent {
|
|
|
82
82
|
/** 节点名称 */
|
|
83
83
|
name: string;
|
|
84
84
|
/** 节点类型 */
|
|
85
|
-
type: 'start' | 'llm' | 'tool' | 'skill' | 'condition' | 'end';
|
|
85
|
+
type: 'start' | 'llm' | 'tool' | 'skill' | 'condition' | 'hitl' | 'end';
|
|
86
86
|
/** 工具运行环境(type=tool) */
|
|
87
87
|
toolRuntime?: 'local' | 'server' | 'both' | 'mock';
|
|
88
88
|
/** Agent 对外主键 */
|
|
@@ -184,6 +184,20 @@ export interface ITraceCollector {
|
|
|
184
184
|
outputTokens?: number;
|
|
185
185
|
totalTokens?: number;
|
|
186
186
|
}): Promise<void>;
|
|
187
|
+
/**
|
|
188
|
+
* Checkpoint 已落盘后的引用回调。Hosting 适配 TraceClient.recordCheckpointRef。
|
|
189
|
+
* Runtime 仅在同时有 traceId 与 spanId 时调用;不传 graph state 正文。
|
|
190
|
+
*/
|
|
191
|
+
recordCheckpointRef?(input: {
|
|
192
|
+
checkpointId: string;
|
|
193
|
+
traceId: string;
|
|
194
|
+
spanId: string;
|
|
195
|
+
summary?: {
|
|
196
|
+
nodeId?: string;
|
|
197
|
+
threadId?: string;
|
|
198
|
+
step?: number;
|
|
199
|
+
};
|
|
200
|
+
}): Promise<void>;
|
|
187
201
|
/**
|
|
188
202
|
* 创建链路
|
|
189
203
|
*
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 LLM 回合落盘:每次模型调用写一份 JSON(入参、响应、URL、脱敏密钥、system 段)。
|
|
3
|
+
* 默认 `.eap/llm-turns/{sessionId}/`。EAP_LLM_TURN_LOG=0 关闭。
|
|
4
|
+
*/
|
|
5
|
+
export interface LlmTurnContext {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
taskId?: string;
|
|
8
|
+
nodeId?: string;
|
|
9
|
+
agentCode?: string;
|
|
10
|
+
correlationId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LlmTurnRecord extends LlmTurnContext {
|
|
13
|
+
id: string;
|
|
14
|
+
ts: string;
|
|
15
|
+
url: string;
|
|
16
|
+
model: string;
|
|
17
|
+
temperature?: number;
|
|
18
|
+
maxTokens?: number;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
systemPrompt?: string;
|
|
21
|
+
systemSections?: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
chars: number;
|
|
25
|
+
body?: string;
|
|
26
|
+
}>;
|
|
27
|
+
tools?: string[];
|
|
28
|
+
messages: Array<{
|
|
29
|
+
role: string;
|
|
30
|
+
content: string;
|
|
31
|
+
toolCallId?: string;
|
|
32
|
+
toolCalls?: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
response: {
|
|
35
|
+
content: string;
|
|
36
|
+
toolCalls?: unknown;
|
|
37
|
+
reasoningContent?: string;
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
finishReason?: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
};
|
|
43
|
+
latencyMs: number;
|
|
44
|
+
}
|
|
45
|
+
export interface LlmTurnIndexEntry {
|
|
46
|
+
id: string;
|
|
47
|
+
ts: string;
|
|
48
|
+
sessionId?: string;
|
|
49
|
+
taskId?: string;
|
|
50
|
+
nodeId?: string;
|
|
51
|
+
model: string;
|
|
52
|
+
url: string;
|
|
53
|
+
file: string;
|
|
54
|
+
inputTokens: number;
|
|
55
|
+
outputTokens: number;
|
|
56
|
+
}
|
|
57
|
+
export type LlmTurnLogKind = 'memory' | 'file' | 'postgres';
|
|
58
|
+
export interface ILlmTurnLog {
|
|
59
|
+
readonly kind: LlmTurnLogKind;
|
|
60
|
+
record(turn: Omit<LlmTurnRecord, 'id' | 'ts'> & {
|
|
61
|
+
id?: string;
|
|
62
|
+
ts?: string;
|
|
63
|
+
}): Promise<LlmTurnRecord>;
|
|
64
|
+
list(sessionId: string): Promise<LlmTurnIndexEntry[]>;
|
|
65
|
+
read(sessionId: string, turnId: string): Promise<LlmTurnRecord | null>;
|
|
66
|
+
listSessions(): Promise<string[]>;
|
|
67
|
+
readonly baseDir: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function finalizeLlmTurn(turn: Omit<LlmTurnRecord, 'id' | 'ts'> & {
|
|
70
|
+
id?: string;
|
|
71
|
+
ts?: string;
|
|
72
|
+
}): LlmTurnRecord;
|
|
73
|
+
export declare function toLlmTurnIndexEntry(record: LlmTurnRecord, file?: string): LlmTurnIndexEntry;
|
|
74
|
+
export declare function maskApiKey(key: string | undefined): string;
|
|
75
|
+
export declare function resolveLlmTurnDir(env?: NodeJS.ProcessEnv, cwd?: string): string;
|
|
76
|
+
export declare function createFileLlmTurnLogFromEnv(env?: NodeJS.ProcessEnv): FileLlmTurnLog | undefined;
|
|
77
|
+
export declare class InMemoryLlmTurnLog implements ILlmTurnLog {
|
|
78
|
+
readonly kind: "memory";
|
|
79
|
+
readonly baseDir = "memory://llm-turns";
|
|
80
|
+
private readonly bySession;
|
|
81
|
+
record(turn: Omit<LlmTurnRecord, 'id' | 'ts'> & {
|
|
82
|
+
id?: string;
|
|
83
|
+
ts?: string;
|
|
84
|
+
}): Promise<LlmTurnRecord>;
|
|
85
|
+
list(sessionId: string): Promise<LlmTurnIndexEntry[]>;
|
|
86
|
+
read(sessionId: string, turnId: string): Promise<LlmTurnRecord | null>;
|
|
87
|
+
listSessions(): Promise<string[]>;
|
|
88
|
+
}
|
|
89
|
+
export declare class FileLlmTurnLog implements ILlmTurnLog {
|
|
90
|
+
readonly baseDir: string;
|
|
91
|
+
readonly kind: "file";
|
|
92
|
+
constructor(baseDir: string);
|
|
93
|
+
record(turn: Omit<LlmTurnRecord, 'id' | 'ts'> & {
|
|
94
|
+
id?: string;
|
|
95
|
+
ts?: string;
|
|
96
|
+
}): Promise<LlmTurnRecord>;
|
|
97
|
+
list(sessionId: string): Promise<LlmTurnIndexEntry[]>;
|
|
98
|
+
read(sessionId: string, turnId: string): Promise<LlmTurnRecord | null>;
|
|
99
|
+
listSessions(): Promise<string[]>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 LLM 回合落盘:每次模型调用写一份 JSON(入参、响应、URL、脱敏密钥、system 段)。
|
|
3
|
+
* 默认 `.eap/llm-turns/{sessionId}/`。EAP_LLM_TURN_LOG=0 关闭。
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readdir, readFile, appendFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
export function finalizeLlmTurn(turn) {
|
|
8
|
+
const ts = turn.ts ?? new Date().toISOString();
|
|
9
|
+
const id = turn.id ?? `${ts.replace(/[:.]/g, '-')}-${turn.nodeId ?? 'llm'}`;
|
|
10
|
+
return { ...turn, id, ts };
|
|
11
|
+
}
|
|
12
|
+
export function toLlmTurnIndexEntry(record, file) {
|
|
13
|
+
return {
|
|
14
|
+
id: record.id,
|
|
15
|
+
ts: record.ts,
|
|
16
|
+
sessionId: record.sessionId,
|
|
17
|
+
taskId: record.taskId,
|
|
18
|
+
nodeId: record.nodeId,
|
|
19
|
+
model: record.model,
|
|
20
|
+
url: record.url,
|
|
21
|
+
file: file ?? `${sanitize(record.id)}.json`,
|
|
22
|
+
inputTokens: record.response.inputTokens,
|
|
23
|
+
outputTokens: record.response.outputTokens,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function maskApiKey(key) {
|
|
27
|
+
if (!key)
|
|
28
|
+
return '';
|
|
29
|
+
if (key.length <= 6)
|
|
30
|
+
return '***';
|
|
31
|
+
// 例:sk-proj-… → sk-proj***;sk-abc123xyz → sk-abc1***
|
|
32
|
+
return `${key.slice(0, 7)}***`;
|
|
33
|
+
}
|
|
34
|
+
export function resolveLlmTurnDir(env = process.env, cwd = process.cwd()) {
|
|
35
|
+
return env.EAP_LLM_TURN_DIR?.trim() || path.join(cwd, '.eap', 'llm-turns');
|
|
36
|
+
}
|
|
37
|
+
export function createFileLlmTurnLogFromEnv(env = process.env) {
|
|
38
|
+
if (env.EAP_LLM_TURN_LOG === '0')
|
|
39
|
+
return undefined;
|
|
40
|
+
return new FileLlmTurnLog(resolveLlmTurnDir(env));
|
|
41
|
+
}
|
|
42
|
+
export class InMemoryLlmTurnLog {
|
|
43
|
+
kind = 'memory';
|
|
44
|
+
baseDir = 'memory://llm-turns';
|
|
45
|
+
bySession = new Map();
|
|
46
|
+
async record(turn) {
|
|
47
|
+
const record = finalizeLlmTurn(turn);
|
|
48
|
+
const sessionId = record.sessionId ?? 'no-session';
|
|
49
|
+
let bucket = this.bySession.get(sessionId);
|
|
50
|
+
if (!bucket) {
|
|
51
|
+
bucket = { records: new Map(), index: [] };
|
|
52
|
+
this.bySession.set(sessionId, bucket);
|
|
53
|
+
}
|
|
54
|
+
bucket.records.set(record.id, record);
|
|
55
|
+
bucket.index.push(toLlmTurnIndexEntry(record));
|
|
56
|
+
return record;
|
|
57
|
+
}
|
|
58
|
+
async list(sessionId) {
|
|
59
|
+
return [...(this.bySession.get(sessionId)?.index ?? [])];
|
|
60
|
+
}
|
|
61
|
+
async read(sessionId, turnId) {
|
|
62
|
+
return this.bySession.get(sessionId)?.records.get(turnId) ?? null;
|
|
63
|
+
}
|
|
64
|
+
async listSessions() {
|
|
65
|
+
return [...this.bySession.keys()].sort();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export class FileLlmTurnLog {
|
|
69
|
+
baseDir;
|
|
70
|
+
kind = 'file';
|
|
71
|
+
constructor(baseDir) {
|
|
72
|
+
this.baseDir = baseDir;
|
|
73
|
+
}
|
|
74
|
+
async record(turn) {
|
|
75
|
+
const record = finalizeLlmTurn(turn);
|
|
76
|
+
const sessionId = sanitize(record.sessionId ?? 'no-session');
|
|
77
|
+
const dir = path.join(this.baseDir, sessionId);
|
|
78
|
+
await mkdir(dir, { recursive: true });
|
|
79
|
+
const file = `${sanitize(record.id)}.json`;
|
|
80
|
+
const filePath = path.join(dir, file);
|
|
81
|
+
await writeFile(filePath, JSON.stringify(record, null, 2), 'utf8');
|
|
82
|
+
await appendFile(path.join(dir, 'index.jsonl'), `${JSON.stringify(toLlmTurnIndexEntry(record, file))}\n`, 'utf8');
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
async list(sessionId) {
|
|
86
|
+
const file = path.join(this.baseDir, sanitize(sessionId), 'index.jsonl');
|
|
87
|
+
try {
|
|
88
|
+
const raw = await readFile(file, 'utf8');
|
|
89
|
+
return raw
|
|
90
|
+
.split('\n')
|
|
91
|
+
.filter((line) => line.trim())
|
|
92
|
+
.map((line) => JSON.parse(line));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async read(sessionId, turnId) {
|
|
99
|
+
const dir = path.join(this.baseDir, sanitize(sessionId));
|
|
100
|
+
const direct = path.join(dir, `${sanitize(turnId)}.json`);
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(await readFile(direct, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// fall through
|
|
106
|
+
}
|
|
107
|
+
const entries = await this.list(sessionId);
|
|
108
|
+
const hit = entries.find((e) => e.id === turnId || e.file === turnId);
|
|
109
|
+
if (!hit)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(await readFile(path.join(dir, hit.file), 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async listSessions() {
|
|
119
|
+
try {
|
|
120
|
+
const names = await readdir(this.baseDir);
|
|
121
|
+
return names.sort();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function sanitize(value) {
|
|
129
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 180);
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 回合 Postgres 存储。与 FileLlmTurnLog / InMemoryLlmTurnLog 共用 ILlmTurnLog。
|
|
3
|
+
*
|
|
4
|
+
* 表结构见 runtime/docs/ddl.md(`llm_turns`)。启动时仍可 CREATE IF NOT EXISTS。
|
|
5
|
+
*/
|
|
6
|
+
import type { IPgPool } from '../session/pg-message.repository.js';
|
|
7
|
+
import { type ILlmTurnLog, type LlmTurnIndexEntry, type LlmTurnRecord } from './llm-turn-log.js';
|
|
8
|
+
export declare class PostgresLlmTurnLog implements ILlmTurnLog {
|
|
9
|
+
private readonly pool;
|
|
10
|
+
readonly kind: "postgres";
|
|
11
|
+
readonly baseDir = "postgres://llm_turns";
|
|
12
|
+
private ready;
|
|
13
|
+
constructor(pool: IPgPool);
|
|
14
|
+
private ensureTable;
|
|
15
|
+
record(turn: Omit<LlmTurnRecord, 'id' | 'ts'> & {
|
|
16
|
+
id?: string;
|
|
17
|
+
ts?: string;
|
|
18
|
+
}): Promise<LlmTurnRecord>;
|
|
19
|
+
list(sessionId: string): Promise<LlmTurnIndexEntry[]>;
|
|
20
|
+
read(sessionId: string, turnId: string): Promise<LlmTurnRecord | null>;
|
|
21
|
+
listSessions(): Promise<string[]>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 回合 Postgres 存储。与 FileLlmTurnLog / InMemoryLlmTurnLog 共用 ILlmTurnLog。
|
|
3
|
+
*
|
|
4
|
+
* 表结构见 runtime/docs/ddl.md(`llm_turns`)。启动时仍可 CREATE IF NOT EXISTS。
|
|
5
|
+
*/
|
|
6
|
+
import { finalizeLlmTurn, toLlmTurnIndexEntry, } from './llm-turn-log.js';
|
|
7
|
+
const CREATE_TABLE_SQL = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS llm_turns (
|
|
9
|
+
id VARCHAR(180) PRIMARY KEY,
|
|
10
|
+
session_id VARCHAR(64) NOT NULL,
|
|
11
|
+
ts TIMESTAMPTZ NOT NULL,
|
|
12
|
+
payload JSONB NOT NULL
|
|
13
|
+
)`;
|
|
14
|
+
const CREATE_INDEX_SQL = 'CREATE INDEX IF NOT EXISTS idx_llm_turns_session_ts ON llm_turns (session_id, ts)';
|
|
15
|
+
export class PostgresLlmTurnLog {
|
|
16
|
+
pool;
|
|
17
|
+
kind = 'postgres';
|
|
18
|
+
baseDir = 'postgres://llm_turns';
|
|
19
|
+
ready;
|
|
20
|
+
constructor(pool) {
|
|
21
|
+
this.pool = pool;
|
|
22
|
+
}
|
|
23
|
+
ensureTable() {
|
|
24
|
+
this.ready ??= this.pool
|
|
25
|
+
.query(CREATE_TABLE_SQL)
|
|
26
|
+
.then(() => this.pool.query(CREATE_INDEX_SQL))
|
|
27
|
+
.then(() => undefined);
|
|
28
|
+
return this.ready;
|
|
29
|
+
}
|
|
30
|
+
async record(turn) {
|
|
31
|
+
await this.ensureTable();
|
|
32
|
+
const record = finalizeLlmTurn(turn);
|
|
33
|
+
await this.pool.query(`INSERT INTO llm_turns (id, session_id, ts, payload)
|
|
34
|
+
VALUES ($1, $2, $3, $4)
|
|
35
|
+
ON CONFLICT (id) DO UPDATE SET payload = EXCLUDED.payload, ts = EXCLUDED.ts, session_id = EXCLUDED.session_id`, [record.id, record.sessionId ?? 'no-session', record.ts, JSON.stringify(record)]);
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
38
|
+
async list(sessionId) {
|
|
39
|
+
await this.ensureTable();
|
|
40
|
+
const { rows } = await this.pool.query(`SELECT payload FROM llm_turns WHERE session_id = $1 ORDER BY ts ASC`, [sessionId]);
|
|
41
|
+
return rows.map((row) => toLlmTurnIndexEntry(parsePayload(row.payload)));
|
|
42
|
+
}
|
|
43
|
+
async read(sessionId, turnId) {
|
|
44
|
+
await this.ensureTable();
|
|
45
|
+
const { rows } = await this.pool.query(`SELECT payload FROM llm_turns WHERE session_id = $1 AND id = $2`, [sessionId, turnId]);
|
|
46
|
+
const row = rows[0];
|
|
47
|
+
return row ? parsePayload(row.payload) : null;
|
|
48
|
+
}
|
|
49
|
+
async listSessions() {
|
|
50
|
+
await this.ensureTable();
|
|
51
|
+
const { rows } = await this.pool.query(`SELECT DISTINCT session_id FROM llm_turns ORDER BY session_id`);
|
|
52
|
+
return rows.map((row) => row.session_id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parsePayload(payload) {
|
|
56
|
+
return typeof payload === 'string' ? JSON.parse(payload) : payload;
|
|
57
|
+
}
|