@downcity/agent 1.1.228 → 1.1.236

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/bin/agent/local/Agent.d.ts +6 -1
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +30 -3
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentSessions.d.ts +23 -0
  6. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  7. package/bin/agent/local/services/AgentSessions.js +44 -0
  8. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  9. package/bin/agent/remote/RemoteSession.d.ts +2 -0
  10. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  11. package/bin/agent/remote/RemoteSession.js +4 -0
  12. package/bin/agent/remote/RemoteSession.js.map +1 -1
  13. package/bin/agent/remote/RemoteTransport.d.ts +2 -0
  14. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  15. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
  16. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  17. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -7
  18. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  19. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
  20. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  21. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
  22. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  23. package/bin/executor/Executor.d.ts +43 -0
  24. package/bin/executor/Executor.d.ts.map +1 -1
  25. package/bin/executor/Executor.js +127 -7
  26. package/bin/executor/Executor.js.map +1 -1
  27. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +10 -0
  28. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  29. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -10
  30. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  31. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -34
  32. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  33. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
  34. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  35. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
  36. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  37. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
  38. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
  39. package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
  40. package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
  41. package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
  42. package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
  43. package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
  44. package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
  45. package/bin/executor/core-engine/CoreEngineRunner.d.ts +15 -0
  46. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  47. package/bin/executor/core-engine/CoreEngineRunner.js +150 -35
  48. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  49. package/bin/executor/store/history/SessionHistoryStore.d.ts +7 -23
  50. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  51. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  52. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -4
  53. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  54. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  55. package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
  56. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  57. package/bin/executor/types/SessionRun.d.ts +8 -0
  58. package/bin/executor/types/SessionRun.d.ts.map +1 -1
  59. package/bin/index.d.ts +3 -2
  60. package/bin/index.d.ts.map +1 -1
  61. package/bin/index.js +2 -2
  62. package/bin/index.js.map +1 -1
  63. package/bin/model/CityModelAdapter.d.ts +6 -0
  64. package/bin/model/CityModelAdapter.d.ts.map +1 -1
  65. package/bin/model/CityModelAdapter.js +13 -0
  66. package/bin/model/CityModelAdapter.js.map +1 -1
  67. package/bin/plugin/core/PluginLocalExecution.js +3 -3
  68. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  69. package/bin/plugin/core/PluginRegistry.d.ts +41 -2
  70. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  71. package/bin/plugin/core/PluginRegistry.js +145 -6
  72. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  73. package/bin/rpc/Client.d.ts +4 -0
  74. package/bin/rpc/Client.d.ts.map +1 -1
  75. package/bin/rpc/Client.js +11 -0
  76. package/bin/rpc/Client.js.map +1 -1
  77. package/bin/session/Session.d.ts +15 -1
  78. package/bin/session/Session.d.ts.map +1 -1
  79. package/bin/session/Session.js +83 -11
  80. package/bin/session/Session.js.map +1 -1
  81. package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
  82. package/bin/session/SessionSystemBuilder.js +3 -1
  83. package/bin/session/SessionSystemBuilder.js.map +1 -1
  84. package/bin/session/browse/Browse.d.ts +0 -8
  85. package/bin/session/browse/Browse.d.ts.map +1 -1
  86. package/bin/session/browse/Browse.js +72 -57
  87. package/bin/session/browse/Browse.js.map +1 -1
  88. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
  89. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  90. package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
  91. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  92. package/bin/session/recorder/SessionRecorder.d.ts +20 -2
  93. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  94. package/bin/session/recorder/SessionRecorder.js +91 -46
  95. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  96. package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
  97. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  98. package/bin/session/recorder/SessionRecorderCompaction.js +118 -88
  99. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  100. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
  101. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  102. package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
  103. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  104. package/bin/session/runtime/SessionPromptRuntime.d.ts +21 -1
  105. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  106. package/bin/session/runtime/SessionPromptRuntime.js +65 -8
  107. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  108. package/bin/session/services/SessionStateService.d.ts +19 -1
  109. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  110. package/bin/session/services/SessionStateService.js +70 -40
  111. package/bin/session/services/SessionStateService.js.map +1 -1
  112. package/bin/session/services/SessionTurnService.d.ts +15 -0
  113. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  114. package/bin/session/services/SessionTurnService.js +104 -7
  115. package/bin/session/services/SessionTurnService.js.map +1 -1
  116. package/bin/session/services/SessionViewService.d.ts +1 -5
  117. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  118. package/bin/session/services/SessionViewService.js +4 -34
  119. package/bin/session/services/SessionViewService.js.map +1 -1
  120. package/bin/session/storage/Paths.d.ts +3 -15
  121. package/bin/session/storage/Paths.d.ts.map +1 -1
  122. package/bin/session/storage/Paths.js +5 -21
  123. package/bin/session/storage/Paths.js.map +1 -1
  124. package/bin/types/agent/SessionActor.d.ts +8 -0
  125. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  126. package/bin/types/agent/SessionTypes.d.ts +2 -24
  127. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  128. package/bin/types/executor/SessionRunContext.d.ts +29 -0
  129. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  130. package/bin/types/plugin/PluginRuntime.d.ts +59 -2
  131. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  132. package/bin/types/plugin/PluginState.d.ts +10 -0
  133. package/bin/types/plugin/PluginState.d.ts.map +1 -1
  134. package/bin/types/rpc/RpcProtocol.d.ts +10 -0
  135. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  136. package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
  137. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  138. package/bin/types/runtime/agent/AgentContext.js +29 -6
  139. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  140. package/bin/types/session/SessionLocalState.d.ts +8 -0
  141. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  142. package/bin/types/session/SessionMessage.d.ts +16 -12
  143. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  144. package/bin/types/session/SessionMutation.d.ts +1 -1
  145. package/bin/types/session/SessionMutation.js +1 -1
  146. package/bin/types/session/SessionOptions.d.ts +16 -0
  147. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  148. package/bin/types/session/SessionQueueCommand.d.ts +68 -0
  149. package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
  150. package/bin/types/session/SessionQueueCommand.js +10 -0
  151. package/bin/types/session/SessionQueueCommand.js.map +1 -0
  152. package/bin/types/session/SessionSegment.d.ts +57 -0
  153. package/bin/types/session/SessionSegment.d.ts.map +1 -0
  154. package/bin/types/session/SessionSegment.js +8 -0
  155. package/bin/types/session/SessionSegment.js.map +1 -0
  156. package/bin/utils/logger/Logger.d.ts +7 -9
  157. package/bin/utils/logger/Logger.d.ts.map +1 -1
  158. package/bin/utils/logger/Logger.js +11 -9
  159. package/bin/utils/logger/Logger.js.map +1 -1
  160. package/package.json +3 -3
  161. package/scripts/city-model-tool-loop.test.mjs +32 -0
  162. package/scripts/core-engine-context-compaction.test.mjs +411 -0
  163. package/scripts/logger-isolation.test.mjs +55 -0
  164. package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
  165. package/scripts/plugin-tool-bridge.test.mjs +52 -0
  166. package/scripts/remote-session-reconnect.test.mjs +18 -0
  167. package/scripts/session-action-message.test.mjs +59 -13
  168. package/scripts/session-compaction-context-window.test.mjs +96 -0
  169. package/scripts/session-config-turn-boundary.test.mjs +278 -0
  170. package/scripts/session-list-title.test.mjs +21 -3
  171. package/scripts/session-prompt-runtime.test.mjs +93 -0
  172. package/scripts/session-recorder.test.mjs +335 -15
  173. package/src/agent/local/Agent.ts +34 -3
  174. package/src/agent/local/services/AgentSessions.ts +61 -0
  175. package/src/agent/remote/RemoteSession.ts +5 -0
  176. package/src/agent/remote/RemoteTransport.ts +2 -0
  177. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -5
  178. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
  179. package/src/executor/Executor.ts +161 -7
  180. package/src/executor/composer/compaction/SessionCompactionComposer.ts +12 -0
  181. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -55
  182. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
  183. package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
  184. package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
  185. package/src/executor/core-engine/CoreEngineRunner.ts +211 -43
  186. package/src/executor/store/history/SessionHistoryStore.ts +7 -26
  187. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -4
  188. package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
  189. package/src/executor/types/SessionRun.ts +9 -0
  190. package/src/index.ts +9 -1
  191. package/src/model/CityModelAdapter.ts +15 -0
  192. package/src/plugin/core/PluginLocalExecution.ts +3 -3
  193. package/src/plugin/core/PluginRegistry.ts +187 -6
  194. package/src/rpc/Client.ts +12 -0
  195. package/src/session/Session.ts +97 -11
  196. package/src/session/SessionSystemBuilder.ts +4 -1
  197. package/src/session/browse/Browse.ts +73 -69
  198. package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
  199. package/src/session/recorder/SessionRecorder.ts +116 -49
  200. package/src/session/recorder/SessionRecorderCompaction.ts +131 -108
  201. package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
  202. package/src/session/runtime/SessionPromptRuntime.ts +86 -10
  203. package/src/session/services/SessionStateService.ts +89 -45
  204. package/src/session/services/SessionTurnService.ts +112 -7
  205. package/src/session/services/SessionViewService.ts +4 -50
  206. package/src/session/storage/Paths.ts +5 -38
  207. package/src/types/agent/SessionActor.ts +9 -0
  208. package/src/types/agent/SessionTypes.ts +2 -24
  209. package/src/types/executor/SessionRunContext.ts +34 -0
  210. package/src/types/plugin/PluginRuntime.ts +63 -2
  211. package/src/types/plugin/PluginState.ts +10 -0
  212. package/src/types/rpc/RpcProtocol.ts +11 -0
  213. package/src/types/runtime/agent/AgentContext.ts +32 -7
  214. package/src/types/session/SessionLocalState.ts +9 -0
  215. package/src/types/session/SessionMessage.ts +16 -12
  216. package/src/types/session/SessionMutation.ts +1 -1
  217. package/src/types/session/SessionOptions.ts +21 -0
  218. package/src/types/session/SessionQueueCommand.ts +78 -0
  219. package/src/types/session/SessionSegment.ts +62 -0
  220. package/src/utils/logger/Logger.ts +10 -10
  221. package/tsconfig.tsbuildinfo +1 -1
