@downcity/agent 1.1.216 → 1.1.222

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 (175) hide show
  1. package/bin/agent/local/Agent.d.ts +2 -0
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +6 -0
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentSessions.d.ts +6 -0
  6. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  7. package/bin/agent/local/services/AgentSessions.js +8 -1
  8. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  9. package/bin/agent/remote/RemoteSession.d.ts +11 -7
  10. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  11. package/bin/agent/remote/RemoteSession.js +36 -11
  12. package/bin/agent/remote/RemoteSession.js.map +1 -1
  13. package/bin/agent/remote/RemoteTransport.d.ts +9 -3
  14. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  15. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +6 -2
  16. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  17. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +38 -11
  18. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  19. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +6 -2
  20. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  21. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -2
  22. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  23. package/bin/executor/types/SessionHistoryMeta.d.ts +2 -0
  24. package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
  25. package/bin/executor/types/SessionRecords.d.ts +2 -0
  26. package/bin/executor/types/SessionRecords.d.ts.map +1 -1
  27. package/bin/executor/types/SessionRecords.js.map +1 -1
  28. package/bin/index.d.ts +5 -2
  29. package/bin/index.d.ts.map +1 -1
  30. package/bin/index.js +1 -0
  31. package/bin/index.js.map +1 -1
  32. package/bin/rpc/Client.d.ts +18 -4
  33. package/bin/rpc/Client.d.ts.map +1 -1
  34. package/bin/rpc/Client.js +25 -3
  35. package/bin/rpc/Client.js.map +1 -1
  36. package/bin/session/Session.d.ts +13 -3
  37. package/bin/session/Session.d.ts.map +1 -1
  38. package/bin/session/Session.js +48 -18
  39. package/bin/session/Session.js.map +1 -1
  40. package/bin/session/browse/Browse.d.ts.map +1 -1
  41. package/bin/session/browse/Browse.js +3 -0
  42. package/bin/session/browse/Browse.js.map +1 -1
  43. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +45 -0
  44. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
  45. package/bin/session/recorder/JsonlSessionMessageStore.js +127 -0
  46. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -0
  47. package/bin/session/recorder/SessionMessageCodec.d.ts +20 -0
  48. package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -0
  49. package/bin/session/recorder/SessionMessageCodec.js +252 -0
  50. package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
  51. package/bin/session/recorder/SessionMessageReducer.d.ts +12 -0
  52. package/bin/session/recorder/SessionMessageReducer.d.ts.map +1 -0
  53. package/bin/session/recorder/SessionMessageReducer.js +109 -0
  54. package/bin/session/recorder/SessionMessageReducer.js.map +1 -0
  55. package/bin/session/recorder/SessionRecorder.d.ts +177 -0
  56. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
  57. package/bin/session/recorder/SessionRecorder.js +610 -0
  58. package/bin/session/recorder/SessionRecorder.js.map +1 -0
  59. package/bin/session/recorder/SessionRecorderCompaction.d.ts +18 -0
  60. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -0
  61. package/bin/session/recorder/SessionRecorderCompaction.js +141 -0
  62. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -0
  63. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +66 -0
  64. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -0
  65. package/bin/session/recorder/SessionRecorderHistoryStore.js +195 -0
  66. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -0
  67. package/bin/session/runtime/SessionMessageEventHub.d.ts +15 -0
  68. package/bin/session/runtime/SessionMessageEventHub.d.ts.map +1 -0
  69. package/bin/session/runtime/SessionMessageEventHub.js +28 -0
  70. package/bin/session/runtime/SessionMessageEventHub.js.map +1 -0
  71. package/bin/session/runtime/SessionPromptRuntime.d.ts +7 -13
  72. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  73. package/bin/session/runtime/SessionPromptRuntime.js +13 -13
  74. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  75. package/bin/session/services/SessionStateService.d.ts +15 -3
  76. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  77. package/bin/session/services/SessionStateService.js +165 -76
  78. package/bin/session/services/SessionStateService.js.map +1 -1
  79. package/bin/session/services/SessionTurnService.d.ts +4 -0
  80. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  81. package/bin/session/services/SessionTurnService.js +82 -52
  82. package/bin/session/services/SessionTurnService.js.map +1 -1
  83. package/bin/session/services/SessionViewService.d.ts +8 -3
  84. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  85. package/bin/session/services/SessionViewService.js +13 -5
  86. package/bin/session/services/SessionViewService.js.map +1 -1
  87. package/bin/session/storage/Metadata.d.ts +2 -0
  88. package/bin/session/storage/Metadata.d.ts.map +1 -1
  89. package/bin/session/storage/Metadata.js +10 -0
  90. package/bin/session/storage/Metadata.js.map +1 -1
  91. package/bin/session/storage/Persistence.d.ts +6 -0
  92. package/bin/session/storage/Persistence.d.ts.map +1 -1
  93. package/bin/session/storage/Persistence.js +12 -0
  94. package/bin/session/storage/Persistence.js.map +1 -1
  95. package/bin/session/storage/RuntimeSessionPort.d.ts +3 -2
  96. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  97. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  98. package/bin/types/agent/AgentOptions.d.ts +10 -0
  99. package/bin/types/agent/AgentOptions.d.ts.map +1 -1
  100. package/bin/types/agent/SessionActor.d.ts +12 -4
  101. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  102. package/bin/types/agent/SessionTypes.d.ts +13 -1
  103. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  104. package/bin/types/rpc/RpcProtocol.d.ts +32 -6
  105. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  106. package/bin/types/runtime/agent/AgentContext.d.ts +4 -3
  107. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  108. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  109. package/bin/types/sdk/AgentSessionEvent.d.ts +2 -1
  110. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
  111. package/bin/types/session/SessionMessage.d.ts +217 -0
  112. package/bin/types/session/SessionMessage.d.ts.map +1 -0
  113. package/bin/types/session/SessionMessage.js +8 -0
  114. package/bin/types/session/SessionMessage.js.map +1 -0
  115. package/bin/types/session/SessionMessageMutation.d.ts +92 -0
  116. package/bin/types/session/SessionMessageMutation.d.ts.map +1 -0
  117. package/bin/types/session/SessionMessageMutation.js +21 -0
  118. package/bin/types/session/SessionMessageMutation.js.map +1 -0
  119. package/bin/types/session/SessionOptions.d.ts +3 -0
  120. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  121. package/package.json +3 -3
  122. package/scripts/agent-env.test.mjs +4 -4
  123. package/scripts/remote-session-reconnect.test.mjs +33 -0
  124. package/scripts/session-action-message.test.mjs +148 -27
  125. package/scripts/session-list-title.test.mjs +6 -19
  126. package/scripts/session-prompt-runtime.test.mjs +4 -16
  127. package/scripts/session-recorder.test.mjs +245 -0
  128. package/scripts/session-title-event.test.mjs +10 -18
  129. package/src/agent/local/Agent.ts +6 -0
  130. package/src/agent/local/services/AgentSessions.ts +14 -1
  131. package/src/agent/remote/RemoteSession.ts +52 -16
  132. package/src/agent/remote/RemoteTransport.ts +20 -6
  133. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +67 -13
  134. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +27 -6
  135. package/src/executor/types/SessionHistoryMeta.ts +2 -0
  136. package/src/executor/types/SessionRecords.ts +2 -0
  137. package/src/index.ts +23 -5
  138. package/src/rpc/Client.ts +47 -8
  139. package/src/session/Session.ts +66 -31
  140. package/src/session/browse/Browse.ts +3 -0
  141. package/src/session/recorder/JsonlSessionMessageStore.ts +162 -0
  142. package/src/session/recorder/SessionMessageCodec.ts +279 -0
  143. package/src/session/recorder/SessionMessageReducer.ts +155 -0
  144. package/src/session/recorder/SessionRecorder.ts +812 -0
  145. package/src/session/recorder/SessionRecorderCompaction.ts +187 -0
  146. package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
  147. package/src/session/runtime/SessionMessageEventHub.ts +35 -0
  148. package/src/session/runtime/SessionPromptRuntime.ts +23 -24
  149. package/src/session/services/SessionStateService.ts +189 -84
  150. package/src/session/services/SessionTurnService.ts +88 -59
  151. package/src/session/services/SessionViewService.ts +28 -13
  152. package/src/session/storage/Metadata.ts +13 -0
  153. package/src/session/storage/Persistence.ts +18 -0
  154. package/src/session/storage/RuntimeSessionPort.ts +6 -4
  155. package/src/types/agent/AgentOptions.ts +12 -0
  156. package/src/types/agent/SessionActor.ts +28 -5
  157. package/src/types/agent/SessionTypes.ts +13 -1
  158. package/src/types/rpc/RpcProtocol.ts +34 -6
  159. package/src/types/runtime/agent/AgentContext.ts +8 -2
  160. package/src/types/sdk/AgentSessionEvent.ts +2 -0
  161. package/src/types/session/SessionMessage.ts +247 -0
  162. package/src/types/session/SessionMessageMutation.ts +121 -0
  163. package/src/types/session/SessionOptions.ts +4 -0
  164. package/tsconfig.tsbuildinfo +1 -1
  165. package/bin/session/SessionEventMapper.d.ts +0 -32
  166. package/bin/session/SessionEventMapper.d.ts.map +0 -1
  167. package/bin/session/SessionEventMapper.js +0 -131
  168. package/bin/session/SessionEventMapper.js.map +0 -1
  169. package/bin/types/sdk/InternalUiChunkEvent.d.ts +0 -100
  170. package/bin/types/sdk/InternalUiChunkEvent.d.ts.map +0 -1
  171. package/bin/types/sdk/InternalUiChunkEvent.js +0 -9
  172. package/bin/types/sdk/InternalUiChunkEvent.js.map +0 -1
  173. package/scripts/session-history-archive.test.mjs +0 -188
  174. package/src/session/SessionEventMapper.ts +0 -148
  175. package/src/types/sdk/InternalUiChunkEvent.ts +0 -108
