@downcity/agent 1.1.228 → 1.1.232

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 (157) 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/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  10. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +0 -7
  11. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  12. package/bin/executor/Executor.d.ts +25 -0
  13. package/bin/executor/Executor.d.ts.map +1 -1
  14. package/bin/executor/Executor.js +36 -0
  15. package/bin/executor/Executor.js.map +1 -1
  16. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +4 -0
  17. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  18. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -1
  19. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  20. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +15 -11
  21. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  22. package/bin/executor/core-engine/CoreEngineRunner.d.ts +11 -0
  23. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  24. package/bin/executor/core-engine/CoreEngineRunner.js +12 -8
  25. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  26. package/bin/executor/store/history/SessionHistoryStore.d.ts +3 -13
  27. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  28. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -1
  29. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  30. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  31. package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
  32. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  33. package/bin/index.d.ts +2 -1
  34. package/bin/index.d.ts.map +1 -1
  35. package/bin/index.js +1 -1
  36. package/bin/index.js.map +1 -1
  37. package/bin/model/CityModelAdapter.d.ts +6 -0
  38. package/bin/model/CityModelAdapter.d.ts.map +1 -1
  39. package/bin/model/CityModelAdapter.js +13 -0
  40. package/bin/model/CityModelAdapter.js.map +1 -1
  41. package/bin/plugin/core/PluginRegistry.d.ts +22 -1
  42. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  43. package/bin/plugin/core/PluginRegistry.js +46 -4
  44. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  45. package/bin/session/Session.d.ts +11 -1
  46. package/bin/session/Session.d.ts.map +1 -1
  47. package/bin/session/Session.js +76 -11
  48. package/bin/session/Session.js.map +1 -1
  49. package/bin/session/browse/Browse.d.ts +0 -8
  50. package/bin/session/browse/Browse.d.ts.map +1 -1
  51. package/bin/session/browse/Browse.js +72 -57
  52. package/bin/session/browse/Browse.js.map +1 -1
  53. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
  54. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  55. package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
  56. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  57. package/bin/session/recorder/SessionRecorder.d.ts +20 -2
  58. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  59. package/bin/session/recorder/SessionRecorder.js +91 -46
  60. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  61. package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
  62. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  63. package/bin/session/recorder/SessionRecorderCompaction.js +50 -62
  64. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  65. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
  66. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  67. package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
  68. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  69. package/bin/session/runtime/SessionPromptRuntime.d.ts +17 -1
  70. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  71. package/bin/session/runtime/SessionPromptRuntime.js +67 -8
  72. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  73. package/bin/session/services/SessionStateService.d.ts +15 -1
  74. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  75. package/bin/session/services/SessionStateService.js +60 -39
  76. package/bin/session/services/SessionStateService.js.map +1 -1
  77. package/bin/session/services/SessionTurnService.d.ts +5 -0
  78. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  79. package/bin/session/services/SessionTurnService.js +17 -3
  80. package/bin/session/services/SessionTurnService.js.map +1 -1
  81. package/bin/session/services/SessionViewService.d.ts +1 -5
  82. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  83. package/bin/session/services/SessionViewService.js +4 -34
  84. package/bin/session/services/SessionViewService.js.map +1 -1
  85. package/bin/session/storage/Paths.d.ts +3 -15
  86. package/bin/session/storage/Paths.d.ts.map +1 -1
  87. package/bin/session/storage/Paths.js +5 -21
  88. package/bin/session/storage/Paths.js.map +1 -1
  89. package/bin/types/agent/SessionTypes.d.ts +2 -24
  90. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  91. package/bin/types/executor/SessionRunContext.d.ts +21 -0
  92. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  93. package/bin/types/plugin/PluginRuntime.d.ts +25 -0
  94. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  95. package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
  96. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  97. package/bin/types/runtime/agent/AgentContext.js +29 -6
  98. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  99. package/bin/types/session/SessionConfigMutation.d.ts +68 -0
  100. package/bin/types/session/SessionConfigMutation.d.ts.map +1 -0
  101. package/bin/types/session/SessionConfigMutation.js +10 -0
  102. package/bin/types/session/SessionConfigMutation.js.map +1 -0
  103. package/bin/types/session/SessionLocalState.d.ts +8 -0
  104. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  105. package/bin/types/session/SessionMessage.d.ts +16 -12
  106. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  107. package/bin/types/session/SessionMutation.d.ts +1 -1
  108. package/bin/types/session/SessionMutation.js +1 -1
  109. package/bin/types/session/SessionOptions.d.ts +16 -0
  110. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  111. package/bin/types/session/SessionSegment.d.ts +57 -0
  112. package/bin/types/session/SessionSegment.d.ts.map +1 -0
  113. package/bin/types/session/SessionSegment.js +8 -0
  114. package/bin/types/session/SessionSegment.js.map +1 -0
  115. package/package.json +3 -3
  116. package/scripts/city-model-tool-loop.test.mjs +32 -0
  117. package/scripts/session-action-message.test.mjs +59 -13
  118. package/scripts/session-compaction-context-window.test.mjs +95 -0
  119. package/scripts/session-config-turn-boundary.test.mjs +224 -0
  120. package/scripts/session-list-title.test.mjs +21 -3
  121. package/scripts/session-prompt-runtime.test.mjs +85 -0
  122. package/scripts/session-recorder.test.mjs +340 -11
  123. package/src/agent/local/Agent.ts +34 -3
  124. package/src/agent/local/services/AgentSessions.ts +61 -0
  125. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +0 -5
  126. package/src/executor/Executor.ts +62 -0
  127. package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -0
  128. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +16 -14
  129. package/src/executor/core-engine/CoreEngineRunner.ts +24 -8
  130. package/src/executor/store/history/SessionHistoryStore.ts +3 -14
  131. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -1
  132. package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
  133. package/src/index.ts +8 -0
  134. package/src/model/CityModelAdapter.ts +15 -0
  135. package/src/plugin/core/PluginRegistry.ts +72 -4
  136. package/src/session/Session.ts +89 -11
  137. package/src/session/browse/Browse.ts +73 -69
  138. package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
  139. package/src/session/recorder/SessionRecorder.ts +116 -49
  140. package/src/session/recorder/SessionRecorderCompaction.ts +55 -80
  141. package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
  142. package/src/session/runtime/SessionPromptRuntime.ts +100 -9
  143. package/src/session/services/SessionStateService.ts +79 -44
  144. package/src/session/services/SessionTurnService.ts +19 -3
  145. package/src/session/services/SessionViewService.ts +4 -50
  146. package/src/session/storage/Paths.ts +5 -38
  147. package/src/types/agent/SessionTypes.ts +2 -24
  148. package/src/types/executor/SessionRunContext.ts +25 -0
  149. package/src/types/plugin/PluginRuntime.ts +27 -0
  150. package/src/types/runtime/agent/AgentContext.ts +32 -7
  151. package/src/types/session/SessionConfigMutation.ts +78 -0
  152. package/src/types/session/SessionLocalState.ts +9 -0
  153. package/src/types/session/SessionMessage.ts +16 -12
  154. package/src/types/session/SessionMutation.ts +1 -1
  155. package/src/types/session/SessionOptions.ts +21 -0
  156. package/src/types/session/SessionSegment.ts +62 -0
  157. 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 { SessionRuntimeConfigMutation } from "@/types/session/SessionConfigMutation.js";
