@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
@@ -16,6 +16,7 @@ import type {
16
16
  import type { SessionMutation } from "@/types/session/SessionMutation.js";
17
17
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
18
18
  import type { AgentSessionStopResult } from "@/types/sdk/AgentSessionStop.js";
19
+ import type { SessionQueueCommand } from "@/types/session/SessionQueueCommand.js";
19
20
  import type {
20
21
  AgentSessionTurnHandle,
21
22
  AgentSessionTurnResult,
@@ -25,7 +26,10 @@ const TURN_STOPPED_MESSAGE = "Turn stopped";
25
26
  const QUEUED_PROMPT_CANCELLED_MESSAGE =
26
27
  "Prompt cancelled because session was stopped";
27
28
 
28
- type QueuedPrompt = {
29
+ type SessionQueuePrompt = {
30
+ /** 当前队列项固定为 prompt。 */
31
+ type: "prompt";
32
+
29
33
  /**
30
34
  * 当前排队中的 prompt 输入。
31
35
  */
@@ -37,6 +41,11 @@ type QueuedPrompt = {
37
41
  deferredHandle: Deferred<AgentSessionTurnHandle>;
38
42
  };
39
43
 
44
+ /**
45
+ * Session queue 的统一有序输入。
46
+ */
47
+ type SessionQueueInput = SessionQueuePrompt | SessionQueueCommand;
48
+
40
49
  /**
41
50
  * Promise 延迟控制器。
42
51
  */
@@ -139,7 +148,7 @@ export class SessionPromptRuntime {
139
148
  private readonly appendErrorMessage: SessionPromptRuntimeOptions["appendErrorMessage"];
140
149
  private readonly executeTurn: SessionPromptRuntimeOptions["executeTurn"];
141
150
  private readonly stopTurn: SessionPromptRuntimeOptions["stopTurn"];
142
- private readonly queue: QueuedPrompt[] = [];
151
+ private readonly queue: SessionQueueInput[] = [];
143
152
  private processingPromise: Promise<void> | null = null;
144
153
  private activeTurn: ActiveTurnState | null = null;
145
154
 
@@ -161,6 +170,7 @@ export class SessionPromptRuntime {
161
170
  prompt(input: AgentSessionPromptInput): Promise<AgentSessionTurnHandle> {
162
171
  const deferredHandle = createDeferred<AgentSessionTurnHandle>();
163
172
  this.queue.push({
173
+ type: "prompt",
164
174
  input,
165
175
  deferredHandle,
166
176
  });
@@ -168,6 +178,27 @@ export class SessionPromptRuntime {
168
178
  return deferredHandle.promise;
169
179
  }
170
180
 
181
+ /**
182
+ * 把一次已成功创建的 command 加入统一输入队列。
183
+ */
184
+ enqueue_command(command: SessionQueueCommand): void {
185
+ this.queue.push(command);
186
+ }
187
+
188
+ /**
189
+ * 判断是否存在等待并入下一 Session step 的 prompt。
190
+ */
191
+ has_pending_prompt(): boolean {
192
+ return this.queue.some((item) => item.type === "prompt");
193
+ }
194
+
195
+ /**
196
+ * 判断是否存在等待在下一 Session step 检查点执行的 command。
197
+ */
198
+ has_pending_command(): boolean {
199
+ return this.queue.some((item) => item.type === "command");
200
+ }
201
+
171
202
  /**
172
203
  * 返回当前 actor prompt 调度器是否仍处于活跃态。
173
204
  *
@@ -176,7 +207,7 @@ export class SessionPromptRuntime {
176
207
  * - Session 会用它阻止内部 direct execution 与 actor 模式并发混用。
177
208
  */
178
209
  isActive(): boolean {
179
- return this.processingPromise !== null || this.queue.length > 0;
210
+ return this.processingPromise !== null || this.has_pending_prompt();
180
211
  }
181
212
 
182
213
  /**
@@ -211,16 +242,19 @@ export class SessionPromptRuntime {
211
242
  if (this.processingPromise) return;
212
243
  this.processingPromise = this.processLoop().finally(() => {
213
244
  this.processingPromise = null;
214
- if (this.queue.length > 0) {
245
+ if (this.has_pending_prompt()) {
215
246
  this.ensureProcessing();
216
247
  }
217
248
  });
218
249
  }
219
250
 
220
251
  private async processLoop(): Promise<void> {
221
- while (this.queue.length > 0) {
252
+ while (this.has_pending_prompt()) {
253
+ const prompt_index = this.queue.findIndex((item) => item.type === "prompt");
254
+ if (prompt_index < 0) break;
255
+ const queued_before_prompt = this.queue.splice(0, prompt_index);
222
256
  const current = this.queue.shift();
223
- if (!current) break;
257
+ if (!current || current.type !== "prompt") break;
224
258
 
225
259
  const turnId = `turn:${this.sessionId}:${Date.now()}:${nanoid(6)}`;
226
260
  const activeTurn = createActiveTurnState(turnId);
@@ -237,12 +271,18 @@ export class SessionPromptRuntime {
237
271
  current.deferredHandle.resolve(createTurnHandle(activeTurn));
238
272
 
239
273
  try {
274
+ await this.execute_commands(
275
+ queued_before_prompt.filter(
276
+ (item): item is SessionQueueCommand => item.type === "command",
277
+ ),
278
+ activeTurn,
279
+ );
240
280
  await this.createAndPersistUserMessage(current.input, turnId, "prompt");
241
281
  const result = await this.executeTurn({
242
282
  turnId,
243
283
  promptInput: current.input,
244
284
  onStepMerge: async () => {
245
- return await this.drainQueuedPromptsAsMessages(activeTurn);
285
+ return await this.drain_queued_inputs(activeTurn);
246
286
  },
247
287
  abortSignal: activeTurn.abortController.signal,
248
288
  });
@@ -317,8 +357,12 @@ export class SessionPromptRuntime {
317
357
  }
318
358
 
319
359
  private cancelQueuedPrompts(): number {
320
- if (this.queue.length <= 0) return 0;
321
- const cancelled = this.queue.splice(0, this.queue.length);
360
+ const cancelled = this.queue.filter(
361
+ (item): item is SessionQueuePrompt => item.type === "prompt",
362
+ );
363
+ if (cancelled.length <= 0) return 0;
364
+ const retained = this.queue.filter((item) => item.type === "command");
365
+ this.queue.splice(0, this.queue.length, ...retained);
322
366
  for (const item of cancelled) {
323
367
  const turnId = `turn:${this.sessionId}:cancelled:${Date.now()}:${nanoid(6)}`;
324
368
  const cancelledTurn = createActiveTurnState(turnId);
@@ -359,7 +403,10 @@ export class SessionPromptRuntime {
359
403
  return cancelled.length;
360
404
  }
361
405
 
362
- private async drainQueuedPromptsAsMessages(
406
+ /**
407
+ * 在下一 Session step 检查点按入队顺序提交配置并持久化 steer。
408
+ */
409
+ private async drain_queued_inputs(
363
410
  activeTurn: ActiveTurnState,
364
411
  ): Promise<SessionUserMessageV1[]> {
365
412
  if (this.queue.length <= 0) return [];
@@ -368,6 +415,16 @@ export class SessionPromptRuntime {
368
415
 
369
416
  for (let index = 0; index < drained.length; index += 1) {
370
417
  const item = drained[index];
418
+ if (item.type === "command") {
419
+ try {
420
+ await item.execute({
421
+ turn_id: activeTurn.turnId,
422
+ });
423
+ } catch {
424
+ // command 自己负责失败观测;单条失败不能吞掉后续 steer 或 command。
425
+ }
426
+ continue;
427
+ }
371
428
  try {
372
429
  const message = await this.createAndPersistUserMessage(
373
430
  item.input,
@@ -386,6 +443,25 @@ export class SessionPromptRuntime {
386
443
 
387
444
  return merged;
388
445
  }
446
+
447
+ /**
448
+ * 执行一组已经从统一队列截取的 command。
449
+ */
450
+ private async execute_commands(
451
+ commands: SessionQueueCommand[],
452
+ active_turn: ActiveTurnState,
453
+ ): Promise<void> {
454
+ if (commands.length <= 0) return;
455
+ for (const command of commands) {
456
+ try {
457
+ await command.execute({
458
+ turn_id: active_turn.turnId,
459
+ });
460
+ } catch {
461
+ // command 自己负责失败观测;单条失败不能阻断当前 prompt。
462
+ }
463
+ }
464
+ }
389
465
  }
390
466
 
391
467
  function buildPromptRuntimeErrorAssistantMessage(input: {
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  inferAgentModelLabel,
12
12
  normalizeAgentModel,
13
+ read_agent_model_context_window,
13
14
  } from "@/model/CityModelAdapter.js";
14
15
  import {
15
16
  patchSessionModelLabel,
@@ -36,6 +37,7 @@ import type {
36
37
  SessionUserMessageV1,
37
38
  } from "@/executor/types/SessionRecords.js";
38
39
  import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
40
+ import type { SessionQueueCommand } from "@/types/session/SessionQueueCommand.js";
39
41
  import { generateId } from "@/utils/Id.js";
40
42
  import type { Logger } from "@/utils/logger/Logger.js";
41
43
  import type { AgentModel } from "@/model/CityModelAdapter.js";
@@ -46,8 +48,6 @@ import {
46
48
  from_ui_user_parts,
47
49
  to_executor_ui_message,
48
50
  } from "@/session/recorder/SessionMessageCodec.js";
49
- import fs from "fs-extra";
50
- import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
51
51
  import type { SessionMessage } from "@/types/session/SessionMessage.js";
52
52
 
53
53
  type SessionStateServiceOptions = {
@@ -116,6 +116,20 @@ type SessionSetOptions = {
116
116
 
117
117
  type EmitActionInput = SessionActionRecordInputV1 | SessionActionRecordV1;
118
118
 
119
+ /**
120
+ * Session 配置成功写入后的队列提交结果。
121
+ */
122
+ export interface SessionConfiguredCommandResult {
123
+ /**
124
+ * 等待在下一 Session step 检查点执行的 command。
125
+ *
126
+ * 说明(中文)
127
+ * - 输入未产生实际配置变化时为空。
128
+ * - fork 等内部初始化路径可以选择立即提交而不返回 command。
129
+ */
130
+ command?: SessionQueueCommand;
131
+ }
132
+
119
133
  /**
120
134
  * 本地 Session 状态与持久化服务。
121
135
  */
@@ -209,6 +223,9 @@ export class SessionStateService {
209
223
  ...(metadata.modelId ? { modelId: metadata.modelId } : {}),
210
224
  };
211
225
  await this.restore_persisted_model();
226
+ this.state.effective_session_config = {
227
+ ...this.state.sessionConfig,
228
+ };
212
229
  })();
213
230
  await this.state.initializePromise;
214
231
  }
@@ -247,7 +264,10 @@ export class SessionStateService {
247
264
  /**
248
265
  * 写入当前 session 配置。
249
266
  */
250
- async set(input: AgentSessionSetInput, options?: SessionSetOptions): Promise<void> {
267
+ async set(
268
+ input: AgentSessionSetInput,
269
+ options?: SessionSetOptions,
270
+ ): Promise<SessionConfiguredCommandResult> {
251
271
  const should_emit_action = options?.emit_action !== false;
252
272
  const previous_model_label = this.state.sessionConfig.modelLabel;
253
273
  const previous_model_id = String(
@@ -273,47 +293,59 @@ export class SessionStateService {
273
293
  const next_model_name = next_model_label || next_model_id;
274
294
  const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
275
295
 
276
- if (should_emit_model_switch_action) {
277
- await this.emit_action_event({
278
- id: action_id,
279
- title: `Switching session model from ${previous_model_name} to ${next_model_name}`,
280
- state: "running",
281
- });
282
- }
283
-
284
296
  try {
297
+ const next_config: AgentSessionConfigSnapshot = {
298
+ ...this.state.sessionConfig,
299
+ };
285
300
  if (next_model) {
286
- this.state.sessionConfig.model = normalizeAgentModel(next_model);
287
- this.state.sessionConfig.modelLabel = next_model_label;
301
+ next_config.model = normalizeAgentModel(next_model);
302
+ next_config.modelLabel = next_model_label;
303
+ next_config.model_context_window =
304
+ read_agent_model_context_window(next_model);
288
305
  }
289
306
  if (next_model_id) {
290
- this.state.sessionConfig.modelId = next_model_id;
307
+ next_config.modelId = next_model_id;
291
308
  }
292
309
  await patchSessionModelLabel({
293
310
  projectRoot: this.project_root,
294
311
  agentId: this.agent_id,
295
312
  sessionId: this.session_id,
296
- model: this.state.sessionConfig.model,
297
- modelId: this.state.sessionConfig.modelId,
313
+ model: next_config.model,
314
+ modelId: next_config.modelId,
298
315
  });
299
- } catch (error) {
300
- if (should_emit_model_switch_action) {
301
- await this.emit_action_event({
316
+ this.state.sessionConfig = next_config;
317
+
318
+ const apply_effective_config = async (turn_id?: string): Promise<void> => {
319
+ this.state.effective_session_config = {
320
+ ...next_config,
321
+ };
322
+ if (!should_emit_model_switch_action) return;
323
+ await this.emit_config_action_event({
302
324
  id: action_id,
303
- title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
304
- description: error instanceof Error ? error.message : String(error),
305
- state: "failed",
325
+ title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
326
+ state: "completed",
327
+ ...(turn_id ? { turnId: turn_id } : {}),
306
328
  });
329
+ };
330
+
331
+ if (options?.emit_action === false) {
332
+ await apply_effective_config();
333
+ return {};
307
334
  }
308
- throw error;
309
- }
310
335
 
311
- if (should_emit_model_switch_action) {
312
- await this.emit_action_event({
313
- id: action_id,
314
- title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
315
- state: "completed",
316
- });
336
+ const command_id = generateId();
337
+ return {
338
+ command: {
339
+ type: "command",
340
+ command_id,
341
+ scope: "session",
342
+ execute: async ({ turn_id }) => {
343
+ await apply_effective_config(turn_id);
344
+ },
345
+ },
346
+ };
347
+ } catch (error) {
348
+ throw error;
317
349
  }
318
350
  }
319
351
 
@@ -324,6 +356,8 @@ export class SessionStateService {
324
356
  const model = await this.resolve_model_by_id(model_id);
325
357
  this.state.sessionConfig.model = normalizeAgentModel(model);
326
358
  this.state.sessionConfig.modelLabel = inferAgentModelLabel(model);
359
+ this.state.sessionConfig.model_context_window =
360
+ read_agent_model_context_window(model);
327
361
  }
328
362
 
329
363
  /** 使用宿主 resolver 解析稳定模型 ID。 */
@@ -384,28 +418,17 @@ export class SessionStateService {
384
418
  * 仅刷新当前 session metadata。
385
419
  */
386
420
  async touch_metadata(): Promise<void> {
387
- const message_page = await this.recorder.list_messages({
388
- limit: 500,
389
- include_internal: true,
390
- });
391
- const messages_path = getSdkAgentSessionMessagesPath(
392
- this.project_root,
393
- this.agent_id,
394
- this.session_id,
395
- );
396
- const history_bytes = await fs.stat(messages_path)
397
- .then((file_stat) => file_stat.size)
398
- .catch(() => 0);
421
+ const stats = await this.recorder.storage_stats();
399
422
  const preview_text = resolve_message_preview(
400
- message_page.items[message_page.items.length - 1],
423
+ stats.latest_message || undefined,
401
424
  ).slice(0, 180);
402
425
  await touchSessionMetadata({
403
426
  projectRoot: this.project_root,
404
427
  agentId: this.agent_id,
405
428
  sessionId: this.session_id,
406
429
  sessionConfig: this.state.sessionConfig,
407
- message_count: message_page.total,
408
- history_bytes,
430
+ message_count: stats.message_count,
431
+ history_bytes: stats.history_bytes,
409
432
  ...(preview_text ? { preview_text } : {}),
410
433
  });
411
434
  }
@@ -512,6 +535,27 @@ export class SessionStateService {
512
535
  });
513
536
  }
514
537
 
538
+ /**
539
+ * 尽力写入配置生效 action,不让 timeline 故障改变 effective state。
540
+ */
541
+ async emit_config_action_event(input: EmitActionInput): Promise<boolean> {
542
+ try {
543
+ await this.emit_action_event(input);
544
+ return true;
545
+ } catch (error) {
546
+ try {
547
+ await this.logger.log("warn", "[agent] config action persistence failed", {
548
+ sessionId: this.session_id,
549
+ actionId: String(input.id || ""),
550
+ error: error instanceof Error ? error.message : String(error),
551
+ });
552
+ } catch {
553
+ // 配置已经提交,日志失败也不能反向改变 effective state。
554
+ }
555
+ return false;
556
+ }
557
+ }
558
+
515
559
  /**
516
560
  * 构造并持久化一条 prompt user 消息。
517
561
  */
@@ -14,6 +14,7 @@ import { isAgentSessionPromptInputEmpty } from "@/types/sdk/AgentSessionPrompt.j
14
14
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
15
15
  import type { AgentSessionStopResult } from "@/types/sdk/AgentSessionStop.js";
16
16
  import type { AgentSessionTurnHandle } from "@/types/sdk/AgentSessionTurn.js";
17
+ import type { SessionRunResult } from "@/executor/types/SessionRun.js";
17
18
  import type {
18
19
  SessionRecordV1,
19
20
  SessionMessageRecordV1,
@@ -22,12 +23,14 @@ import type {
22
23
  import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
23
24
  import { SessionPromptRuntime } from "@/session/runtime/SessionPromptRuntime.js";
24
25
  import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
26
+ import type { SessionQueueCommand } from "@/types/session/SessionQueueCommand.js";
25
27
  import { SessionStateService } from "@/session/services/SessionStateService.js";
26
28
  import {
27
29
  SessionAssistantMessageWriter,
28
30
  SessionRecorder,
29
31
  } from "@/session/recorder/SessionRecorder.js";
30
32
  import { from_ui_assistant_parts } from "@/session/recorder/SessionMessageCodec.js";
33
+ import { generateId } from "@/utils/Id.js";
31
34
 
32
35
  type SessionTurnServiceOptions = {
33
36
  /**
@@ -70,6 +73,8 @@ export class SessionTurnService {
70
73
  private readonly event_hub: SessionEventHub;
71
74
  private readonly recorder: SessionRecorder;
72
75
  private readonly prompt_runtime: SessionPromptRuntime;
76
+ private active_run_context: SessionRunContext | null = null;
77
+ private request_active_history_reload: (() => void) | null = null;
73
78
 
74
79
  constructor(options: SessionTurnServiceOptions) {
75
80
  this.session_id = options.session_id;
@@ -136,6 +141,52 @@ export class SessionTurnService {
136
141
  return await this.prompt_runtime.prompt(input);
137
142
  }
138
143
 
144
+ /**
145
+ * 把 configured state command 加入当前 Session 的统一输入队列。
146
+ */
147
+ enqueue_command(command: SessionQueueCommand): void {
148
+ this.prompt_runtime.enqueue_command(command);
149
+ }
150
+
151
+ /**
152
+ * 把一次显式历史压缩加入当前 Session 的统一输入队列。
153
+ */
154
+ async compact(): Promise<void> {
155
+ await this.state_service.ensure_runnable();
156
+ const command_id = generateId();
157
+ this.enqueue_command({
158
+ type: "command",
159
+ command_id,
160
+ scope: "session",
161
+ execute: async ({ turn_id }) => {
162
+ const active_run_context = this.active_run_context;
163
+ const run_context = active_run_context ||
164
+ this.create_compaction_run_context(turn_id);
165
+ const result = await this.executor.compact_history(run_context);
166
+ if (
167
+ result.compacted &&
168
+ active_run_context !== null &&
169
+ active_run_context === this.active_run_context
170
+ ) {
171
+ this.request_active_history_reload?.();
172
+ }
173
+ await this.state_service.touch_metadata();
174
+ if (result.compacted) {
175
+ return;
176
+ }
177
+ if (!result.compacted && result.reason === "nothing_to_compact") {
178
+ await this.state_service.emit_action_event({
179
+ id: `compacting:${this.session_id}:${command_id}`,
180
+ title: "Session records already compact",
181
+ description: "The Session has no active records to compact.",
182
+ state: "completed",
183
+ turnId: turn_id,
184
+ });
185
+ }
186
+ },
187
+ });
188
+ }
189
+
139
190
  /**
140
191
  * 停止当前 turn,并取消尚未被吸收的排队 prompt。
141
192
  */
@@ -161,6 +212,7 @@ export class SessionTurnService {
161
212
  current: SessionAssistantMessageWriter | null;
162
213
  } = { current: null };
163
214
  let assistant_segment_index = 0;
215
+ let history_reload_requested = false;
164
216
  const ensure_assistant_writer = async (): Promise<SessionAssistantMessageWriter> => {
165
217
  if (assistant_writer_ref.current) return assistant_writer_ref.current;
166
218
  assistant_segment_index += 1;
@@ -175,6 +227,13 @@ export class SessionTurnService {
175
227
  sessionId: this.session_id,
176
228
  projectRoot: this.project_root,
177
229
  onStepCallback: async () => {
230
+ if (
231
+ this.prompt_runtime.has_pending_command() &&
232
+ assistant_writer_ref.current
233
+ ) {
234
+ await assistant_writer_ref.current.complete();
235
+ assistant_writer_ref.current = null;
236
+ }
178
237
  const merged = await input.onStepMerge();
179
238
  if (merged.length > 0 && assistant_writer_ref.current) {
180
239
  await assistant_writer_ref.current.complete();
@@ -182,6 +241,12 @@ export class SessionTurnService {
182
241
  }
183
242
  return merged;
184
243
  },
244
+ hasPendingStepInput: () => this.prompt_runtime.has_pending_prompt(),
245
+ consume_history_reload: () => {
246
+ const requested = history_reload_requested;
247
+ history_reload_requested = false;
248
+ return requested;
249
+ },
185
250
  onUiMessageChunkCallback: async (chunk) => {
186
251
  if (!is_assistant_content_chunk(chunk.type)) return;
187
252
  const writer = await ensure_assistant_writer();
@@ -197,12 +262,32 @@ export class SessionTurnService {
197
262
  };
198
263
  const query = input.promptInput.query;
199
264
  const executor_query = typeof query === "string" ? query : extractTextFromParts(query);
200
- const result = await this.executor.run({
201
- query: executor_query,
202
- runContext: run_context,
203
- });
265
+ this.active_run_context = run_context;
266
+ this.request_active_history_reload = () => {
267
+ history_reload_requested = true;
268
+ };
269
+ let result: SessionRunResult;
270
+ try {
271
+ result = await this.executor.run({
272
+ query: executor_query,
273
+ runContext: run_context,
274
+ });
275
+ } finally {
276
+ if (this.active_run_context === run_context) {
277
+ this.active_run_context = null;
278
+ this.request_active_history_reload = null;
279
+ }
280
+ }
281
+ const final_assistant_parts = result.assistantMessage
282
+ ? from_ui_assistant_parts(result.assistantMessage.parts)
283
+ : [];
204
284
  const final_assistant_writer = assistant_writer_ref.current;
205
285
  if (final_assistant_writer) {
286
+ for (const part of final_assistant_parts) {
287
+ if (part.type === "file") {
288
+ await final_assistant_writer.append_file_part(part);
289
+ }
290
+ }
206
291
  if (input.abortSignal?.aborted) {
207
292
  await final_assistant_writer.stop();
208
293
  } else if (result.success) {
@@ -211,14 +296,13 @@ export class SessionTurnService {
211
296
  await final_assistant_writer.fail(result.error);
212
297
  }
213
298
  } else if (result.assistantMessage) {
214
- const parts = from_ui_assistant_parts(result.assistantMessage.parts);
215
- if (parts.length > 0) {
299
+ if (final_assistant_parts.length > 0) {
216
300
  assistant_segment_index += 1;
217
301
  const fallback_writer = await this.recorder.open_assistant_message({
218
302
  turn_id: input.turnId,
219
303
  segment_index: assistant_segment_index,
220
304
  });
221
- for (const part of parts) await fallback_writer.upsert_part(part);
305
+ for (const part of final_assistant_parts) await fallback_writer.upsert_part(part);
222
306
  if (result.success) await fallback_writer.complete();
223
307
  else await fallback_writer.fail(result.error);
224
308
  }
@@ -236,6 +320,10 @@ export class SessionTurnService {
236
320
  await this.state_service.persist_deferred_user_messages(
237
321
  result.deferredPersistedUserMessages,
238
322
  );
323
+ if (result.compact_required) {
324
+ await this.executor.compact_history(run_context);
325
+ await this.state_service.touch_metadata();
326
+ }
239
327
  return {
240
328
  text: result.assistantMessage
241
329
  ? extractTextFromUiMessage(result.assistantMessage)
@@ -247,6 +335,23 @@ export class SessionTurnService {
247
335
  ...(result.error ? { error: result.error } : {}),
248
336
  };
249
337
  }
338
+
339
+ /**
340
+ * 为 turn 开始前执行的 compact command 构造最小运行上下文。
341
+ */
342
+ private create_compaction_run_context(turn_id: string): SessionRunContext {
343
+ return {
344
+ turnId: turn_id,
345
+ sessionId: this.session_id,
346
+ projectRoot: this.project_root,
347
+ onActionCallback: async (event) => {
348
+ await this.state_service.persist_action_event(event);
349
+ },
350
+ injectedUserMessages: [],
351
+ deferredPersistedUserMessages: [],
352
+ pendingAssistantFileParts: [],
353
+ };
354
+ }
250
355
  }
251
356
 
252
357
  function is_assistant_content_chunk(type: string): boolean {