@@ -19,10 +19,9 @@ import {
19
19
  } from "@/session/storage/Metadata.js";
20
20
  import { touchSessionMetadata } from "@/session/storage/Persistence.js";
21
21
  import { ensureSessionTitle } from "@/session/SessionTitle.js";
22
- import { persistSdkAssistantResult } from "@/session/storage/Persistence.js";
23
22
  import { hydrateUserPromptFileParts } from "@executor/messages/SessionAttachmentMapper.js";
24
23
  import type { Executor } from "@executor/Executor.js";
25
- import type { JsonlSessionHistoryStore } from "@/executor/store/history/jsonl/JsonlSessionHistoryStore.js";
24
+ import type { SessionHistoryStore } from "@/executor/store/history/SessionHistoryStore.js";
26
25
  import type {
27
26
  AgentSessionConfigSnapshot,
28
27
  AgentSessionSetInput,
@@ -38,10 +37,20 @@ import type {
38
37
  SessionMessageRecordV1,
39
38
  SessionUserMessageV1,
40
39
  } from "@/executor/types/SessionRecords.js";
41
- import { to_session_action_record } from "@/executor/types/SessionRecords.js";
42
40
  import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
43
41
  import { generateId } from "@/utils/Id.js";
44
42
  import type { Logger } from "@/utils/logger/Logger.js";
43
+ import type { AgentModel } from "@/model/CityModelAdapter.js";
44
+ import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
45
+ import { normalize_session_user_parts } from "@/session/recorder/SessionRecorder.js";
46
+ import {
47
+ from_ui_assistant_parts,
48
+ from_ui_user_parts,
49
+ to_executor_ui_message,
50
+ } from "@/session/recorder/SessionMessageCodec.js";
51
+ import fs from "fs-extra";
52
+ import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
53
+ import type { SessionMessage } from "@/types/session/SessionMessage.js";
45
54
 
46
55
  type SessionStateServiceOptions = {
47
56
  /**
@@ -62,7 +71,10 @@ type SessionStateServiceOptions = {
62
71
  /**
63
72
  * 当前 session 历史事实源。
64
73
  */
65
- history_store: JsonlSessionHistoryStore;
74
+ history_store: SessionHistoryStore;
75
+
76
+ /** 当前 Session Message Recorder。 */
77
+ recorder: SessionRecorder;
66
78
 
67
79
  /**
68
80
  * 当前 session 执行器。
@@ -84,6 +96,9 @@ type SessionStateServiceOptions = {
84
96
  */
85
97
  ensure_configured_hook?: () => Promise<void>;
86
98
 
99
+ /** 按稳定 ID 解析当前 Session 的运行时模型。 */
100
+ resolve_model?: (model_id: string) => Promise<AgentModel>;
101
+
87
102
  /**
88
103
  * 发布 session 事件。
89
104
  */
@@ -110,11 +125,13 @@ export class SessionStateService {
110
125
  private readonly agent_id: string;
111
126
  private readonly project_root: string;
112
127
  private readonly session_id: string;
113
- private readonly history_store: JsonlSessionHistoryStore;
128
+ private readonly history_store: SessionHistoryStore;
129
+ private readonly recorder: SessionRecorder;
114
130
  private readonly executor: Executor;
115
131
  private readonly state: SessionLocalState;
116
132
  private readonly logger: Logger;
117
133
  private readonly ensure_configured_hook?: SessionStateServiceOptions["ensure_configured_hook"];
134
+ private readonly resolve_model?: SessionStateServiceOptions["resolve_model"];
118
135
  private readonly publish_event: SessionStateServiceOptions["publish_event"];
119
136
 
120
137
  constructor(options: SessionStateServiceOptions) {
@@ -122,10 +139,12 @@ export class SessionStateService {
122
139
  this.project_root = options.project_root;
123
140
  this.session_id = options.session_id;
124
141
  this.history_store = options.history_store;
142
+ this.recorder = options.recorder;
125
143
  this.executor = options.executor;
126
144
  this.state = options.state;
127
145
  this.logger = options.logger;
128
146
  this.ensure_configured_hook = options.ensure_configured_hook;
147
+ this.resolve_model = options.resolve_model;
129
148
  this.publish_event = options.publish_event;
130
149
  }
131
150
 
@@ -189,7 +208,9 @@ export class SessionStateService {
189
208
  ...(metadata.modelLabel
190
209
  ? { modelLabel: metadata.modelLabel }
191
210
  : {}),
211
+ ...(metadata.modelId ? { modelId: metadata.modelId } : {}),
192
212
  };
213
+ await this.restore_persisted_model();
193
214
  })();
194
215
  await this.state.initializePromise;
195
216
  }
@@ -231,46 +252,57 @@ export class SessionStateService {
231
252
  async set(input: AgentSessionSetInput, options?: SessionSetOptions): Promise<void> {
232
253
  const should_emit_action = options?.emit_action !== false;
233
254
  const previous_model_label = this.state.sessionConfig.modelLabel;
234
- const next_model_label = input.model
235
- ? inferAgentModelLabel(input.model)
255
+ const previous_model_id = String(
256
+ this.state.sessionConfig.modelId || previous_model_label || "",
257
+ ).trim();
258
+ const requested_model_id = String(input.modelId || "").trim();
259
+ const next_model = input.model || (requested_model_id
260
+ ? await this.resolve_model_by_id(requested_model_id)
261
+ : undefined);
262
+ const next_model_label = next_model
263
+ ? inferAgentModelLabel(next_model)
236
264
  : undefined;
265
+ const next_model_id = requested_model_id || next_model_label || "";
237
266
  const should_emit_model_switch_action = Boolean(
238
- input.model &&
267
+ next_model &&
239
268
  should_emit_action &&
240
269
  this.state.sessionConfig.model &&
241
- previous_model_label &&
242
- next_model_label &&
243
- previous_model_label !== next_model_label,
270
+ previous_model_id &&
271
+ next_model_id &&
272
+ previous_model_id !== next_model_id,
244
273
  );
274
+ const previous_model_name = previous_model_label || previous_model_id;
275
+ const next_model_name = next_model_label || next_model_id;
245
276
  const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
246
277
 
247
278
  if (should_emit_model_switch_action) {
248
279
  await this.emit_action_event({
249
280
  id: action_id,
250
- title: "Switching session model",
251
- description: next_model_label
252
- ? `Switching to ${next_model_label}.`
253
- : undefined,
281
+ title: `Switching session model from ${previous_model_name} to ${next_model_name}`,
254
282
  state: "running",
255
283
  });
256
284
  }
257
285
 
258
286
  try {
259
- if (input.model) {
260
- this.state.sessionConfig.model = normalizeAgentModel(input.model);
287
+ if (next_model) {
288
+ this.state.sessionConfig.model = normalizeAgentModel(next_model);
261
289
  this.state.sessionConfig.modelLabel = next_model_label;
262
290
  }
291
+ if (next_model_id) {
292
+ this.state.sessionConfig.modelId = next_model_id;
293
+ }
263
294
  await patchSessionModelLabel({
264
295
  projectRoot: this.project_root,
265
296
  agentId: this.agent_id,
266
297
  sessionId: this.session_id,
267
298
  model: this.state.sessionConfig.model,
299
+ modelId: this.state.sessionConfig.modelId,
268
300
  });
269
301
  } catch (error) {
270
302
  if (should_emit_model_switch_action) {
271
303
  await this.emit_action_event({
272
304
  id: action_id,
273
- title: "Session model switch failed",
305
+ title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
274
306
  description: error instanceof Error ? error.message : String(error),
275
307
  state: "failed",
276
308
  });
@@ -281,15 +313,29 @@ export class SessionStateService {
281
313
  if (should_emit_model_switch_action) {
282
314
  await this.emit_action_event({
283
315
  id: action_id,
284
- title: "Session model switched",
285
- description: this.state.sessionConfig.modelLabel
286
- ? `Using ${this.state.sessionConfig.modelLabel}.`
287
- : undefined,
316
+ title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
288
317
  state: "completed",
289
318
  });
290
319
  }
291
320
  }
292
321
 
322
+ /** 恢复 metadata 中持久化的模型覆盖。 */
323
+ private async restore_persisted_model(): Promise<void> {
324
+ const model_id = String(this.state.sessionConfig.modelId || "").trim();
325
+ if (!model_id || this.state.sessionConfig.model) return;
326
+ const model = await this.resolve_model_by_id(model_id);
327
+ this.state.sessionConfig.model = normalizeAgentModel(model);
328
+ this.state.sessionConfig.modelLabel = inferAgentModelLabel(model);
329
+ }
330
+
331
+ /** 使用宿主 resolver 解析稳定模型 ID。 */
332
+ private async resolve_model_by_id(model_id: string): Promise<AgentModel> {
333
+ if (!this.resolve_model) {
334
+ throw new Error("Session model resolver is not configured.");
335
+ }
336
+ return await this.resolve_model(model_id);
337
+ }
338
+
293
339
  /**
294
340
  * 追加一条 user 消息并刷新标题与 metadata。
295
341
  */
@@ -297,7 +343,20 @@ export class SessionStateService {
297
343
  message?: SessionRecordV1 | null;
298
344
  text?: string;
299
345
  }): Promise<void> {
300
- await this.executor.append_user_message(params);
346
+ const parts = params.message && "role" in params.message
347
+ ? from_ui_user_parts(params.message.parts)
348
+ : [{
349
+ part_id: `external-user-text:${Date.now()}`,
350
+ type: "text" as const,
351
+ text: String(params.text || "").trim(),
352
+ state: "done" as const,
353
+ }];
354
+ if (parts.length === 0) return;
355
+ await this.recorder.append_user_message({
356
+ turn_id: `external:${this.session_id}:${Date.now()}`,
357
+ input_type: "prompt",
358
+ parts,
359
+ });
301
360
  await this.ensure_title_from_history({ generate: true });
302
361
  await this.touch_metadata();
303
362
  }
@@ -309,7 +368,16 @@ export class SessionStateService {
309
368
  message?: SessionRecordV1 | null;
310
369
  fallbackText?: string;
311
370
  }): Promise<void> {
312
- await this.executor.append_assistant_message(params);
371
+ const parts = params.message && "role" in params.message
372
+ ? from_ui_assistant_parts(params.message.parts)
373
+ : [{
374
+ part_id: `external-assistant-text:${Date.now()}`,
375
+ type: "text" as const,
376
+ text: String(params.fallbackText || "").trim(),
377
+ state: "done" as const,
378
+ }];
379
+ if (parts.length === 0) return;
380
+ await this.recorder.append_completed_assistant_message({ parts });
313
381
  await this.touch_metadata();
314
382
  }
315
383
 
@@ -317,11 +385,29 @@ export class SessionStateService {
317
385
  * 仅刷新当前 session metadata。
318
386
  */
319
387
  async touch_metadata(): Promise<void> {
388
+ const message_page = await this.recorder.list_messages({
389
+ limit: 500,
390
+ include_internal: true,
391
+ });
392
+ const messages_path = getSdkAgentSessionMessagesPath(
393
+ this.project_root,
394
+ this.agent_id,
395
+ this.session_id,
396
+ );
397
+ const history_bytes = await fs.stat(messages_path)
398
+ .then((file_stat) => file_stat.size)
399
+ .catch(() => 0);
400
+ const preview_text = resolve_message_preview(
401
+ message_page.items[message_page.items.length - 1],
402
+ ).slice(0, 180);
320
403
  await touchSessionMetadata({
321
404
  projectRoot: this.project_root,
322
405
  agentId: this.agent_id,
323
406
  sessionId: this.session_id,
324
407
  sessionConfig: this.state.sessionConfig,
408
+ message_count: message_page.total,
409
+ history_bytes,
410
+ ...(preview_text ? { preview_text } : {}),
325
411
  });
326
412
  }
327
413
 
@@ -368,14 +454,8 @@ export class SessionStateService {
368
454
  async persist_assistant_result(
369
455
  assistant_message?: SessionMessageRecordV1 | null,
370
456
  ): Promise<void> {
371
- await persistSdkAssistantResult({
372
- projectRoot: this.project_root,
373
- agentId: this.agent_id,
374
- sessionId: this.session_id,
375
- sessionConfig: this.state.sessionConfig,
376
- executor: this.executor,
377
- assistantMessage: assistant_message,
378
- });
457
+ void assistant_message;
458
+ await this.touch_metadata();
379
459
  }
380
460
 
381
461
  /**
@@ -384,8 +464,23 @@ export class SessionStateService {
384
464
  async persist_action_event(
385
465
  event: SessionActionRecordV1,
386
466
  ): Promise<void> {
387
- const message = to_session_action_record(event, this.session_id);
388
- await this.history_store.write_record(message);
467
+ const existing = this.recorder.get_message(event.id);
468
+ if (!existing) {
469
+ const writer = await this.recorder.open_action_message({
470
+ message_id: event.id,
471
+ turn_id: event.metadata.turnId,
472
+ action_type: infer_action_type(event.id),
473
+ title: event.title,
474
+ description: event.description,
475
+ });
476
+ if (event.state === "completed") await writer.complete();
477
+ if (event.state === "failed") await writer.fail(event.description || event.title);
478
+ } else if (existing.type === "action" && event.state !== "running") {
479
+ await this.recorder.update_action_message(event.id, event.state, {
480
+ title: event.title,
481
+ description: event.description,
482
+ });
483
+ }
389
484
  await this.touch_metadata();
390
485
  }
391
486
 
@@ -393,21 +488,26 @@ export class SessionStateService {
393
488
  * 写入并发布一条 action。
394
489
  */
395
490
  async emit_action_event(input: EmitActionInput): Promise<void> {
396
- const event = to_session_action_record(
397
- {
398
- ...input,
399
- id:
400
- String(input.id || "").trim() ||
401
- `action:${this.session_id}:${Date.now()}:${generateId()}`,
491
+ const action_id = String(input.id || "").trim() ||
492
+ `action:${this.session_id}:${Date.now()}`;
493
+ const turn_id = "turnId" in input
494
+ ? input.turnId
495
+ : input.metadata?.turnId;
496
+ await this.persist_action_event({
497
+ type: "action",
498
+ id: action_id,
499
+ title: input.title,
500
+ ...(input.description ? { description: input.description } : {}),
501
+ state: input.state,
502
+ metadata: {
503
+ v: 1,
504
+ ts: input.metadata?.ts || Date.now(),
505
+ sessionId: this.session_id,
506
+ ...(turn_id
507
+ ? { turnId: turn_id }
508
+ : {}),
402
509
  },
403
- this.session_id,
404
- );
405
- try {
406
- await this.persist_action_event(event);
407
- } catch {
408
- // action 持久化失败不应阻断宿主操作。
409
- }
410
- this.publish_event(event);
510
+ });
411
511
  }
412
512
 
413
513
  /**
@@ -415,43 +515,22 @@ export class SessionStateService {
415
515
  */
416
516
  async create_and_persist_user_prompt_message(
417
517
  input: AgentSessionPromptInput,
518
+ turn_id: string,
519
+ input_type: "prompt" | "steer" = "prompt",
418
520
  ): Promise<SessionUserMessageV1> {
419
521
  const query = input.query;
420
- if (typeof query === "string") {
421
- const message = this.history_store.userText({
422
- text: query.trim(),
423
- metadata: {
424
- sessionId: this.session_id,
425
- },
426
- }) as SessionUserMessageV1;
427
- await this.executor.append_user_message({
428
- message,
429
- });
430
- await this.ensure_title_from_history({ generate: true });
431
- await this.touch_metadata();
432
- return message;
433
- }
434
-
435
- // query 是 parts 数组,先把本地图片附件转换为模型可消费的 data URL。
436
- const parts = await hydrateUserPromptFileParts(
437
- Array.isArray(query) ? query : [],
438
- this.project_root,
439
- );
440
- const message: SessionUserMessageV1 = {
441
- id: `u:${this.session_id}:${generateId()}`,
442
- role: "user",
443
- parts,
444
- metadata: {
445
- v: 1,
446
- ts: Date.now(),
447
- sessionId: this.session_id,
448
- source: "ingress",
449
- kind: "normal",
450
- },
451
- };
452
- await this.executor.append_user_message({
453
- message,
522
+ const ui_parts = typeof query === "string"
523
+ ? [{ type: "text" as const, text: query.trim() }]
524
+ : await hydrateUserPromptFileParts(
525
+ Array.isArray(query) ? query : [],
526
+ this.project_root,
527
+ );
528
+ const canonical = await this.recorder.append_user_message({
529
+ turn_id,
530
+ input_type,
531
+ parts: normalize_session_user_parts(ui_parts),
454
532
  });
533
+ const message = to_executor_ui_message(canonical) as SessionUserMessageV1;
455
534
  await this.ensure_title_from_history({ generate: true });
456
535
  await this.touch_metadata();
457
536
  return message;
@@ -468,10 +547,36 @@ export class SessionStateService {
468
547
  : [];
469
548
  if (normalized_messages.length <= 0) return;
470
549
  for (const message of normalized_messages) {
471
- await this.executor.append_user_message({
472
- message,
550
+ await this.recorder.append_user_message({
551
+ turn_id: String(message.metadata?.turnId || `deferred:${this.session_id}:${Date.now()}`),
552
+ input_type: "steer",
553
+ parts: from_ui_user_parts(message.parts),
473
554
  });
474
555
  }
475
556
  await this.touch_metadata();
476
557
  }
477
558
  }
559
+
560
+ function resolve_message_preview(message: SessionMessage | undefined): string {
561
+ if (!message) return "";
562
+ if (message.type === "user") {
563
+ return message.parts
564
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
565
+ .join("")
566
+ .trim();
567
+ }
568
+ if (message.type === "assistant") {
569
+ return message.parts
570
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
571
+ .join("")
572
+ .trim();
573
+ }
574
+ if (message.type === "action") {
575
+ return [message.title, message.description].filter(Boolean).join("\n");
576
+ }
577
+ return message.message.trim();
578
+ }
579
+
580
+ function infer_action_type(message_id: string): string {
581
+ return String(message_id || "").split(":")[0] || "action";
582
+ }
@@ -23,11 +23,15 @@ import type {
23
23
  SessionMessageRecordV1,
24
24
  SessionUserMessageV1,
25
25
  } from "@/executor/types/SessionRecords.js";
26
- import { mapAgentEventToSessionEvent, mapUiMessageChunkToAgentEvent } from "@/session/SessionEventMapper.js";
27
26
  import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
28
27
  import { SessionPromptRuntime } from "@/session/runtime/SessionPromptRuntime.js";
29
28
  import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
30
29
  import { SessionStateService } from "@/session/services/SessionStateService.js";
30
+ import {
31
+ SessionAssistantMessageWriter,
32
+ SessionRecorder,
33
+ } from "@/session/recorder/SessionRecorder.js";
34
+ import { from_ui_assistant_parts } from "@/session/recorder/SessionMessageCodec.js";
31
35
 
32
36
  type SessionTurnServiceOptions = {
33
37
  /**
@@ -54,6 +58,9 @@ type SessionTurnServiceOptions = {
54
58
  * 当前 session 共享事件总线。
55
59
  */
56
60
  event_hub: SessionEventHub;
61
+
62
+ /** 当前 Session Message Recorder。 */
63
+ recorder: SessionRecorder;
57
64
  };
58
65
 
59
66
  /**
@@ -65,6 +72,7 @@ export class SessionTurnService {
65
72
  private readonly executor: Executor;
66
73
  private readonly state_service: SessionStateService;
67
74
  private readonly event_hub: SessionEventHub;
75
+ private readonly recorder: SessionRecorder;
68
76
  private readonly prompt_runtime: SessionPromptRuntime;
69
77
 
70
78
  constructor(options: SessionTurnServiceOptions) {
@@ -73,23 +81,26 @@ export class SessionTurnService {
73
81
  this.executor = options.executor;
74
82
  this.state_service = options.state_service;
75
83
  this.event_hub = options.event_hub;
84
+ this.recorder = options.recorder;
76
85
  this.prompt_runtime = new SessionPromptRuntime({
77
86
  sessionId: this.session_id,
78
87
  publish: (event) => {
79
88
  this.publish_event(event);
80
89
  },
81
- createAndPersistUserMessage: async (input) => {
90
+ createAndPersistUserMessage: async (input, turn_id, input_type) => {
82
91
  return await this.state_service.create_and_persist_user_prompt_message(
83
92
  input,
93
+ turn_id,
94
+ input_type,
84
95
  );
85
96
  },
86
- emit_steer_action: async ({ turn_id, message }) => {
87
- await this.state_service.emit_action_event({
88
- id: `steer-message:${message.id}`,
89
- title: "Session steer message sent",
90
- description: "Merged a new user message into the active turn.",
91
- state: "completed",
92
- turnId: turn_id,
97
+ appendErrorMessage: async ({ turn_id, message }) => {
98
+ await this.recorder.append_error_message({
99
+ scope: "turn",
100
+ turn_id,
101
+ code: "turn_execution_failed",
102
+ message,
103
+ recoverable: true,
93
104
  });
94
105
  },
95
106
  executeTurn: async ({ turnId, promptInput, onStepMerge, abortSignal }) => {
@@ -157,64 +168,38 @@ export class SessionTurnService {
157
168
  assistantMessage?: SessionMessageRecordV1 | null;
158
169
  error?: string;
159
170
  }> {
160
- const tool_name_by_call_id = new Map<string, string>();
171
+ const assistant_writer_ref: {
172
+ current: SessionAssistantMessageWriter | null;
173
+ } = { current: null };
174
+ let assistant_segment_index = 0;
175
+ const ensure_assistant_writer = async (): Promise<SessionAssistantMessageWriter> => {
176
+ if (assistant_writer_ref.current) return assistant_writer_ref.current;
177
+ assistant_segment_index += 1;
178
+ assistant_writer_ref.current = await this.recorder.open_assistant_message({
179
+ turn_id: input.turnId,
180
+ segment_index: assistant_segment_index,
181
+ });
182
+ return assistant_writer_ref.current;
183
+ };
161
184
  const run_context: SessionRunContext = {
162
185
  turnId: input.turnId,
163
186
  sessionId: this.session_id,
164
187
  projectRoot: this.project_root,
165
- onStepCallback: input.onStepMerge,
166
- onAssistantStepCallback: async (step) => {
167
- this.publish_event({
168
- type: "assistant-step",
169
- turnId: input.turnId,
170
- text: step.text,
171
- stepIndex: step.stepIndex,
172
- ...(step.visibility ? { visibility: step.visibility } : {}),
173
- });
188
+ onStepCallback: async () => {
189
+ const merged = await input.onStepMerge();
190
+ if (merged.length > 0 && assistant_writer_ref.current) {
191
+ await assistant_writer_ref.current.complete();
192
+ assistant_writer_ref.current = null;
193
+ }
194
+ return merged;
174
195
  },
175
196
  onUiMessageChunkCallback: async (chunk) => {
176
- if (chunk.type === "tool-input-start") {
177
- tool_name_by_call_id.set(chunk.toolCallId, chunk.toolName);
178
- return;
179
- }
180
- const event = mapUiMessageChunkToAgentEvent(chunk);
181
- if (!event) return;
182
- const resolved_event =
183
- (
184
- event.type === "tool-result" ||
185
- event.type === "tool-error"
186
- ) &&
187
- event.toolName === "unknown"
188
- ? {
189
- ...event,
190
- toolName:
191
- tool_name_by_call_id.get(event.toolCallId) || event.toolName,
192
- }
193
- : event;
194
- if (
195
- resolved_event.type === "tool-call" ||
196
- resolved_event.type === "tool-error"
197
- ) {
198
- tool_name_by_call_id.set(
199
- resolved_event.toolCallId,
200
- resolved_event.toolName,
201
- );
202
- }
203
- const session_event = mapAgentEventToSessionEvent({
204
- event: resolved_event,
205
- turnId: input.turnId,
206
- });
207
- if (session_event) {
208
- this.publish_event(session_event);
209
- }
197
+ if (!is_assistant_content_chunk(chunk.type)) return;
198
+ const writer = await ensure_assistant_writer();
199
+ await writer.apply_chunk(chunk);
210
200
  },
211
201
  onActionCallback: async (event) => {
212
- try {
213
- await this.state_service.persist_action_event(event);
214
- } catch {
215
- // action 持久化失败不应阻断当前 turn。
216
- }
217
- this.publish_event(event);
202
+ await this.state_service.persist_action_event(event);
218
203
  },
219
204
  injectedUserMessages: [],
220
205
  deferredPersistedUserMessages: [],
@@ -227,6 +212,37 @@ export class SessionTurnService {
227
212
  query: executor_query,
228
213
  runContext: run_context,
229
214
  });
215
+ const final_assistant_writer = assistant_writer_ref.current;
216
+ if (final_assistant_writer) {
217
+ if (input.abortSignal?.aborted) {
218
+ await final_assistant_writer.stop();
219
+ } else if (result.success) {
220
+ await final_assistant_writer.complete();
221
+ } else {
222
+ await final_assistant_writer.fail(result.error);
223
+ }
224
+ } else if (result.assistantMessage) {
225
+ const parts = from_ui_assistant_parts(result.assistantMessage.parts);
226
+ if (parts.length > 0) {
227
+ assistant_segment_index += 1;
228
+ const fallback_writer = await this.recorder.open_assistant_message({
229
+ turn_id: input.turnId,
230
+ segment_index: assistant_segment_index,
231
+ });
232
+ for (const part of parts) await fallback_writer.upsert_part(part);
233
+ if (result.success) await fallback_writer.complete();
234
+ else await fallback_writer.fail(result.error);
235
+ }
236
+ }
237
+ if (!result.success && !input.abortSignal?.aborted && result.error) {
238
+ await this.recorder.append_error_message({
239
+ scope: "turn",
240
+ turn_id: input.turnId,
241
+ code: "turn_execution_failed",
242
+ message: result.error,
243
+ recoverable: true,
244
+ });
245
+ }
230
246
  await this.state_service.persist_assistant_result(result.assistantMessage);
231
247
  await this.state_service.persist_deferred_user_messages(
232
248
  result.deferredPersistedUserMessages,
@@ -243,3 +259,16 @@ export class SessionTurnService {
243
259
  };
244
260
  }
245
261
  }
262
+
263
+ function is_assistant_content_chunk(type: string): boolean {
264
+ return (
265
+ type === "text-start" ||
266
+ type === "text-delta" ||
267
+ type === "text-end" ||
268
+ type === "reasoning-start" ||
269
+ type === "reasoning-delta" ||
270
+ type === "reasoning-end" ||
271
+ type.startsWith("tool-") ||
272
+ type === "file"
273
+ );
274
+ }