@@ -11,6 +11,7 @@ import type { JsonObject, JsonValue } from "@/types/common/Json.js";
11
11
  import type {
12
12
  ListSessionMessagesInput,
13
13
  SessionActionMessage,
14
+ SessionAssistantFilePart,
14
15
  SessionAssistantMessage,
15
16
  SessionAssistantMessagePart,
16
17
  SessionAssistantToolPart,
@@ -24,6 +25,11 @@ import type {
24
25
  SessionMutation,
25
26
  SessionMessageMutation as SessionMessageSnapshotMutation,
26
27
  } from "@/types/session/SessionMutation.js";
28
+ import type {
29
+ SessionContextSnapshot,
30
+ SessionMessageStorageStats,
31
+ SessionSegmentSummary,
32
+ } from "@/types/session/SessionSegment.js";
27
33
 
28
34
  /** SessionRecorder 构造参数。 */
29
35
  export interface SessionRecorderOptions {
@@ -289,38 +295,88 @@ export class SessionRecorder {
289
295
  }))) as SessionErrorMessage;
290
296
  }
291
297
 
292
- /** 读取折叠后的 Message snapshot。 */
298
+ /** 读取 Active 或指定边界之前最近的完整 Segment。 */
293
299
  async list_messages(
294
300
  input?: ListSessionMessagesInput,
295
301
  ): Promise<SessionMessagePage> {
296
302
  await this.ensure_initialized();
297
- const include_internal = input?.include_internal === true;
298
- const through_sequence = Number.isFinite(input?.through_sequence)
299
- ? Number(input?.through_sequence)
300
- : Number.POSITIVE_INFINITY;
301
- const before_sequence = Number.isFinite(input?.before_sequence)
302
- ? Number(input?.before_sequence)
303
- : Number.POSITIVE_INFINITY;
304
- const all = [...this.messages_by_id.values()]
305
- .filter(
306
- (message) =>
307
- (include_internal || message.visibility === "visible") &&
308
- message.sequence <= through_sequence &&
309
- message.sequence < before_sequence,
310
- )
311
- .sort((left, right) => left.sequence - right.sequence);
312
- const offset = decode_cursor(input?.cursor);
313
- const limit = normalize_limit(input?.limit, 100, 500);
314
- const items = all.slice(offset, offset + limit).map((item) => structuredClone(item));
315
- const next_offset = offset + items.length;
303
+ const requests_segment = input?.before_sequence !== undefined;
304
+ const before_sequence = input?.before_sequence;
305
+ if (
306
+ requests_segment &&
307
+ (!Number.isInteger(before_sequence) || Number(before_sequence) <= 0)
308
+ ) {
309
+ throw new Error("before_sequence must be a positive integer");
310
+ }
311
+ const segment = requests_segment
312
+ ? await this.store.read_segment_before(Number(before_sequence))
313
+ : null;
314
+ const source = requests_segment ? "segment" as const : "active" as const;
315
+ const messages = requests_segment
316
+ ? segment?.messages || []
317
+ : [...this.messages_by_id.values()].sort(compare_message_sequence);
318
+ const start_sequence = messages[0]?.sequence;
319
+ const end_sequence = messages.at(-1)?.sequence;
320
+ const history_boundary = source === "segment"
321
+ ? segment?.range.start_sequence
322
+ : await this.store.active_before_sequence(messages);
323
+ const has_more = history_boundary !== undefined &&
324
+ await this.store.has_segment_before(history_boundary);
325
+ const stats = await this.store.stats();
326
+ const items = messages
327
+ .filter((message) => input?.include_internal === true || message.visibility === "visible")
328
+ .map((message) => structuredClone(message));
316
329
  return {
317
330
  items,
318
- total: all.length,
319
- ...(next_offset < all.length ? { next_cursor: encode_cursor(next_offset) } : {}),
320
- has_more: next_offset < all.length,
331
+ total: stats.message_count,
332
+ source,
333
+ ...(start_sequence !== undefined ? { start_sequence } : {}),
334
+ ...(end_sequence !== undefined ? { end_sequence } : {}),
335
+ ...(has_more && history_boundary !== undefined
336
+ ? { next_before_sequence: history_boundary }
337
+ : {}),
338
+ has_more,
321
339
  };
322
340
  }