19
20
  import type {
20
21
  AgentSessionTurnHandle,
21
22
  AgentSessionTurnResult,
@@ -26,6 +27,9 @@ const QUEUED_PROMPT_CANCELLED_MESSAGE =
26
27
  "Prompt cancelled because session was stopped";
27
28
 
28
29
  type QueuedPrompt = {
30
+ /** 当前队列项固定为 prompt。 */
31
+ type: "prompt";
32
+
29
33
  /**
30
34
  * 当前排队中的 prompt 输入。
31
35
  */
@@ -37,6 +41,22 @@ type QueuedPrompt = {
37
41
  deferredHandle: Deferred<AgentSessionTurnHandle>;
38
42
  };
39
43
 
44
+ /**
45
+ * 等待在模型 turn 边界提交的配置队列项。
46
+ */
47
+ type QueuedConfigMutation = {
48
+ /** 当前队列项固定为配置 mutation。 */
49
+ type: "config";
50
+
51
+ /** 当前待提交的配置 mutation。 */
52
+ mutation: SessionRuntimeConfigMutation;
53
+ };
54
+
55
+ /**
56
+ * Session actor 的统一有序输入。
57
+ */
58
+ type QueuedSessionInput = QueuedPrompt | QueuedConfigMutation;
59
+
40
60
  /**
41
61
  * Promise 延迟控制器。
42
62
  */
@@ -75,6 +95,9 @@ interface ActiveTurnState {
75
95
  * 当前 turn 的取消控制器。
76
96
  */
77
97
  abortController: AbortController;
98
+
99
+ /** 当前公开 turn 已开始的模型 turn 数量。 */
100
+ model_turn_index: number;
78
101
  }
79
102
 
80
103
  /**
@@ -139,7 +162,7 @@ export class SessionPromptRuntime {
139
162
  private readonly appendErrorMessage: SessionPromptRuntimeOptions["appendErrorMessage"];
140
163
  private readonly executeTurn: SessionPromptRuntimeOptions["executeTurn"];
141
164
  private readonly stopTurn: SessionPromptRuntimeOptions["stopTurn"];
142
- private readonly queue: QueuedPrompt[] = [];
165
+ private readonly queue: QueuedSessionInput[] = [];
143
166
  private processingPromise: Promise<void> | null = null;
144
167
  private activeTurn: ActiveTurnState | null = null;
145
168
 
@@ -161,6 +184,7 @@ export class SessionPromptRuntime {
161
184
  prompt(input: AgentSessionPromptInput): Promise<AgentSessionTurnHandle> {
162
185
  const deferredHandle = createDeferred<AgentSessionTurnHandle>();
163
186
  this.queue.push({
187
+ type: "prompt",
164
188
  input,
165
189
  deferredHandle,
166
190
  });
@@ -168,6 +192,23 @@ export class SessionPromptRuntime {
168
192
  return deferredHandle.promise;
169
193
  }
170
194
 
195
+ /**
196
+ * 把一次已成功写入 configured state 的配置修改加入统一输入队列。
197
+ */
198
+ enqueue_config(mutation: SessionRuntimeConfigMutation): void {
199
+ this.queue.push({
200
+ type: "config",
201
+ mutation,
202
+ });
203
+ }
204
+
205
+ /**
206
+ * 判断是否存在等待并入模型 turn 的 prompt。
207
+ */
208
+ has_pending_prompt(): boolean {
209
+ return this.queue.some((item) => item.type === "prompt");
210
+ }
211
+
171
212
  /**
172
213
  * 返回当前 actor prompt 调度器是否仍处于活跃态。
173
214
  *
@@ -176,7 +217,7 @@ export class SessionPromptRuntime {
176
217
  * - Session 会用它阻止内部 direct execution 与 actor 模式并发混用。
177
218
  */
178
219
  isActive(): boolean {
179
- return this.processingPromise !== null || this.queue.length > 0;
220
+ return this.processingPromise !== null || this.has_pending_prompt();
180
221
  }
181
222
 
182
223
  /**
@@ -211,16 +252,19 @@ export class SessionPromptRuntime {
211
252
  if (this.processingPromise) return;
212
253
  this.processingPromise = this.processLoop().finally(() => {
213
254
  this.processingPromise = null;
214
- if (this.queue.length > 0) {
255
+ if (this.has_pending_prompt()) {
215
256
  this.ensureProcessing();
216
257
  }
217
258
  });
218
259
  }
219
260
 
220
261
  private async processLoop(): Promise<void> {
221
- while (this.queue.length > 0) {
262
+ while (this.has_pending_prompt()) {
263
+ const prompt_index = this.queue.findIndex((item) => item.type === "prompt");
264
+ if (prompt_index < 0) break;
265
+ const queued_before_prompt = this.queue.splice(0, prompt_index);
222
266
  const current = this.queue.shift();
223
- if (!current) break;
267
+ if (!current || current.type !== "prompt") break;
224
268
 
225
269
  const turnId = `turn:${this.sessionId}:${Date.now()}:${nanoid(6)}`;
226
270
  const activeTurn = createActiveTurnState(turnId);
@@ -237,12 +281,18 @@ export class SessionPromptRuntime {
237
281
  current.deferredHandle.resolve(createTurnHandle(activeTurn));
238
282
 
239
283
  try {
284
+ await this.apply_config_mutations(
285
+ queued_before_prompt.filter(
286
+ (item): item is QueuedConfigMutation => item.type === "config",
287
+ ),
288
+ activeTurn,
289
+ );
240
290
  await this.createAndPersistUserMessage(current.input, turnId, "prompt");
241
291
  const result = await this.executeTurn({
242
292
  turnId,
243
293
  promptInput: current.input,
244
294
  onStepMerge: async () => {
245
- return await this.drainQueuedPromptsAsMessages(activeTurn);
295
+ return await this.drain_queued_inputs(activeTurn);
246
296
  },
247
297
  abortSignal: activeTurn.abortController.signal,
248
298
  });
@@ -317,8 +367,12 @@ export class SessionPromptRuntime {
317
367
  }
318
368
 
319
369
  private cancelQueuedPrompts(): number {
320
- if (this.queue.length <= 0) return 0;
321
- const cancelled = this.queue.splice(0, this.queue.length);
370
+ const cancelled = this.queue.filter(
371
+ (item): item is QueuedPrompt => item.type === "prompt",
372
+ );
373
+ if (cancelled.length <= 0) return 0;
374
+ const retained = this.queue.filter((item) => item.type === "config");
375
+ this.queue.splice(0, this.queue.length, ...retained);
322
376
  for (const item of cancelled) {
323
377
  const turnId = `turn:${this.sessionId}:cancelled:${Date.now()}:${nanoid(6)}`;
324
378
  const cancelledTurn = createActiveTurnState(turnId);
@@ -359,15 +413,30 @@ export class SessionPromptRuntime {
359
413
  return cancelled.length;
360
414
  }
361
415
 
362
- private async drainQueuedPromptsAsMessages(
416
+ /**
417
+ * 在下一模型 turn 开始前按入队顺序提交配置并持久化 steer。
418
+ */
419
+ private async drain_queued_inputs(
363
420
  activeTurn: ActiveTurnState,
364
421
  ): Promise<SessionUserMessageV1[]> {
422
+ activeTurn.model_turn_index += 1;
365
423
  if (this.queue.length <= 0) return [];
366
424
  const drained = this.queue.splice(0, this.queue.length);
367
425
  const merged: SessionUserMessageV1[] = [];
368
426
 
369
427
  for (let index = 0; index < drained.length; index += 1) {
370
428
  const item = drained[index];
429
+ if (item.type === "config") {
430
+ try {
431
+ await item.mutation.apply({
432
+ turn_id: activeTurn.turnId,
433
+ model_turn_index: activeTurn.model_turn_index,
434
+ });
435
+ } catch {
436
+ // 配置实现负责写 failed action;单条失败不能吞掉后续 steer 或配置。
437
+ }
438
+ continue;
439
+ }
371
440
  try {
372
441
  const message = await this.createAndPersistUserMessage(
373
442
  item.input,
@@ -386,6 +455,27 @@ export class SessionPromptRuntime {
386
455
 
387
456
  return merged;
388
457
  }
458
+
459
+ /**
460
+ * 提交一组已经从统一队列截取的配置 mutation。
461
+ */
462
+ private async apply_config_mutations(
463
+ mutations: QueuedConfigMutation[],
464
+ active_turn: ActiveTurnState,
465
+ ): Promise<void> {
466
+ if (mutations.length <= 0) return;
467
+ active_turn.model_turn_index += 1;
468
+ for (const item of mutations) {
469
+ try {
470
+ await item.mutation.apply({
471
+ turn_id: active_turn.turnId,
472
+ model_turn_index: active_turn.model_turn_index,
473
+ });
474
+ } catch {
475
+ // 配置实现负责写 failed action;单条失败不能阻断当前 prompt。
476
+ }
477
+ }
478
+ }
389
479
  }
390
480
 
391
481
  function buildPromptRuntimeErrorAssistantMessage(input: {
@@ -426,6 +516,7 @@ function createActiveTurnState(turnId: string): ActiveTurnState {
426
516
  result: null,
427
517
  deferredFinished: createDeferred<AgentSessionTurnResult>(),
428
518
  abortController: new AbortController(),
519
+ model_turn_index: 0,
429
520
  };
430
521
  }
431
522
 
@@ -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 { SessionRuntimeConfigMutation } from "@/types/session/SessionConfigMutation.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 SessionConfiguredMutationResult {
123
+ /**
124
+ * 等待在下一模型 turn 提交的 mutation。
125
+ *
126
+ * 说明(中文)
127
+ * - 输入未产生实际配置变化时为空。
128
+ * - fork 等内部初始化路径可以选择立即提交而不返回 mutation。
129
+ */
130
+ mutation?: SessionRuntimeConfigMutation;
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<SessionConfiguredMutationResult> {
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,71 @@ 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) {
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;
301
323
  await this.emit_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 mutation_id = generateId();
337
+ return {
338
+ mutation: {
339
+ mutation_id,
340
+ scope: "session",
341
+ apply: async ({ turn_id }) => {
342
+ try {
343
+ await apply_effective_config(turn_id);
344
+ } catch (error) {
345
+ if (should_emit_model_switch_action) {
346
+ await this.emit_action_event({
347
+ id: action_id,
348
+ title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
349
+ description: error instanceof Error ? error.message : String(error),
350
+ state: "failed",
351
+ turnId: turn_id,
352
+ });
353
+ }
354
+ throw error;
355
+ }
356
+ },
357
+ },
358
+ };
359
+ } catch (error) {
360
+ throw error;
317
361
  }
318
362
  }
319
363
 
@@ -324,6 +368,8 @@ export class SessionStateService {
324
368
  const model = await this.resolve_model_by_id(model_id);
325
369
  this.state.sessionConfig.model = normalizeAgentModel(model);
326
370
  this.state.sessionConfig.modelLabel = inferAgentModelLabel(model);
371
+ this.state.sessionConfig.model_context_window =
372
+ read_agent_model_context_window(model);
327
373
  }
328
374
 
329
375
  /** 使用宿主 resolver 解析稳定模型 ID。 */
@@ -384,28 +430,17 @@ export class SessionStateService {
384
430
  * 仅刷新当前 session metadata。
385
431
  */
386
432
  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);
433
+ const stats = await this.recorder.storage_stats();
399
434
  const preview_text = resolve_message_preview(
400
- message_page.items[message_page.items.length - 1],
435
+ stats.latest_message || undefined,
401
436
  ).slice(0, 180);
402
437
  await touchSessionMetadata({
403
438
  projectRoot: this.project_root,
404
439
  agentId: this.agent_id,
405
440
  sessionId: this.session_id,
406
441
  sessionConfig: this.state.sessionConfig,
407
- message_count: message_page.total,
408
- history_bytes,
442
+ message_count: stats.message_count,
443
+ history_bytes: stats.history_bytes,
409
444
  ...(preview_text ? { preview_text } : {}),
410
445
  });
411
446
  }
@@ -22,6 +22,7 @@ import type {
22
22
  import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
23
23
  import { SessionPromptRuntime } from "@/session/runtime/SessionPromptRuntime.js";
24
24
  import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
25
+ import type { SessionRuntimeConfigMutation } from "@/types/session/SessionConfigMutation.js";
25
26
  import { SessionStateService } from "@/session/services/SessionStateService.js";
26
27
  import {
27
28
  SessionAssistantMessageWriter,
@@ -136,6 +137,13 @@ export class SessionTurnService {
136
137
  return await this.prompt_runtime.prompt(input);
137
138
  }
138
139
 
140
+ /**
141
+ * 把 configured state 修改加入当前 Session 的统一输入队列。
142
+ */
143
+ enqueue_config(mutation: SessionRuntimeConfigMutation): void {
144
+ this.prompt_runtime.enqueue_config(mutation);
145
+ }
146
+
139
147
  /**
140
148
  * 停止当前 turn,并取消尚未被吸收的排队 prompt。
141
149
  */
@@ -182,6 +190,7 @@ export class SessionTurnService {
182
190
  }
183
191
  return merged;
184
192
  },
193
+ hasPendingStepInput: () => this.prompt_runtime.has_pending_prompt(),
185
194
  onUiMessageChunkCallback: async (chunk) => {
186
195
  if (!is_assistant_content_chunk(chunk.type)) return;
187
196
  const writer = await ensure_assistant_writer();
@@ -201,8 +210,16 @@ export class SessionTurnService {
201
210
  query: executor_query,
202
211
  runContext: run_context,
203
212
  });
213
+ const final_assistant_parts = result.assistantMessage
214
+ ? from_ui_assistant_parts(result.assistantMessage.parts)
215
+ : [];
204
216
  const final_assistant_writer = assistant_writer_ref.current;
205
217
  if (final_assistant_writer) {
218
+ for (const part of final_assistant_parts) {
219
+ if (part.type === "file") {
220
+ await final_assistant_writer.append_file_part(part);
221
+ }
222
+ }
206
223
  if (input.abortSignal?.aborted) {
207
224
  await final_assistant_writer.stop();
208
225
  } else if (result.success) {
@@ -211,14 +228,13 @@ export class SessionTurnService {
211
228
  await final_assistant_writer.fail(result.error);
212
229
  }
213
230
  } else if (result.assistantMessage) {
214
- const parts = from_ui_assistant_parts(result.assistantMessage.parts);
215
- if (parts.length > 0) {
231
+ if (final_assistant_parts.length > 0) {
216
232
  assistant_segment_index += 1;
217
233
  const fallback_writer = await this.recorder.open_assistant_message({
218
234
  turn_id: input.turnId,
219
235
  segment_index: assistant_segment_index,
220
236
  });
221
- for (const part of parts) await fallback_writer.upsert_part(part);
237
+ for (const part of final_assistant_parts) await fallback_writer.upsert_part(part);
222
238
  if (result.success) await fallback_writer.complete();
223
239
  else await fallback_writer.fail(result.error);
224
240
  }
@@ -8,12 +8,7 @@
8
8
  */
9
9
 
10
10
  import { nanoid } from "nanoid";
11
- import {
12
- buildSessionRecordsPage,
13
- buildSessionInfo,
14
- loadSessionArchiveMessagesFromPath,
15
- } from "@/session/browse/Browse.js";
16
- import { getSdkAgentSessionArchiveFilePath } from "@/session/storage/Paths.js";
11
+ import { buildSessionInfo } from "@/session/browse/Browse.js";
17
12
  import { ensureSessionTitle } from "@/session/SessionTitle.js";
18
13
  import { readSessionMetadata } from "@/session/storage/Metadata.js";
19
14
  import { buildSessionSystemBlocks } from "@/session/SessionSystemBuilder.js";
@@ -21,8 +16,6 @@ import type { SessionHistoryStore } from "@/executor/store/history/SessionHistor
21
16
  import type { SessionSystemComposer } from "@/executor/composer/system/SessionSystemComposer.js";
22
17
  import type {
23
18
  AgentSessionForkInput,
24
- AgentSessionRecordsInput,
25
- AgentSessionRecordsPage,
26
19
  AgentSessionInfo,
27
20
  AgentSessionSystemBlock,
28
21
  AgentSessionSystemSnapshot,
@@ -192,42 +185,6 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
192
185
  });
193
186
  }
194
187
 
195
- /**
196
- * 读取当前 session records 分页。
197
- */
198
- async records(
199
- input?: AgentSessionRecordsInput,
200
- ): Promise<AgentSessionRecordsPage> {
201
- const archive_id = String(input?.archive_id || "").trim();
202
- const [metadata, current_messages] = await Promise.all([
203
- readSessionMetadata({
204
- projectRoot: this.project_root,
205
- agentId: this.agent_id,
206
- sessionId: this.session_id,
207
- }),
208
- this.history_store.list_records(),
209
- ]);
210
- const page_messages = archive_id
211
- ? await loadSessionArchiveMessagesFromPath(
212
- getSdkAgentSessionArchiveFilePath(
213
- this.project_root,
214
- this.agent_id,
215
- this.session_id,
216
- archive_id,
217
- ),
218
- )
219
- : current_messages;
220
- const session = await this.build_info({
221
- metadata,
222
- messages: current_messages,
223
- });
224
- return buildSessionRecordsPage({
225
- session,
226
- messages: page_messages,
227
- input,
228
- });
229
- }
230
-
231
188
  /**
232
189
  * 读取当前 session 生效的 system 快照。
233
190
  */
@@ -290,14 +247,11 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
290
247
  typeof input === "string"
291
248
  ? String(input || "").trim() || undefined
292
249
  : String(input?.messageId || "").trim() || undefined;
293
- const message_page = await this.recorder.list_messages({
294
- limit: 500,
295
- include_internal: true,
296
- });
250
+ const messages = await this.recorder.list_history_messages();
297
251
  const fork_messages =
298
252
  !message_id
299
- ? message_page.items
300
- : this.resolve_fork_messages(message_page.items, message_id);
253
+ ? messages
254
+ : this.resolve_fork_messages(messages, message_id);
301
255
  const action_id = `history-forking:${this.session_id}:${Date.now()}:${nanoid(8)}`;
302
256
 
303
257
  await this.state_service.emit_action_event({
@@ -88,7 +88,7 @@ export function getSdkAgentArchivedSessionMessagesDirPath(
88
88
  }
89
89
 
90
90
  /**
91
- * 单个已归档 session 的消息 JSONL 文件路径。
91
+ * 单个已归档 session Active JSONL 文件路径。
92
92
  */
93
93
  export function getSdkAgentArchivedSessionMessagesPath(
94
94
  projectRoot: string,
@@ -97,7 +97,7 @@ export function getSdkAgentArchivedSessionMessagesPath(
97
97
  ): string {
98
98
  return path.join(
99
99
  getSdkAgentArchivedSessionMessagesDirPath(projectRoot, agentId, sessionId),
100
- "messages.jsonl",
100
+ "active.jsonl",
101
101
  );
102
102
  }
103
103
 
@@ -144,7 +144,7 @@ export function getSdkAgentSessionMessagesDirPath(
144
144
  }
145
145
 
146
146
  /**
147
- * 单个 session 的消息 JSONL 文件路径。
147
+ * 单个 session Active JSONL 文件路径。
148
148
  */
149
149
  export function getSdkAgentSessionMessagesPath(
150
150
  projectRoot: string,
@@ -153,7 +153,7 @@ export function getSdkAgentSessionMessagesPath(
153
153
  ): string {
154
154
  return path.join(
155
155
  getSdkAgentSessionMessagesDirPath(projectRoot, agentId, sessionId),
156
- "messages.jsonl",
156
+ "active.jsonl",
157
157
  );
158
158
  }
159
159
 
@@ -171,46 +171,13 @@ export function getSdkAgentSessionMetaPath(
171
171
  );
172
172
  }
173
173
 
174
- /**
175
- * 单个 session 的 archive 目录路径。
176
- */
177
- export function getSdkAgentSessionArchiveDirPath(
178
- projectRoot: string,
179
- agentId: string,
180
- sessionId: string,
181
- ): string {
182
- return path.join(
183
- getSdkAgentSessionMessagesDirPath(projectRoot, agentId, sessionId),
184
- "archive",
185
- );
186
- }
187
-
188
- /**
189
- * 单个 session 的 compact archive JSON 文件路径。
190
- *
191
- * 关键点(中文)
192
- * - `archiveId` 来自 compact summary metadata,不由调用方拼接扩展名。
193
- * - 这里统一 encode,避免 archiveId 中的特殊字符影响文件路径。
194
- */
195
- export function getSdkAgentSessionArchiveFilePath(
196
- projectRoot: string,
197
- agentId: string,
198
- sessionId: string,
199
- archiveId: string,
200
- ): string {
201
- return path.join(
202
- getSdkAgentSessionArchiveDirPath(projectRoot, agentId, sessionId),
203
- `${encodeURIComponent(String(archiveId || "").trim())}.json`,
204
- );
205
- }
206
-
207
174
  /**
208
175
  * 单个 session 的 inflight assistant 路径。
209
176
  *
210
177
  * 关键点(中文)
211
178
  * - 运行中的 assistant 只保留一份增量快照。
212
179
  * - step / tool 过程会持续重写这个文件,避免中途中断后过程完全丢失。
213
- * - 完成后再把最终 assistant 合并进 `messages.jsonl`,并清理该文件。
180
+ * - 完成后再把最终 assistant 合并进 `active.jsonl`,并清理该文件。
214
181
  */
215
182
  export function getSdkAgentSessionAssistantMessagePath(
216
183
  projectRoot: string,