@gt-fe/eap-runtime 0.0.24 → 0.0.25

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 CHANGED
@@ -8,6 +8,7 @@ Enterprise Agent Platform **执行引擎**(LangGraph.js):Agent Graph 编
8
8
 
9
9
  | 日期 | 摘要 |
10
10
  |------|------|
11
+ | 2026-09-14 | 会话滑动窗口压缩默认开;`EAP_RUNTIME_COMPRESSION=0` 关闭 |
11
12
  | 2026-09-08 | [`docs/TODO.md`](./docs/TODO.md) 改为主路径状态(已落地 / 不在本包),不是排期 |
12
13
  | 2026-08-29 | 待办收口:[`docs/TODO.md`](./docs/TODO.md)。此后压缩已挂主路径(默认关);窗口展示不在 Runtime;Gateway 须 `tools/list` |
13
14
  | 2026-08-24 | 0.0.10:官方 Skill / Harness、`ForkParams`、对话详情、存储两档。依赖 `@eap/contracts@0.0.9`。详见 [CHANGELOG.md](./CHANGELOG.md) |
@@ -15,7 +15,7 @@
15
15
  * @see requirements.md Requirement 11 AC1-AC2
16
16
  */
17
17
  import { randomUUID } from 'node:crypto';
18
- import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, sessionVisibleGraphMessages, } from '../../session/persist-turn-messages.js';
18
+ import { buildHitlWaitingTaskOutput, loadModelFacingHistory, messageRecordsToGraphMessages, persistGraphMessageBaseline, turnGraphMessagesAfterCompression, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, sessionVisibleGraphMessages, } from '../../session/persist-turn-messages.js';
19
19
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
20
20
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
21
21
  import { recordTaskTokenUsages, tokenUsagesFromLlmChannel, } from '../../token/record-task-token-usage.js';
@@ -33,7 +33,7 @@ import { assembleLoadedTools } from '../../tool/harness-primitives.js';
33
33
  import { resolveAgentCode } from '../../common/agent-code.js';
34
34
  import { unwrapHitlPaused } from '../../graph/hitl/hitl-handler.js';
35
35
  import { resolveTraceId } from '../../common/trace-id.js';
36
- import { applySessionCompression, withCompressionSummaryModel, } from '../../session/apply-session-compression.js';
36
+ import { runSessionCompression, withCompressionSummaryModel, } from '../../session/apply-session-compression.js';
37
37
  import { applySessionMemory, } from '../../session/apply-session-memory.js';
38
38
  // ─────────────────────────────────────────────
39
39
  // Default Graph DSL fallback