323
341
 
342
+ /** 读取最新累计 Summary 与全部 Active Message,供模型上下文使用。 */
343
+ async context_snapshot(): Promise<SessionContextSnapshot> {
344
+ await this.ensure_initialized();
345
+ return {
346
+ summary: await this.store.read_latest_summary(),
347
+ messages: [...this.messages_by_id.values()]
348
+ .sort(compare_message_sequence)
349
+ .map((message) => structuredClone(message)),
350
+ };
351
+ }
352
+
353
+ /** 读取全部真实历史,供 Fork 等明确的全量复制操作使用。 */
354
+ async list_history_messages(): Promise<SessionMessage[]> {
355
+ await this.ensure_initialized();
356
+ return await this.store.list_history_messages();
357
+ }
358
+
359
+ /** 读取当前 Session 的存储统计。 */
360
+ async storage_stats(): Promise<SessionMessageStorageStats> {
361
+ await this.ensure_initialized();
362
+ return await this.store.stats();
363
+ }
364
+
365
+ /** 把 Active 前缀和累计 Summary 提交为不可变 Segment。 */
366
+ async compact_active(input: {
367
+ /** 移入 Segment 的最后一条真实 Message sequence。 */
368
+ through_sequence: number;
369
+ /** 写入 Segment footer 的累计 Summary。 */
370
+ summary: SessionSegmentSummary;
371
+ }): Promise<void> {
372
+ await this.ensure_initialized();
373
+ const result = await this.store.compact_active(input);
374
+ this.messages_by_id.clear();
375
+ for (const message of result.active_messages) {
376
+ this.messages_by_id.set(message.message_id, structuredClone(message));
377
+ }
378
+ }
379
+
324
380
  /** 向当前 Session 导入 fork 来源 Message,并重新分配全部身份和顺序。 */