@@ -180,6 +180,7 @@ export class InvokeOrchestrator {
180
180
  // ─── Step 5: 构建/执行 Graph ───
181
181
  let graphResult;
182
182
  let finishPoints;
183
+ let persistBaseline = 0;
183
184
  try {
184
185
  // 构建图 — 每次请求创建独立 StateGraph 实例,确保请求间无共享可变状态
185
186
  const graph = await this.graphEngine.buildGraph(graphDsl);
@@ -202,32 +203,45 @@ export class InvokeOrchestrator {
202
203
  ? (await this.graphLoader.resolveAgentDir(agentCode, req.agentVersion)) ?? undefined
203
204
  : undefined;
204
205
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.graphLoader, agentCode, req.agentVersion);
205
- // 组装 GraphState:有 Checkpoint 历史则只追加本轮 user,避免双重 append
206
+ // 模型上下文:Checkpoint 已有 messages 则压它;压成功用 __set 写回最新 Checkpoint。Message 库不改。
207
+ const facing = await loadModelFacingHistory({
208
+ sessionHistory: messageRecordsToGraphMessages(sessionContext.messages),
209
+ checkpointSaver: this.checkpointSaver,
210
+ threadId: sessionId,
211
+ });
212
+ const compression = await runSessionCompression({
213
+ history: facing.messages,
214
+ sessionId,
215
+ userId: effectiveUserId,
216
+ taskId: task.id,
217
+ options: await withCompressionSummaryModel(this.sessionCompression, () => this.graphLoader
218
+ ? this.graphLoader.resolveDefaultModel(agentCode, req.agentVersion)
219
+ : Promise.resolve(undefined)),
220
+ });
206
221
  const history = await applySessionMemory({
207
- history: await applySessionCompression({
208
- history: messageRecordsToGraphMessages(sessionContext.messages),
209
- sessionId,
210
- userId: effectiveUserId,
211
- taskId: task.id,
212
- options: await withCompressionSummaryModel(this.sessionCompression, () => this.graphLoader
213
- ? this.graphLoader.resolveDefaultModel(agentCode, req.agentVersion)
214
- : Promise.resolve(undefined)),
215
- }),
222
+ history: compression.messages,
216
223
  userId: effectiveUserId,
217
224
  agentCode,
218
225
  query: typeof req.input === 'string' ? req.input : undefined,
219
226
  options: this.sessionMemory,
220
227
  });
221
- const messages = await buildTurnGraphMessages({
228
+ const { messages, replaceCheckpointMessages } = turnGraphMessagesAfterCompression({
222
229
  history,
223
230
  input: req.input,
224
- checkpointSaver: this.checkpointSaver,
225
- threadId: sessionId,
231
+ fromCheckpoint: facing.fromCheckpoint,
232
+ compressed: compression.compressed,
233
+ });
234
+ persistBaseline = persistGraphMessageBaseline({
235
+ fromCheckpoint: facing.fromCheckpoint,
236
+ replaceCheckpointMessages,
237
+ graphInputCount: messages.length,
238
+ modelFacingCount: facing.messages.length,
226
239
  });
227
240
  const skillInput = resolveSkillInputFromInvokeInput(req.input);
228
241
  // 同步执行图
229
242
  graphResult = await runWithExecutionTrace({ abortSignal }, () => this.graphEngine.invoke(graph, {
230
243
  messages,
244
+ replaceCheckpointMessages,
231
245
  userId: effectiveUserId,
232
246
  delegationToken: ctx.delegationToken,
233
247
  agentCode,
@@ -369,12 +383,11 @@ export class InvokeOrchestrator {
369
383
  // ─── Step 7: 完成 Task (running → success) ───
370
384
  await this.taskService.updateStatus(task.id, 'success', graphResult.output);
371
385
  // ─── Step 7b: 持久化本轮 transcript(统一顺序:assistant tool_calls → tool → assistant 文本)───
372
- // 按 Session 已有行数切 Graph 增量。多段正文收成一条后 Session 比 Graph 短,切点可能提前;finishPoints 只含一个实际节点 id 时靠最后一条纯文本仍落到 merge。
373
- const initialMessagesCount = sessionContext.messages.length + 1;
386
+ // 切点是入图 messages 基线(压缩 `__set` 后 Graph 短于 Session 全文)。user 已在 4b 写入。
374
387
  const allMessages = Array.isArray(graphResult.finalState?.messages)
375
388
  ? graphResult.finalState.messages
376
389
  : [];
377
- const newMessages = sessionVisibleGraphMessages(allMessages.slice(initialMessagesCount), finishPoints);
390
+ const newMessages = sessionVisibleGraphMessages(allMessages.slice(persistBaseline), finishPoints);
378
391
  const duration = Date.now() - startTime;
379
392
  await persistTurnTranscript(this.messageService, {
380
393
  sessionId,
@@ -19,7 +19,7 @@
19
19
  * @see design.md §1.5 /stream 内部执行逻辑
20
20
  * @see requirements.md Requirement 11 AC2
21
21
  */
22
- import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscriptFromStreamEvents, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, appendTerminalToolResult, } from '../../session/persist-turn-messages.js';
22
+ import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, appendTerminalToolResult, } from '../../session/persist-turn-messages.js';
23
23
  import { SessionBudgetExhaustedError } from '../../token/session-budget-checker.js';
24
24
  import { TokenBudgetExhaustedError } from '../../token/token-budget-guard.js';
25
25
  import { recordTaskTokenUsages } from '../../token/record-task-token-usage.js';
@@ -221,8 +221,14 @@ export class StreamOrchestrator {
221
221
  ? (await this.graphLoader.resolveAgentDir(agentCode, req.agentVersion)) ?? undefined
222
222
  : undefined;
223
223
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.graphLoader, agentCode, req.agentVersion);
224
- // 组装 GraphState:有 Checkpoint 历史则只追加本轮 user,避免双重 append
225
- let history = messageRecordsToGraphMessages(sessionContext.messages);
224
+ // 模型上下文:Checkpoint 已有 messages 则压它;压成功用 __set 写回最新 Checkpoint。Message 库不改。
225
+ const facing = await loadModelFacingHistory({
226
+ sessionHistory: messageRecordsToGraphMessages(sessionContext.messages),
227
+ checkpointSaver: this.checkpointSaver,
228
+ threadId: sessionId,
229
+ });
230
+ let history = facing.messages;
231
+ let compressed = false;
226
232
  const compressionOptions = await withCompressionSummaryModel(this.sessionCompression, () => this.graphLoader
227
233
  ? this.graphLoader.resolveDefaultModel(agentCode, req.agentVersion)
228
234
  : Promise.resolve(undefined));
@@ -237,6 +243,9 @@ export class StreamOrchestrator {
237
243
  history = step.messages;
238
244
  continue;
239
245
  }
246
+ if (step.kind === 'completed') {
247
+ compressed = step.payload.success === true;
248
+ }
240
249
  emitter.emit(this.buildSSEEvent(step.kind === 'started' ? 'session.compression.started' : 'session.compression.completed', correlationId, taskId, { ...step.payload }));
241
250
  }
242
251
  history = await applySessionMemory({
@@ -247,11 +256,11 @@ export class StreamOrchestrator {
247
256
  options: this.sessionMemory,
248
257
  });
249
258
  const tokenDetail = resolveTokenDetail(req.config?.tokenDetail);
250
- const messages = await buildTurnGraphMessages({
259
+ const { messages, replaceCheckpointMessages } = turnGraphMessagesAfterCompression({
251
260
  history,
252
261
  input: req.input,
253
- checkpointSaver: this.checkpointSaver,
254
- threadId: sessionId,
262
+ fromCheckpoint: facing.fromCheckpoint,
263
+ compressed,
255
264
  });
256
265
  const skillInput = resolveSkillInputFromInvokeInput(req.input);
257
266
  const assistantBuckets = createStreamAssistantBuckets();
@@ -263,6 +272,7 @@ export class StreamOrchestrator {
263
272
  try {
264
273
  const eventStream = runWithExecutionTrace({ abortSignal }, () => this.graphEngine.stream(graph, {
265
274
  messages,
275
+ replaceCheckpointMessages,
266
276
  userId,
267
277
  delegationToken: executionContext.delegationToken,
268
278
  agentCode,
@@ -239,8 +239,8 @@ export interface GraphState {
239
239
  /** 最近一次 createNode 的 span id;Checkpoint 引用挂在此 Span 上 */
240
240
  lastSpanId?: string;
241
241
  /**
242
- * HITL 续跑:入口 messages 以 __set 覆盖 checkpoint 历史,避免 append 重复。
243
- * 仅 stream/invoke 入口使用,不入持久化语义。
242
+ * 入口 messages 以 __set 覆盖 checkpoint 历史,避免 append 重复。
243
+ * HITL 续跑与会话压缩写回最新 Checkpoint 使用。不入持久化语义。
244
244
  */
245
245
  replaceCheckpointMessages?: boolean;
246
246
  /**
@@ -48,7 +48,7 @@ export interface AgentRuntimeDeps {
48
48
  */
49
49
  traceCollector?: ITraceCollector;
50
50
  recordForkRelation?: RecordForkRelation;
51
- /** 历史压缩;默认关。见 EAP_RUNTIME_COMPRESSION */
51
+ /** 历史压缩;默认开。EAP_RUNTIME_COMPRESSION=0 关闭 */
52
52
  sessionCompression?: SessionCompressionOptions;
53
53
  /** 长期记忆注入;默认关。见 EAP_RUNTIME_MEMORY */
54
54
  sessionMemory?: import('../session/apply-session-memory.js').SessionMemoryOptions;
@@ -15,7 +15,7 @@ import { resolveAgentSystemPromptFromLoader } from '../registry/graph-loader.js'
15
15
  import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
16
16
  import { assembleLoadedTools } from '../tool/harness-primitives.js';
17
17
  import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
18
- import { buildHitlWaitingTaskOutput, buildTurnGraphMessages, buildTurnTranscriptFromStreamEvents, extractHitlThreadId, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, appendTerminalToolResult, } from '../session/persist-turn-messages.js';
18
+ import { buildHitlWaitingTaskOutput, buildTurnTranscriptFromStreamEvents, loadModelFacingHistory, turnGraphMessagesAfterCompression, extractHitlThreadId, extractStreamReasoning, applyStreamTextDelta, createStreamAssistantBuckets, visibleStreamAssistantText, streamAssistantTexts, messageRecordsToGraphMessages, persistHitlWaitingPlaceholder, persistTurnTranscript, persistUserTurnMessage, resolveCheckpointThreadId, appendTerminalToolResult, } from '../session/persist-turn-messages.js';
19
19
  import { iterSessionCompression, withCompressionSummaryModel, } from '../session/apply-session-compression.js';
20
20
  import { applySessionMemory } from '../session/apply-session-memory.js';
21
21
  import { resolveTokenDetail, shouldEmitTokenSse } from '../token/token-detail.js';
@@ -294,7 +294,13 @@ export class AgentRuntime {
294
294
  ? (await this.deps.graphLoader.resolveAgentDir(agentCode, params.agentVersion)) ?? undefined
295
295
  : undefined;
296
296
  const agentSystemPrompt = await resolveAgentSystemPromptFromLoader(this.deps.graphLoader, agentCode, params.agentVersion);
297
- let history = messageRecordsToGraphMessages(sessionContext.messages);
297
+ const facing = await loadModelFacingHistory({
298
+ sessionHistory: messageRecordsToGraphMessages(sessionContext.messages),
299
+ checkpointSaver: this.deps.checkpointSaver,
300
+ threadId: sessionId,
301
+ });
302
+ let history = facing.messages;
303
+ let compressed = false;
298
304
  const compressionOptions = await withCompressionSummaryModel({
299
305
  ...this.deps.sessionCompression,
300
306
  turnLog: this.deps.sessionCompression?.turnLog ?? this.deps.turnLog,
@@ -312,6 +318,9 @@ export class AgentRuntime {
312
318
  history = step.messages;
313
319
  continue;
314
320
  }
321
+ if (step.kind === 'completed') {
322
+ compressed = step.payload.success === true;
323
+ }
315
324
  yield this.toSSE(step.kind === 'started' ? 'session.compression.started' : 'session.compression.completed', correlationId, task.id, { ...step.payload });
316
325
  }
317
326
  history = await applySessionMemory({
@@ -322,11 +331,11 @@ export class AgentRuntime {
322
331
  options: this.deps.sessionMemory,
323
332
  });
324
333
  const tokenDetail = resolveTokenDetail(params.config?.tokenDetail);
325
- const messages = await buildTurnGraphMessages({
334
+ const { messages, replaceCheckpointMessages } = turnGraphMessagesAfterCompression({
326
335
  history,
327
336
  input: params.input,
328
- checkpointSaver: this.deps.checkpointSaver,
329
- threadId: sessionId,
337
+ fromCheckpoint: facing.fromCheckpoint,
338
+ compressed,
330
339
  });
331
340
  const startTime = Date.now();
332
341
  const assistantBuckets = createStreamAssistantBuckets();
@@ -338,6 +347,7 @@ export class AgentRuntime {
338
347
  try {
339
348
  for await (const runtimeEvent of runWithExecutionTrace({ abortSignal }, () => this.deps.graphEngine.stream(graph, {
340
349
  messages,
350
+ replaceCheckpointMessages,
341
351
  userId,
342
352
  delegationToken: ctx.delegationToken,
343
353
  agentCode,
@@ -33,6 +33,7 @@ import { LongTermMemoryService } from '../memory/long-term-memory.service.js';
33
33
  export interface CreateDevRuntimeOptions {
34
34
  interceptors?: ExecutionInterceptors;
35
35
  compression?: {
36
+ /** 默认开。false 或 EAP_RUNTIME_COMPRESSION=0 关闭 */
36
37
  enabled?: boolean;
37
38
  persistCompressed?: boolean;
38
39
  };
@@ -1,11 +1,11 @@
1
1
  /**
2
- * execute/stream 装载历史后的压缩入口。默认关。
2
+ * execute/stream 装载历史后的压缩入口。默认开。
3
3
  *
4
- * C-1a 开关:options.enabled 或 EAP_RUNTIME_COMPRESSION=1
4
+ * C-1a 开关:默认开。options.enabled=false 或 EAP_RUNTIME_COMPRESSION=0 / false 关闭
5
5
  * C-1b 有窗口:用上一轮 llm_turns 的 used/window,不用字符估窗
6
6
  * C-1c 无窗口:只按轮次上限,不按窗压
7
- * C-1d 摘要只走 ILLMProvider;失败则不压缩
8
- * C-1e 默认只改本轮内存历史;persistCompressed 时必须能写 compression record
7
+ * C-1d 摘要只走 ILLMProvider;Echo 或未注入 provider 则不压;失败则不压缩
8
+ * C-1e 压成功且来自 Checkpoint 时,入图 `__set` 覆盖最新 Checkpoint messages;不回写 Message 库。persistCompressed 只写审计 record
9
9
  * C-1f 流式路径在摘要前产出 session.compression.started,结束后产出 completed。
10
10
  * 不新开「占用 ≥ 80% 再 compact」链;触发仍是 C-1b/c。
11
11
  */
@@ -15,6 +15,8 @@ import type { ILlmTurnLog } from '../debug/llm-turn-log.js';
15
15
  import { type CompressionRecord } from '../memory/message-compression.service.js';
16
16
  import type { ICompressionRepository } from '../memory/compression/compression.repository.js';
17
17
  import type { SessionCompressionCompletedPayload, SessionCompressionReason, SessionCompressionStartedPayload } from '../sse/events/session-events.js';
18
+ /** 摘要 LLM 写入 llm_turns 的 nodeId。lastTurnUsage 跳过,避免用摘要占用触发下一轮压缩。 */
19
+ export declare const COMPRESSION_TURN_NODE_ID = "session-compression";
18
20
  export interface SessionCompressionOptions {
19
21
  enabled?: boolean;
20
22
  persistCompressed?: boolean;
@@ -1,11 +1,11 @@
1
1
  /**
2
- * execute/stream 装载历史后的压缩入口。默认关。
2
+ * execute/stream 装载历史后的压缩入口。默认开。
3
3
  *
4
- * C-1a 开关:options.enabled 或 EAP_RUNTIME_COMPRESSION=1
4
+ * C-1a 开关:默认开。options.enabled=false 或 EAP_RUNTIME_COMPRESSION=0 / false 关闭
5
5
  * C-1b 有窗口:用上一轮 llm_turns 的 used/window,不用字符估窗
6
6
  * C-1c 无窗口:只按轮次上限,不按窗压
7
- * C-1d 摘要只走 ILLMProvider;失败则不压缩
8
- * C-1e 默认只改本轮内存历史;persistCompressed 时必须能写 compression record
7
+ * C-1d 摘要只走 ILLMProvider;Echo 或未注入 provider 则不压;失败则不压缩
8
+ * C-1e 压成功且来自 Checkpoint 时,入图 `__set` 覆盖最新 Checkpoint messages;不回写 Message 库。persistCompressed 只写审计 record
9
9
  * C-1f 流式路径在摘要前产出 session.compression.started,结束后产出 completed。
10
10
  * 不新开「占用 ≥ 80% 再 compact」链;触发仍是 C-1b/c。
11
11
  */
@@ -13,13 +13,17 @@ import { EchoLLMProvider } from '../providers/echo-llm-provider.js';
13
13
  import { COMPRESSION_WINDOW_FACTOR, countRounds, DEFAULT_COMPRESSION_CONFIG, } from '../memory/message-compression.service.js';
14
14
  import { SlidingWindowCompression } from '../memory/compression/sliding-window.compression.js';
15
15
  import { emitRuntimeLog } from '../common/logger.js';
16
+ /** 摘要 LLM 写入 llm_turns 的 nodeId。lastTurnUsage 跳过,避免用摘要占用触发下一轮压缩。 */
17
+ export const COMPRESSION_TURN_NODE_ID = 'session-compression';
16
18
  export function isRuntimeCompressionEnabled(explicit, env = process.env) {
17
19
  if (explicit === true)
18
20
  return true;
19
21
  if (explicit === false)
20
22
  return false;
21
23
  const raw = env.EAP_RUNTIME_COMPRESSION ?? env.EAP_COMPRESSION_ENABLED;
22
- return raw === '1' || raw === 'true';
24
+ if (raw === '0' || raw === 'false')
25
+ return false;
26
+ return true;
23
27
  }
24
28
  export function isRuntimeCompressionPersist(explicit, env = process.env) {
25
29
  if (explicit === true)
@@ -164,7 +168,7 @@ async function runDecidedSessionCompression(params, decision) {
164
168
  maxRounds,
165
169
  preserveRecentRounds: options.preserveRecentRounds ?? DEFAULT_COMPRESSION_CONFIG.preserveRecentRounds,
166
170
  ...(decision.window ? { maxContextTokens: decision.window } : {}),
167
- }, async (msgs) => summarizeWithProvider(provider, msgs, options.summaryModel));
171
+ }, async (msgs) => summarizeWithProvider(provider, msgs, options.summaryModel, params.sessionId));
168
172
  const compressed = await compression.compress(history);
169
173
  if (compressed === history) {
170
174
  return {
@@ -250,12 +254,17 @@ async function lastTurnUsage(turnLog, sessionId) {
250
254
  if (!turnLog || !sessionId)
251
255
  return undefined;
252
256
  const index = await turnLog.list(sessionId);
253
- const last = index[index.length - 1];
254
- if (!last || typeof last.used !== 'number')
255
- return undefined;
256
- return { used: last.used, window: last.window };
257
+ for (let i = index.length - 1; i >= 0; i--) {
258
+ const entry = index[i];
259
+ if (!entry || entry.nodeId === COMPRESSION_TURN_NODE_ID)
260
+ continue;
261
+ if (typeof entry.used !== 'number')
262
+ continue;
263
+ return { used: entry.used, window: entry.window };
264
+ }
265
+ return undefined;
257
266
  }
258
- async function summarizeWithProvider(provider, messages, summaryModel) {
267
+ async function summarizeWithProvider(provider, messages, summaryModel, sessionId) {
259
268
  const body = messages
260
269
  .map((m) => `${m.role}: ${m.content}`)
261
270
  .join('\n')
@@ -272,6 +281,10 @@ async function summarizeWithProvider(provider, messages, summaryModel) {
272
281
  model,
273
282
  temperature: 0.2,
274
283
  maxTokens: 1024,
284
+ turn: {
285
+ sessionId,
286
+ nodeId: COMPRESSION_TURN_NODE_ID,
287
+ },
275
288
  });
276
289
  const text = response.content?.trim();
277
290
  if (!text) {
@@ -10,6 +10,6 @@ export type { FileMessageRepositoryOptions } from './file-message.repository.js'
10
10
  export { SessionContextService } from './session-context.service.js';
11
11
  export type { ResolveSessionParams, SessionContext, } from './session-context.service.js';
12
12
  export { CompressionMiddleware, createCompressionMiddleware } from './compression-middleware.js';
13
- export { applySessionCompression, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, type SessionCompressionOptions, type RunSessionCompressionParams, type RunSessionCompressionResult, type SessionCompressionDecision, type SessionCompressionStep, } from './apply-session-compression.js';
13
+ export { applySessionCompression, COMPRESSION_TURN_NODE_ID, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, type SessionCompressionOptions, type RunSessionCompressionParams, type RunSessionCompressionResult, type SessionCompressionDecision, type SessionCompressionStep, } from './apply-session-compression.js';
14
14
  export type { CompressionMiddlewareConfig } from './compression-middleware.js';
15
15
  export { applySessionMemory, isRuntimeMemoryEnabled, type SessionMemoryOptions, } from './apply-session-memory.js';
@@ -10,5 +10,5 @@ export { FileMessageRepository } from './file-message.repository.js';
10
10
  export { SessionContextService } from './session-context.service.js';
11
11
  // 压缩中间件
12
12
  export { CompressionMiddleware, createCompressionMiddleware } from './compression-middleware.js';
13
- export { applySessionCompression, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, } from './apply-session-compression.js';
13
+ export { applySessionCompression, COMPRESSION_TURN_NODE_ID, withCompressionSummaryModel, runSessionCompression, decideSessionCompression, iterSessionCompression, formatCompressionStartedMessage, formatCompressionCompletedMessage, isRuntimeCompressionEnabled, isRuntimeCompressionPersist, } from './apply-session-compression.js';
14
14
  export { applySessionMemory, isRuntimeMemoryEnabled, } from './apply-session-memory.js';
@@ -99,6 +99,15 @@ export declare function appendTerminalToolResult(eventType: string, payload: unk
99
99
  }>): void;
100
100
  /** 历史 MessageRecord → GraphMessage(保留 tool 元数据与 reasoning) */
101
101
  export declare function messageRecordsToGraphMessages(records: MessageRecord[]): GraphMessage[];
102
+ type CheckpointLoad = {
103
+ state?: {
104
+ messages?: unknown;
105
+ };
106
+ engineSnapshot?: Record<string, unknown>;
107
+ } | null;
108
+ type CheckpointLoader = {
109
+ load(threadId: string): Promise<CheckpointLoad>;
110
+ };
102
111
  /**
103
112
  * 组装本轮送入 Graph 的 messages。
104
113
  *
@@ -107,21 +116,52 @@ export declare function messageRecordsToGraphMessages(records: MessageRecord[]):
107
116
  * - Session 级 Checkpoint 持有图内 messages(含 reasoning)
108
117
  * - 当 Checkpoint 已有历史时,只追加本轮 user,避免双重 append
109
118
  * - Checkpoint 缺失/无 messages 时,回退 Session 全量历史
119
+ * - 压缩针对 Checkpoint(无则 Session)中的模型上下文;压成功且来自 Checkpoint 时由调用方设 `replaceCheckpointMessages`
110
120
  */
111
121
  export declare function buildTurnGraphMessages(params: {
112
122
  history: GraphMessage[];
113
123
  input: string;
114
- checkpointSaver?: {
115
- load(threadId: string): Promise<{
116
- state?: {
117
- messages?: unknown;
118
- };
119
- engineSnapshot?: Record<string, unknown>;
120
- } | null>;
121
- } | null;
124
+ checkpointSaver?: CheckpointLoader | null;
122
125
  /** 与 LangGraph configurable.thread_id 一致(通常为 sessionId) */
123
126
  threadId?: string;
124
127
  }): Promise<GraphMessage[]>;
128
+ /**
129
+ * 模型上下文:Checkpoint 已有 messages 用它,否则用 Session 历史。
130
+ * 压缩必须压这份列表,不能只压 Session 再在有 Checkpoint 时丢掉。
131
+ */
132
+ export declare function loadModelFacingHistory(params: {
133
+ sessionHistory: GraphMessage[];
134
+ checkpointSaver?: CheckpointLoader | null;
135
+ threadId?: string;
136
+ }): Promise<{
137
+ messages: GraphMessage[];
138
+ fromCheckpoint: boolean;
139
+ }>;
140
+ /**
141
+ * 压缩后的入图列表。来自 Checkpoint 且本轮压成功:整表 `__set` 覆盖,写入最新 Checkpoint。
142
+ * 未压且来自 Checkpoint:只追加本轮 user。无 Checkpoint:历史 + 本轮 user。
143
+ */
144
+ export declare function turnGraphMessagesAfterCompression(params: {
145
+ history: GraphMessage[];
146
+ input: string;
147
+ fromCheckpoint: boolean;
148
+ compressed: boolean;
149
+ }): {
150
+ messages: GraphMessage[];
151
+ replaceCheckpointMessages: boolean;
152
+ };
153
+ /**
154
+ * invoke 从 Graph 最终 messages 切本轮增量的起点。
155
+ * 覆盖(`__set`)或无 Checkpoint:入图数组长度(已含本轮 user)。
156
+ * 追加到已有 Checkpoint:模型上下文条数 + 入图条数(通常 1 条 user)。
157
+ */
158
+ export declare function persistGraphMessageBaseline(params: {
159
+ fromCheckpoint: boolean;
160
+ replaceCheckpointMessages: boolean;
161
+ graphInputCount: number;
162
+ modelFacingCount: number;
163
+ }): number;
164
+ export declare function graphMessagesFromCheckpoint(cp: CheckpointLoad): GraphMessage[];
125
165
  /**
126
166
  * HITL 等待态写入 Task.output 的结构化载荷。
127
167
  * 冷恢复时优先读 `hitlThreadId`,避免误用 correlationId。
@@ -146,7 +186,7 @@ export declare function extractHitlThreadId(output: unknown): string | undefined
146
186
  * 与 LangGraph put / 子图内 HITL interrupt 同一把钥匙。
147
187
  *
148
188
  * 注意:session 级 thread + 全量 Session 历史同时注入会导致 messages 双重 append。
149
- * 组装本轮 Graph 输入请用 {@link buildTurnGraphMessages}(有 Checkpoint 历史则只追加 user)。
189
+ * 组装本轮 Graph 输入请用 {@link loadModelFacingHistory} 与 {@link turnGraphMessagesAfterCompression}。
150
190
  */
151
191
  export declare function resolveCheckpointThreadId(params: {
152
192
  sessionId?: string;
@@ -199,3 +239,4 @@ export declare function persistTurnTranscript(messageService: MessageService | u
199
239
  assistantTokenCount?: number;
200
240
  assistantDurationMs?: number | null;
201
241
  }): Promise<void>;
242
+ export {};
@@ -305,31 +305,149 @@ function extractReasoningContent(reasoning) {
305
305
  * - Session 级 Checkpoint 持有图内 messages(含 reasoning)
306
306
  * - 当 Checkpoint 已有历史时,只追加本轮 user,避免双重 append
307
307
  * - Checkpoint 缺失/无 messages 时,回退 Session 全量历史
308
+ * - 压缩针对 Checkpoint(无则 Session)中的模型上下文;压成功且来自 Checkpoint 时由调用方设 `replaceCheckpointMessages`
308
309
  */
309
310
  export async function buildTurnGraphMessages(params) {
310
- const newUser = { role: 'user', content: params.input };
311
- const history = params.history;
312
- if (!params.checkpointSaver || !params.threadId || history.length === 0) {
313
- return [...history, newUser];
311
+ const facing = await loadModelFacingHistory({
312
+ sessionHistory: params.history,
313
+ checkpointSaver: params.checkpointSaver,
314
+ threadId: params.threadId,
315
+ });
316
+ return turnGraphMessagesAfterCompression({
317
+ history: facing.messages,
318
+ input: params.input,
319
+ fromCheckpoint: facing.fromCheckpoint,
320
+ compressed: false,
321
+ }).messages;
322
+ }
323
+ /**
324
+ * 模型上下文:Checkpoint 已有 messages 用它,否则用 Session 历史。
325
+ * 压缩必须压这份列表,不能只压 Session 再在有 Checkpoint 时丢掉。
326
+ */
327
+ export async function loadModelFacingHistory(params) {
328
+ const sessionHistory = params.sessionHistory ?? [];
329
+ if (!params.checkpointSaver || !params.threadId) {
330
+ return { messages: sessionHistory, fromCheckpoint: false };
314
331
  }
315
332
  const cp = await params.checkpointSaver.load(params.threadId);
316
- if (checkpointHasMessages(cp)) {
317
- return [newUser];
333
+ const checkpointMessages = graphMessagesFromCheckpoint(cp);
334
+ if (checkpointMessages.length > 0) {
335
+ return { messages: checkpointMessages, fromCheckpoint: true };
318
336
  }
319
- return [...history, newUser];
337
+ return { messages: sessionHistory, fromCheckpoint: false };
320
338
  }
321
- function checkpointHasMessages(cp) {
339
+ /**
340
+ * 压缩后的入图列表。来自 Checkpoint 且本轮压成功:整表 `__set` 覆盖,写入最新 Checkpoint。
341
+ * 未压且来自 Checkpoint:只追加本轮 user。无 Checkpoint:历史 + 本轮 user。
342
+ */
343
+ export function turnGraphMessagesAfterCompression(params) {
344
+ const newUser = { role: 'user', content: params.input };
345
+ if (params.fromCheckpoint && params.compressed) {
346
+ return {
347
+ messages: [...params.history, newUser],
348
+ replaceCheckpointMessages: true,
349
+ };
350
+ }
351
+ if (params.fromCheckpoint) {
352
+ return { messages: [newUser], replaceCheckpointMessages: false };
353
+ }
354
+ return {
355
+ messages: [...params.history, newUser],
356
+ replaceCheckpointMessages: false,
357
+ };
358
+ }
359
+ /**
360
+ * invoke 从 Graph 最终 messages 切本轮增量的起点。
361
+ * 覆盖(`__set`)或无 Checkpoint:入图数组长度(已含本轮 user)。
362
+ * 追加到已有 Checkpoint:模型上下文条数 + 入图条数(通常 1 条 user)。
363
+ */
364
+ export function persistGraphMessageBaseline(params) {
365
+ if (params.replaceCheckpointMessages || !params.fromCheckpoint) {
366
+ return params.graphInputCount;
367
+ }
368
+ return params.modelFacingCount + params.graphInputCount;
369
+ }
370
+ export function graphMessagesFromCheckpoint(cp) {
322
371
  if (!cp)
323
- return false;
324
- if (Array.isArray(cp.state?.messages) && cp.state.messages.length > 0)
325
- return true;
372
+ return [];
326
373
  const channelValues = cp.engineSnapshot?.['channel_values'];
327
374
  if (channelValues && typeof channelValues === 'object') {
328
375
  const messages = channelValues['messages'];
329
- if (Array.isArray(messages) && messages.length > 0)
330
- return true;
376
+ if (Array.isArray(messages) && messages.length > 0) {
377
+ return unknownListToGraphMessages(messages);
378
+ }
379
+ }
380
+ if (Array.isArray(cp.state?.messages) && cp.state.messages.length > 0) {
381
+ return unknownListToGraphMessages(cp.state.messages);
382
+ }
383
+ return [];
384
+ }
385
+ function unknownListToGraphMessages(raw) {
386
+ return raw.map((msg) => unknownToGraphMessage(msg));
387
+ }
388
+ function unknownToGraphMessage(msg) {
389
+ if (typeof msg !== 'object' || msg === null) {
390
+ return { role: 'user', content: String(msg) };
331
391
  }
332
- return false;
392
+ const m = msg;
393
+ const nested = m.kwargs && typeof m.kwargs === 'object' ? m.kwargs : undefined;
394
+ const lcName = Array.isArray(m.id) ? m.id[m.id.length - 1] : undefined;
395
+ const typeHint = m.type === 'constructor' ? undefined : m.type;
396
+ const role = graphRoleFromUnknown(m.role ?? nested?.role ?? nested?.type ?? typeHint ?? lcName);
397
+ const content = messageContentToString(m.content ?? nested?.content);
398
+ const toolCalls = (m.toolCalls ?? m.tool_calls ?? nested?.tool_calls);
399
+ const toolCallId = (typeof m.toolCallId === 'string' && m.toolCallId) ||
400
+ (typeof m.tool_call_id === 'string' && m.tool_call_id) ||
401
+ (typeof nested?.tool_call_id === 'string' && nested.tool_call_id) ||
402
+ undefined;
403
+ const toolName = (typeof m.toolName === 'string' && m.toolName) ||
404
+ (typeof nested?.name === 'string' && nested.name) ||
405
+ undefined;
406
+ const success = typeof m.success === 'boolean' ? m.success : undefined;
407
+ const reasoningContent = (typeof m.reasoningContent === 'string' && m.reasoningContent) ||
408
+ (typeof m.reasoning_content === 'string' && m.reasoning_content) ||
409
+ undefined;
410
+ return {
411
+ role,
412
+ content,
413
+ ...(toolCalls ? { toolCalls } : {}),
414
+ ...(toolCallId ? { toolCallId } : {}),
415
+ ...(toolName ? { toolName } : {}),
416
+ ...(success !== undefined ? { success } : {}),
417
+ ...(reasoningContent ? { reasoningContent } : {}),
418
+ };
419
+ }
420
+ function graphRoleFromUnknown(raw) {
421
+ if (raw === 'user' || raw === 'assistant' || raw === 'system' || raw === 'tool')
422
+ return raw;
423
+ if (raw === 'human' || raw === 'HumanMessage')
424
+ return 'user';
425
+ if (raw === 'ai' || raw === 'AIMessage')
426
+ return 'assistant';
427
+ if (raw === 'system' || raw === 'SystemMessage')
428
+ return 'system';
429
+ if (raw === 'tool' || raw === 'ToolMessage')
430
+ return 'tool';
431
+ return 'user';
432
+ }
433
+ function messageContentToString(content) {
434
+ if (typeof content === 'string')
435
+ return content;
436
+ if (Array.isArray(content)) {
437
+ return content
438
+ .map((part) => {
439
+ if (typeof part === 'string')
440
+ return part;
441
+ if (part && typeof part === 'object' && typeof part.text === 'string') {
442
+ return part.text;
443
+ }
444
+ return '';
445
+ })
446
+ .join('');
447
+ }
448
+ if (content == null)
449
+ return '';
450
+ return String(content);
333
451
  }
334
452
  export function buildHitlWaitingTaskOutput(params) {
335
453
  return {
@@ -362,7 +480,7 @@ export function extractHitlThreadId(output) {
362
480
  * 与 LangGraph put / 子图内 HITL interrupt 同一把钥匙。
363
481
  *
364
482
  * 注意:session 级 thread + 全量 Session 历史同时注入会导致 messages 双重 append。
365
- * 组装本轮 Graph 输入请用 {@link buildTurnGraphMessages}(有 Checkpoint 历史则只追加 user)。
483
+ * 组装本轮 Graph 输入请用 {@link loadModelFacingHistory} 与 {@link turnGraphMessagesAfterCompression}。
366
484
  */
367
485
  export function resolveCheckpointThreadId(params) {
368
486
  if (params.sessionId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gt-fe/eap-runtime",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "Enterprise Agent Platform — Runtime核心:Graph执行引擎、Task生命周期、Session管理、Checkpoint持久化",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",