325
381
  async import_messages(messages: SessionMessage[]): Promise<void> {
326
382
  const turn_ids = new Map<string, string>();
@@ -551,6 +607,7 @@ export class SessionRecorder {
551
607
  private async ensure_initialized(): Promise<void> {
552
608
  if (!this.initialized) await this.initialize();
553
609
  }
610
+
554
611
  }
555
612
 
556
613
  /** 单个 Assistant segment 的流式 writer。 */
@@ -671,10 +728,7 @@ export class SessionAssistantMessageWriter {
671
728
  return;
672
729
  }
673
730
  case "file":
674
- await this.upsert_part({
675
- part_id: `file:${generateId()}`,
676
- sequence: this.next_part_sequence(),
677
- type: "file",
731
+ await this.append_file_part({
678
732
  media_type: chunk.mediaType,
679
733
  url: chunk.url,
680
734
  });
@@ -689,6 +743,36 @@ export class SessionAssistantMessageWriter {
689
743
  await this.recorder.update_assistant_part(this.message_id, part);
690
744
  }
691
745
 
746
+ /**
747
+ * 把最终结果中的文件补入当前 Assistant,并对流式已写入文件去重。
748
+ */
749
+ async append_file_part(
750
+ input: Pick<SessionAssistantFilePart, "filename" | "media_type" | "url">,
751
+ ): Promise<void> {
752
+ const filename = String(input.filename || "").trim();
753
+ const current = this.current_message();
754
+ const existing = current.parts.find(
755
+ (part) =>
756
+ part.type === "file" &&
757
+ part.url === input.url &&
758
+ part.media_type === input.media_type,
759
+ );
760
+ if (existing?.type === "file") {
761
+ if (filename && String(existing.filename || "").trim() !== filename) {
762
+ await this.upsert_part({ ...existing, filename });
763
+ }
764
+ return;
765
+ }
766
+ await this.upsert_part({
767
+ part_id: `file:${generateId()}`,
768
+ sequence: this.next_part_sequence(),
769
+ type: "file",
770
+ media_type: input.media_type,
771
+ url: input.url,
772
+ ...(filename ? { filename } : {}),
773
+ });
774
+ }
775
+
692
776
  /** 当前实现逐 chunk 等待落盘,因此 flush 在返回时天然完成。 */
693
777
  async flush(): Promise<void> {}
694
778
 
@@ -907,28 +991,6 @@ function to_json_value(input: unknown): JsonValue {
907
991
  }
908
992
  }
909
993
 
910
- function normalize_limit(value: number | undefined, fallback: number, maximum: number): number {
911
- if (!Number.isFinite(value)) return fallback;
912
- return Math.min(maximum, Math.max(1, Math.floor(Number(value))));
913
- }
914
-
915
- function encode_cursor(offset: number): string {
916
- return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
917
- }
918
-
919
- function decode_cursor(cursor: string | undefined): number {
920
- const value = String(cursor || "").trim();
921
- if (!value) return 0;
922
- try {
923
- const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as { offset?: unknown };
924
- return typeof parsed.offset === "number" && parsed.offset >= 0
925
- ? Math.floor(parsed.offset)
926
- : 0;
927
- } catch {
928
- throw new Error("Invalid Session Message cursor");
929
- }
930
- }
931
-
932
994
  function resolve_import_id(map: Map<string, string>, source_id: string, prefix: string): string {
933
995
  const existing = map.get(source_id);
934
996
  if (existing) return existing;
@@ -936,3 +998,8 @@ function resolve_import_id(map: Map<string, string>, source_id: string, prefix:
936
998
  map.set(source_id, created);
937
999
  return created;
938
1000
  }
1001
+
1002
+ /** 按真实 Message sequence 升序排序。 */
1003
+ function compare_message_sequence(left: SessionMessage, right: SessionMessage): number {
1004
+ return left.sequence - right.sequence;
1005
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * SessionRecorder canonical 上下文压缩。
2
+ * SessionRecorder Active/Segment 上下文压缩。
3
3
  *
4
- * 压缩只追加一条 internal summary Message,不删除、不归档、不重写 Mutation 日志。
4
+ * Compact Active 前缀写入不可变 Segment,并在 footer 保存累计 Summary。
5
5
  */
6
6
 
7
7
  import { generateText } from "ai";
@@ -13,44 +13,38 @@ import {
13
13
  build_update_session_compaction_prompt,
14
14
  SESSION_COMPACTION_SYSTEM_PROMPT,
15
15
  } from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
16
+ import { fold_compacted_text } from "@executor/core-engine/CoreEngineContextCompaction.js";
16
17
  import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
17
18
  import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
18
- import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
19
+ import type { SessionMessage } from "@/types/session/SessionMessage.js";
19
20
 
20
- /** 必要时把较早上下文追加为 canonical internal summary。 */
21
+ /** 单次摘要请求中 conversation 文本的最大字符数。 */
22
+ const SUMMARY_CHUNK_MAX_CHARS = 20_000;
23
+
24
+ /** 单条 canonical Message 投影到摘要输入后的最大字符数。 */
25
+ const SUMMARY_MESSAGE_MAX_CHARS = 12_000;
26
+
27
+ /** 上一轮累计 Summary 进入下一次 reduce 请求时的最大字符数。 */
28
+ const PREVIOUS_SUMMARY_MAX_CHARS = 12_000;
29
+
30
+ /** 摘要模型的最大输出 token,用于避免 Summary 自身无限增长。 */
31
+ const SUMMARY_MAX_OUTPUT_TOKENS = 4_000;
32
+
33
+ /** 必要时把 Active 前缀关闭为带累计 Summary 的 Segment。 */
21
34
  export async function compact_session_recorder_messages(input: {
22
35
  /** 当前 Session Recorder。 */
23
36
  recorder: SessionRecorder;
24
37
  /** 压缩策略和模型。 */
25
38
  compact_input: SessionHistoryCompactInput;
26
39
  }): Promise<{ compacted: boolean; reason?: string }> {
27
- const page = await input.recorder.list_messages({
28
- limit: 500,
29
- include_internal: true,
30
- });
31
- const context_messages = project_current_context(page.items);
32
- if (context_messages.length < 4) {
33
- return { compacted: false, reason: "small_messages" };
34
- }
35
- const system_text = input.compact_input.system
36
- .map((message) => String(message.content || ""))
37
- .join("\n\n");
38
- const estimated_tokens = Math.ceil(
39
- `${system_text}\n${JSON.stringify(context_messages)}`.length / 3,
40
+ const snapshot = await input.recorder.context_snapshot();
41
+ const context_messages = snapshot.messages.filter(
42
+ (message) => message.type === "user" || message.type === "assistant",
40
43
  );
41
- if (estimated_tokens <= input.compact_input.maxInputTokensApprox) {
42
- return { compacted: false, reason: "under_budget" };
44
+ if (!input.compact_input.force) {
45
+ return { compacted: false, reason: "not_requested" };
43
46
  }
44
-
45
- const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
46
- const compact_count = Math.max(
47
- 2,
48
- Math.min(context_messages.length - 1, Math.floor(context_messages.length * ratio)),
49
- );
50
- const older = context_messages.slice(0, compact_count);
51
- const boundary = [...older]
52
- .reverse()
53
- .find((message) => message.type !== "assistant" || message.kind !== "summary");
47
+ const boundary = context_messages.at(-1);
54
48
  if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
55
49
 
56
50
  const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
@@ -61,104 +55,133 @@ export async function compact_session_recorder_messages(input: {
61
55
  state: "running",
62
56
  });
63
57
 
64
- const previous_summary = older.find(
65
- (message): message is SessionAssistantMessage =>
66
- message.type === "assistant" && message.kind === "summary",
67
- );
68
- const conversation_text = older
69
- .filter((message) => message !== previous_summary)
58
+ const conversation_messages = context_messages
70
59
  .map((message) => message_to_compaction_text(message))
71
- .filter(Boolean)
72
- .join("\n")
73
- .slice(-24_000);
74
- const previous_summary_text = previous_summary
75
- ? assistant_text(previous_summary)
76
- : "";
77
- const prompt = previous_summary_text
78
- ? build_update_session_compaction_prompt({
79
- previous_summary: previous_summary_text,
80
- new_conversation_text: conversation_text || "(none)",
81
- })
82
- : build_initial_session_compaction_prompt({
83
- conversation_text: conversation_text || "(none)",
60
+ .filter(Boolean);
61
+ const chunks = build_summary_chunks(conversation_messages);
62
+ let summary = String(snapshot.summary?.text || "").trim();
63
+ let used_fallback = false;
64
+ for (const chunk of chunks) {
65
+ const prompt = summary
66
+ ? build_update_session_compaction_prompt({
67
+ previous_summary: fold_compacted_text(
68
+ summary,
69
+ PREVIOUS_SUMMARY_MAX_CHARS,
70
+ ),
71
+ new_conversation_text: chunk || "(none)",
72
+ })
73
+ : build_initial_session_compaction_prompt({
74
+ conversation_text: chunk || "(none)",
75
+ });
76
+ try {
77
+ const result = await generateText({
78
+ model: input.compact_input.model,
79
+ system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
80
+ prompt,
81
+ maxOutputTokens: SUMMARY_MAX_OUTPUT_TOKENS,
84
82
  });
85
-
86
- let summary = "";
87
- try {
88
- const result = await generateText({
89
- model: input.compact_input.model,
90
- system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
91
- prompt,
92
- });
93
- summary = String(result.text || "").trim();
94
- } catch {
95
- summary = "Earlier conversation was compacted because the context window was exceeded.";
83
+ const next_summary = String(result.text || "").trim();
84
+ if (!next_summary) throw new Error("Compaction model returned an empty Summary");
85
+ summary = next_summary;
86
+ } catch {
87
+ used_fallback = true;
88
+ summary = build_deterministic_summary(summary, chunk);
89
+ }
90
+ }
91
+ if (!summary) {
92
+ used_fallback = true;
93
+ summary = build_deterministic_summary("", "(no textual context)");
96
94
  }
97
95
 
98
- await input.recorder.append_completed_assistant_message({
99
- turn_id: `compact:${input.recorder.session_id}:${generateId()}`,
100
- kind: "summary",
101
- visibility: "internal",
102
- summary_through_message_id: boundary.message_id,
103
- parts: [{
104
- part_id: `summary-text:${generateId()}`,
105
- sequence: 1,
106
- type: "text",
96
+ await input.recorder.compact_active({
97
+ through_sequence: boundary.sequence,
98
+ summary: {
99
+ record_type: "summary",
100
+ session_id: input.recorder.session_id,
101
+ summary_id: `summary:${input.recorder.session_id}:${generateId()}`,
102
+ through_sequence: boundary.sequence,
107
103
  text: summary,
108
- state: "done",
109
- }],
104
+ created_at: Date.now(),
105
+ },
110
106
  });
111
107
  await publish_action(input.compact_input.onAction, {
112
108
  id: action_id,
113
109
  session_id: input.recorder.session_id,
114
110
  title: "Session messages compacted",
115
- description: `Compacted through Message ${boundary.message_id}.`,
111
+ description: used_fallback
112
+ ? `Closed Active through Message ${boundary.message_id} with deterministic fallback Summary.`
113
+ : `Closed Active through Message ${boundary.message_id}.`,
116
114
  state: "completed",
117
115
  });
118
116
  return { compacted: true };
119
117
  }
120
118
 
121
- function project_current_context(messages: SessionMessage[]): SessionMessage[] {
122
- const latest_summary = [...messages].reverse().find(
123
- (message): message is SessionAssistantMessage =>
124
- message.type === "assistant" &&
125
- message.kind === "summary" &&
126
- message.visibility === "internal",
127
- );
128
- if (!latest_summary?.summary_through_message_id) {
129
- return messages.filter(
130
- (message) => message.type === "user" || message.type === "assistant",
131
- );
132
- }
133
- const boundary_index = messages.findIndex(
134
- (message) => message.message_id === latest_summary.summary_through_message_id,
135
- );
136
- return [
137
- latest_summary,
138
- ...messages.slice(boundary_index + 1).filter(
139
- (message) =>
140
- message !== latest_summary &&
141
- (message.type === "user" ||
142
- message.type === "assistant" && message.kind !== "summary"),
143
- ),
144
- ];
145
- }
146
-
147
119
  function message_to_compaction_text(message: SessionMessage): string {
148
120
  const projected = to_executor_ui_message(message);
149
121
  if (!projected) return "";
150
- const text = projected.parts
151
- .flatMap((part) => part.type === "text" ? [String(part.text || "")] : [])
152
- .join("\n")
153
- .trim();
154
- return text ? `${message.type}: ${text}` : "";
122
+ const parts = projected.parts
123
+ .filter((part) => part.type !== "reasoning")
124
+ .map((part) => {
125
+ const serialized = safe_stringify(part);
126
+ return serialized.length <= SUMMARY_MESSAGE_MAX_CHARS
127
+ ? part
128
+ : {
129
+ type: part.type,
130
+ compacted_preview: fold_compacted_text(
131
+ serialized,
132
+ SUMMARY_MESSAGE_MAX_CHARS,
133
+ ),
134
+ };
135
+ });
136
+ return fold_compacted_text(safe_stringify({
137
+ role: projected.role,
138
+ parts,
139
+ }), SUMMARY_MESSAGE_MAX_CHARS);
140
+ }
141
+
142
+ /** 按 Message 边界把摘要输入拆为固定字符上限的 chunk。 */
143
+ function build_summary_chunks(messages: string[]): string[] {
144
+ const chunks: string[] = [];
145
+ let current = "";
146
+ for (const message of messages) {
147
+ const value = fold_compacted_text(message, SUMMARY_CHUNK_MAX_CHARS);
148
+ const candidate = current ? `${current}\n${value}` : value;
149
+ if (current && candidate.length > SUMMARY_CHUNK_MAX_CHARS) {
150
+ chunks.push(current);
151
+ current = value;
152
+ continue;
153
+ }
154
+ current = candidate;
155
+ }
156
+ if (current) chunks.push(current);
157
+ return chunks;
155
158
  }
156
159
 
157
- function assistant_text(message: SessionAssistantMessage): string {
158
- return message.parts
159
- .flatMap((part) => part.type === "text" ? [part.text] : [])
160
- .join("\n")
161
- .trim();
160
+ /** 摘要模型不可用时仍可完成归档的确定性 checkpoint。 */
161
+ function build_deterministic_summary(
162
+ previous_summary: string,
163
+ new_context: string,
164
+ ): string {
165
+ const body = [
166
+ String(previous_summary || "").trim(),
167
+ String(new_context || "").trim(),
168
+ ]
169
+ .filter(Boolean)
170
+ .join("\n\n");
171
+ return [
172
+ "## Compacted Context",
173
+ "The summary model was unavailable. The following deterministic checkpoint preserves the beginning and latest context.",
174
+ "",
175
+ fold_compacted_text(body || "(none)", PREVIOUS_SUMMARY_MAX_CHARS),
176
+ ].join("\n");
177
+ }
178
+
179
+ function safe_stringify(value: unknown): string {
180
+ try {
181
+ return JSON.stringify(value) || String(value || "");
182
+ } catch {
183
+ return String(value || "");
184
+ }
162
185
  }
163
186
 
164
187
  async function publish_action(
@@ -168,7 +191,7 @@ async function publish_action(
168
191
  session_id: string;
169
192
  title: string;
170
193
  description?: string;
171
- state: "running" | "completed";
194
+ state: "running" | "completed" | "failed";
172
195
  },
173
196
  ): Promise<void> {
174
197
  if (!callback) return;
@@ -2,7 +2,7 @@
2
2
  * SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
3
3
  *
4
4
  * 关键点(中文)
5
- * - Recorder 的 messages.jsonl 与 assistant_message.json 共同组成持久化事实源。
5
+ * - Recorder 的 active.jsonl、segments 与 assistant_message.json 共同组成持久化事实源。
6
6
  * - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
7
7
  * - 流式草稿写入由 AssistantMessageWriter 负责,Executor 的旧接口只提供投影兼容。
8
8
  */
@@ -111,11 +111,8 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
111
111
 
112
112
  /** 从 Recorder 当前快照投影运行中的 Assistant。 */
113
113
  async read_inflight(): Promise<SessionRecordV1 | null> {
114
- const page = await this.recorder.list_messages({
115
- limit: 500,
116
- include_internal: true,
117
- });
118
- const assistant = [...page.items]
114
+ const snapshot = await this.recorder.context_snapshot();
115
+ const assistant = [...snapshot.messages]
119
116
  .reverse()
120
117
  .find(
121
118
  (message) =>
@@ -132,15 +129,32 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
132
129
 
133
130
  /** 返回 Context Composer 使用的 UIMessage 投影。 */
134
131
  async list_records(): Promise<SessionRecordV1[]> {
135
- const page = await this.recorder.list_messages({
136
- limit: 500,
137
- include_internal: true,
138
- });
139
- const messages = project_context_messages(page.items);
140
- return messages.flatMap((message) => {
132
+ const snapshot = await this.recorder.context_snapshot();
133
+ const records: SessionRecordV1[] = [];
134
+ if (snapshot.summary) {
135
+ records.push({
136
+ id: snapshot.summary.summary_id,
137
+ role: "assistant",
138
+ metadata: {
139
+ v: 1,
140
+ ts: snapshot.summary.created_at,
141
+ sessionId: this.sessionId,
142
+ source: "compact",
143
+ kind: "summary",
144
+ extra: {
145
+ visibility: "internal",
146
+ summaryThroughSequence: snapshot.summary.through_sequence,
147
+ },
148
+ },
149
+ parts: [{ type: "text", text: snapshot.summary.text }],
150
+ });
151
+ }
152
+ for (const message of snapshot.messages) {
153
+ if (message.type !== "user" && message.type !== "assistant") continue;
141
154
  const projected = to_executor_ui_message(message);
142
- return projected ? [projected] : [];
143
- });
155
+ if (projected) records.push(projected);
156
+ }
157
+ return records;
144
158
  }
145
159
 
146
160
  /** 返回 Context Message 区间。 */
@@ -158,7 +172,7 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
158
172
  return {};
159
173
  }
160
174
 
161
- /** 通过 Recorder 追加 internal summary,不重写 Mutation 历史。 */
175
+ /** 通过 Recorder Active 前缀关闭为带累计 Summary Segment。 */
162
176
  async compact(input: SessionHistoryCompactInput): Promise<{
163
177
  compacted: boolean;
164
178
  reason?: string;
@@ -237,40 +251,6 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
237
251
  }
238
252
  }
239
253
 
240
- function project_context_messages(messages: import("@/types/session/SessionMessage.js").SessionMessage[]) {
241
- const latest_summary = [...messages]
242
- .reverse()
243
- .find(
244
- (message) =>
245
- message.type === "assistant" &&
246
- message.kind === "summary" &&
247
- message.visibility === "internal",
248
- );
249
- if (
250
- !latest_summary ||
251
- latest_summary.type !== "assistant" ||
252
- !latest_summary.summary_through_message_id
253
- ) {
254
- return messages.filter(
255
- (message) => message.type === "user" || message.type === "assistant",
256
- );
257
- }
258
- const boundary_index = messages.findIndex(
259
- (message) => message.message_id === latest_summary.summary_through_message_id,
260
- );
261
- return [
262
- latest_summary,
263
- ...messages
264
- .slice(boundary_index + 1)
265
- .filter(
266
- (message) =>
267
- message !== latest_summary &&
268
- message.type === "user" ||
269
- (message.type === "assistant" && message.kind !== "summary"),
270
- ),
271
- ];
272
- }
273
-
274
254
  function infer_action_type(message_id: string): string {
275
255
  return String(message_id || "").split(":")[0] || "action";
276
256
  }