@downcity/agent 1.1.217 → 1.1.224

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 (184) hide show
  1. package/bin/agent/local/Agent.d.ts +1 -34
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +1 -44
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
  6. package/bin/agent/local/services/AgentAssemblyService.js +35 -2
  7. package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
  8. package/bin/agent/local/services/AgentSessions.d.ts +6 -0
  9. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  10. package/bin/agent/local/services/AgentSessions.js +43 -4
  11. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  12. package/bin/agent/remote/RemoteAgent.d.ts +1 -35
  13. package/bin/agent/remote/RemoteAgent.d.ts.map +1 -1
  14. package/bin/agent/remote/RemoteAgent.js +1 -37
  15. package/bin/agent/remote/RemoteAgent.js.map +1 -1
  16. package/bin/agent/remote/RemoteSession.d.ts +26 -41
  17. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  18. package/bin/agent/remote/RemoteSession.js +75 -107
  19. package/bin/agent/remote/RemoteSession.js.map +1 -1
  20. package/bin/agent/remote/RemoteTransport.d.ts +15 -27
  21. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  22. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +10 -21
  23. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  24. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +24 -46
  25. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  26. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +10 -20
  27. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  28. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +10 -16
  29. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  30. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -2
  31. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  32. package/bin/executor/types/SessionHistoryPaths.d.ts +1 -1
  33. package/bin/executor/types/SessionRecords.d.ts +2 -0
  34. package/bin/executor/types/SessionRecords.d.ts.map +1 -1
  35. package/bin/executor/types/SessionRecords.js.map +1 -1
  36. package/bin/index.d.ts +6 -3
  37. package/bin/index.d.ts.map +1 -1
  38. package/bin/index.js +1 -0
  39. package/bin/index.js.map +1 -1
  40. package/bin/rpc/Client.d.ts +12 -34
  41. package/bin/rpc/Client.d.ts.map +1 -1
  42. package/bin/rpc/Client.js +31 -70
  43. package/bin/rpc/Client.js.map +1 -1
  44. package/bin/session/Session.d.ts +22 -4
  45. package/bin/session/Session.d.ts.map +1 -1
  46. package/bin/session/Session.js +95 -21
  47. package/bin/session/Session.js.map +1 -1
  48. package/bin/session/browse/Browse.d.ts.map +1 -1
  49. package/bin/session/browse/Browse.js +49 -18
  50. package/bin/session/browse/Browse.js.map +1 -1
  51. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +51 -0
  52. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
  53. package/bin/session/recorder/JsonlSessionMessageStore.js +231 -0
  54. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -0
  55. package/bin/session/recorder/SessionMessageCodec.d.ts +20 -0
  56. package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -0
  57. package/bin/session/recorder/SessionMessageCodec.js +257 -0
  58. package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
  59. package/bin/session/recorder/SessionRecorder.d.ts +186 -0
  60. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
  61. package/bin/session/recorder/SessionRecorder.js +655 -0
  62. package/bin/session/recorder/SessionRecorder.js.map +1 -0
  63. package/bin/session/recorder/SessionRecorderCompaction.d.ts +18 -0
  64. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -0
  65. package/bin/session/recorder/SessionRecorderCompaction.js +142 -0
  66. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -0
  67. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +66 -0
  68. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -0
  69. package/bin/session/recorder/SessionRecorderHistoryStore.js +195 -0
  70. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -0
  71. package/bin/session/runtime/SessionEventHub.d.ts +12 -5
  72. package/bin/session/runtime/SessionEventHub.d.ts.map +1 -1
  73. package/bin/session/runtime/SessionEventHub.js +11 -6
  74. package/bin/session/runtime/SessionEventHub.js.map +1 -1
  75. package/bin/session/runtime/SessionPromptRuntime.d.ts +9 -15
  76. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  77. package/bin/session/runtime/SessionPromptRuntime.js +48 -30
  78. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  79. package/bin/session/services/SessionStateService.d.ts +9 -5
  80. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  81. package/bin/session/services/SessionStateService.js +138 -74
  82. package/bin/session/services/SessionStateService.js.map +1 -1
  83. package/bin/session/services/SessionTurnService.d.ts +6 -7
  84. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  85. package/bin/session/services/SessionTurnService.js +84 -60
  86. package/bin/session/services/SessionTurnService.js.map +1 -1
  87. package/bin/session/services/SessionViewService.d.ts +8 -3
  88. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  89. package/bin/session/services/SessionViewService.js +10 -5
  90. package/bin/session/services/SessionViewService.js.map +1 -1
  91. package/bin/session/storage/Paths.d.ts +1 -1
  92. package/bin/session/storage/Paths.d.ts.map +1 -1
  93. package/bin/session/storage/Paths.js +2 -2
  94. package/bin/session/storage/Paths.js.map +1 -1
  95. package/bin/session/storage/Persistence.d.ts +6 -0
  96. package/bin/session/storage/Persistence.d.ts.map +1 -1
  97. package/bin/session/storage/Persistence.js +7 -0
  98. package/bin/session/storage/Persistence.js.map +1 -1
  99. package/bin/session/storage/RuntimeSessionPort.d.ts +3 -3
  100. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  101. package/bin/session/storage/RuntimeSessionPort.js +2 -2
  102. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  103. package/bin/types/agent/SessionActor.d.ts +15 -5
  104. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  105. package/bin/types/rpc/RpcProtocol.d.ts +48 -61
  106. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  107. package/bin/types/runtime/agent/AgentContext.d.ts +5 -4
  108. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  109. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  110. package/bin/types/session/SessionApproval.d.ts +97 -0
  111. package/bin/types/session/SessionApproval.d.ts.map +1 -0
  112. package/bin/types/session/SessionApproval.js +7 -0
  113. package/bin/types/session/SessionApproval.js.map +1 -0
  114. package/bin/types/session/SessionMessage.d.ts +223 -0
  115. package/bin/types/session/SessionMessage.d.ts.map +1 -0
  116. package/bin/types/session/SessionMessage.js +8 -0
  117. package/bin/types/session/SessionMessage.js.map +1 -0
  118. package/bin/types/session/SessionMutation.d.ts +116 -0
  119. package/bin/types/session/SessionMutation.d.ts.map +1 -0
  120. package/bin/types/session/SessionMutation.js +19 -0
  121. package/bin/types/session/SessionMutation.js.map +1 -0
  122. package/bin/types/session/SessionOptions.d.ts +9 -0
  123. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  124. package/package.json +3 -3
  125. package/scripts/agent-env.test.mjs +4 -4
  126. package/scripts/remote-session-reconnect.test.mjs +7 -3
  127. package/scripts/session-action-message.test.mjs +98 -27
  128. package/scripts/session-history-inflight.test.mjs +2 -2
  129. package/scripts/session-list-title.test.mjs +6 -19
  130. package/scripts/session-prompt-runtime.test.mjs +7 -19
  131. package/scripts/session-recorder.test.mjs +199 -0
  132. package/scripts/session-title-event.test.mjs +10 -18
  133. package/src/agent/local/Agent.ts +1 -55
  134. package/src/agent/local/services/AgentAssemblyService.ts +39 -3
  135. package/src/agent/local/services/AgentSessions.ts +48 -3
  136. package/src/agent/remote/RemoteAgent.ts +1 -54
  137. package/src/agent/remote/RemoteSession.ts +106 -136
  138. package/src/agent/remote/RemoteTransport.ts +30 -30
  139. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +52 -77
  140. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +30 -34
  141. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -2
  142. package/src/executor/types/SessionHistoryPaths.ts +1 -1
  143. package/src/executor/types/SessionRecords.ts +2 -0
  144. package/src/index.ts +36 -10
  145. package/src/rpc/Client.ts +58 -95
  146. package/src/session/Session.ts +128 -37
  147. package/src/session/browse/Browse.ts +51 -22
  148. package/src/session/recorder/JsonlSessionMessageStore.ts +246 -0
  149. package/src/session/recorder/SessionMessageCodec.ts +284 -0
  150. package/src/session/recorder/SessionRecorder.ts +871 -0
  151. package/src/session/recorder/SessionRecorderCompaction.ts +188 -0
  152. package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
  153. package/src/session/runtime/SessionEventHub.ts +25 -12
  154. package/src/session/runtime/SessionPromptRuntime.ts +60 -43
  155. package/src/session/services/SessionStateService.ts +159 -86
  156. package/src/session/services/SessionTurnService.ts +91 -73
  157. package/src/session/services/SessionViewService.ts +25 -13
  158. package/src/session/storage/Paths.ts +2 -2
  159. package/src/session/storage/Persistence.ts +13 -0
  160. package/src/session/storage/RuntimeSessionPort.ts +8 -9
  161. package/src/types/agent/SessionActor.ts +31 -8
  162. package/src/types/rpc/RpcProtocol.ts +44 -67
  163. package/src/types/runtime/agent/AgentContext.ts +9 -7
  164. package/src/types/session/SessionApproval.ts +108 -0
  165. package/src/types/session/SessionMessage.ts +253 -0
  166. package/src/types/session/SessionMutation.ts +141 -0
  167. package/src/types/session/SessionOptions.ts +25 -0
  168. package/tsconfig.tsbuildinfo +1 -1
  169. package/bin/session/SessionEventMapper.d.ts +0 -32
  170. package/bin/session/SessionEventMapper.d.ts.map +0 -1
  171. package/bin/session/SessionEventMapper.js +0 -131
  172. package/bin/session/SessionEventMapper.js.map +0 -1
  173. package/bin/types/sdk/AgentSessionEvent.d.ts +0 -302
  174. package/bin/types/sdk/AgentSessionEvent.d.ts.map +0 -1
  175. package/bin/types/sdk/AgentSessionEvent.js +0 -9
  176. package/bin/types/sdk/AgentSessionEvent.js.map +0 -1
  177. package/bin/types/sdk/InternalUiChunkEvent.d.ts +0 -100
  178. package/bin/types/sdk/InternalUiChunkEvent.d.ts.map +0 -1
  179. package/bin/types/sdk/InternalUiChunkEvent.js +0 -9
  180. package/bin/types/sdk/InternalUiChunkEvent.js.map +0 -1
  181. package/scripts/session-history-archive.test.mjs +0 -188
  182. package/src/session/SessionEventMapper.ts +0 -148
  183. package/src/types/sdk/AgentSessionEvent.ts +0 -370
  184. package/src/types/sdk/InternalUiChunkEvent.ts +0 -108
@@ -13,7 +13,7 @@ import type {
13
13
  SessionRecordV1,
14
14
  SessionMessageRecordV1,
15
15
  } from "@/executor/types/SessionRecords.js";
16
- import type { AgentSessionEvent } from "@/types/sdk/AgentSessionEvent.js";
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
19
  import type {
@@ -89,28 +89,23 @@ export interface SessionPromptRuntimeOptions {
89
89
  /**
90
90
  * 广播 session 事件。
91
91
  */
92
- publish: (event: AgentSessionEvent) => void;
92
+ publish: (mutation: SessionMutation) => void;
93
93
 
94
94
  /**
95
95
  * 构造并持久化一条 user 消息。
96
96
  */
97
97
  createAndPersistUserMessage: (
98
98
  input: AgentSessionPromptInput,
99
+ turn_id: string,
100
+ input_type: "prompt" | "steer",
99
101
  ) => Promise<SessionUserMessageV1>;
100
102
 
101
- /**
102
- * steer message 成功持久化后发送对应 action。
103
- */
104
- emit_steer_action: (input: {
105
- /**
106
- * steer message 所属的当前 turn 标识。
107
- */
103
+ /** 持久化一条用户可见的 turn 错误 Message。 */
104
+ appendErrorMessage: (input: {
105
+ /** 错误所属 turn。 */
108
106
  turn_id: string;
109
-
110
- /**
111
- * 已成功持久化的 steer message。
112
- */
113
- message: SessionUserMessageV1;
107
+ /** 用户可见错误文本。 */
108
+ message: string;
114
109
  }) => Promise<void>;
115
110
 
116
111
  /**
@@ -141,7 +136,7 @@ export class SessionPromptRuntime {
141
136
  private readonly sessionId: string;
142
137
  private readonly publish: SessionPromptRuntimeOptions["publish"];
143
138
  private readonly createAndPersistUserMessage: SessionPromptRuntimeOptions["createAndPersistUserMessage"];
144
- private readonly emit_steer_action: SessionPromptRuntimeOptions["emit_steer_action"];
139
+ private readonly appendErrorMessage: SessionPromptRuntimeOptions["appendErrorMessage"];
145
140
  private readonly executeTurn: SessionPromptRuntimeOptions["executeTurn"];
146
141
  private readonly stopTurn: SessionPromptRuntimeOptions["stopTurn"];
147
142
  private readonly queue: QueuedPrompt[] = [];
@@ -152,7 +147,7 @@ export class SessionPromptRuntime {
152
147
  this.sessionId = String(options.sessionId || "").trim();
153
148
  this.publish = options.publish;
154
149
  this.createAndPersistUserMessage = options.createAndPersistUserMessage;
155
- this.emit_steer_action = options.emit_steer_action;
150
+ this.appendErrorMessage = options.appendErrorMessage;
156
151
  this.executeTurn = options.executeTurn;
157
152
  this.stopTurn = options.stopTurn;
158
153
  if (!this.sessionId) {
@@ -231,13 +226,18 @@ export class SessionPromptRuntime {
231
226
  const activeTurn = createActiveTurnState(turnId);
232
227
  this.activeTurn = activeTurn;
233
228
  this.publish({
234
- type: "turn-start",
235
- turnId,
229
+ mutation_id: nanoid(),
230
+ variant: "turn",
231
+ type: "start",
232
+ session_id: this.sessionId,
233
+ turn_id: turnId,
234
+ status: "running",
235
+ created_at: Date.now(),
236
236
  });
237
237
  current.deferredHandle.resolve(createTurnHandle(activeTurn));
238
238
 
239
239
  try {
240
- await this.createAndPersistUserMessage(current.input);
240
+ await this.createAndPersistUserMessage(current.input, turnId, "prompt");
241
241
  const result = await this.executeTurn({
242
242
  turnId,
243
243
  promptInput: current.input,
@@ -260,10 +260,14 @@ export class SessionPromptRuntime {
260
260
  };
261
261
  activeTurn.result = finalResult;
262
262
  this.publish({
263
- type: "turn-finish",
264
- turnId,
263
+ mutation_id: nanoid(),
264
+ variant: "turn",
265
+ type: "finish",
266
+ session_id: this.sessionId,
267
+ turn_id: turnId,
268
+ status: stopped ? "stopped" : finalResult.success ? "completed" : "failed",
269
+ created_at: Date.now(),
265
270
  text: finalResult.text,
266
- success: finalResult.success,
267
271
  ...(finalResult.error ? { error: finalResult.error } : {}),
268
272
  });
269
273
  activeTurn.deferredFinished.resolve(finalResult);
@@ -283,16 +287,24 @@ export class SessionPromptRuntime {
283
287
  };
284
288
  activeTurn.result = finalResult;
285
289
  if (message !== TURN_STOPPED_MESSAGE) {
286
- this.publish({
287
- type: "error",
288
- message,
289
- });
290
+ try {
291
+ await this.appendErrorMessage({
292
+ turn_id: turnId,
293
+ message,
294
+ });
295
+ } catch {
296
+ // Error Message 写入失败不能阻止 turn handle 收口。
297
+ }
290
298
  }
291
299
  this.publish({
292
- type: "turn-finish",
293
- turnId,
300
+ mutation_id: nanoid(),
301
+ variant: "turn",
302
+ type: "finish",
303
+ session_id: this.sessionId,
304
+ turn_id: turnId,
305
+ status: activeTurn.abortController.signal.aborted ? "stopped" : "failed",
306
+ created_at: Date.now(),
294
307
  text: "",
295
- success: false,
296
308
  error: message,
297
309
  });
298
310
  activeTurn.deferredFinished.resolve(finalResult);
@@ -323,14 +335,23 @@ export class SessionPromptRuntime {
323
335
  cancelledTurn.result = finalResult;
324
336
  cancelledTurn.deferredFinished.resolve(finalResult);
325
337
  this.publish({
326
- type: "turn-start",
327
- turnId,
338
+ mutation_id: nanoid(),
339
+ variant: "turn",
340
+ type: "start",
341
+ session_id: this.sessionId,
342
+ turn_id: turnId,
343
+ status: "running",
344
+ created_at: Date.now(),
328
345
  });
329
346
  this.publish({
330
- type: "turn-finish",
331
- turnId,
347
+ mutation_id: nanoid(),
348
+ variant: "turn",
349
+ type: "finish",
350
+ session_id: this.sessionId,
351
+ turn_id: turnId,
352
+ status: "failed",
353
+ created_at: Date.now(),
332
354
  text: "",
333
- success: false,
334
355
  error: QUEUED_PROMPT_CANCELLED_MESSAGE,
335
356
  });
336
357
  item.deferredHandle.resolve(createTurnHandle(cancelledTurn));
@@ -348,17 +369,13 @@ export class SessionPromptRuntime {
348
369
  for (let index = 0; index < drained.length; index += 1) {
349
370
  const item = drained[index];
350
371
  try {
351
- const message = await this.createAndPersistUserMessage(item.input);
372
+ const message = await this.createAndPersistUserMessage(
373
+ item.input,
374
+ activeTurn.turnId,
375
+ "steer",
376
+ );
352
377
  item.deferredHandle.resolve(createTurnHandle(activeTurn));
353
378
  merged.push(message);
354
- try {
355
- await this.emit_steer_action({
356
- turn_id: activeTurn.turnId,
357
- message,
358
- });
359
- } catch {
360
- // action 发送失败不应阻断已经持久化的 steer message。
361
- }
362
379
  } catch {
363
380
  // 关键点(中文):若某条消息持久化失败,把未处理部分重新放回队列头部,避免静默丢失。
364
381
  const remaining = drained.slice(index);
@@ -19,17 +19,14 @@ 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,
29
28
  } from "@/types/agent/SessionTypes.js";
30
- import type {
31
- AgentSessionEvent,
32
- } from "@/types/sdk/AgentSessionEvent.js";
29
+ import type { SessionMutation } from "@/types/session/SessionMutation.js";
33
30
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
34
31
  import type {
35
32
  SessionActionRecordInputV1,
@@ -38,11 +35,20 @@ import type {
38
35
  SessionMessageRecordV1,
39
36
  SessionUserMessageV1,
40
37
  } from "@/executor/types/SessionRecords.js";
41
- import { to_session_action_record } from "@/executor/types/SessionRecords.js";
42
38
  import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
43
39
  import { generateId } from "@/utils/Id.js";
44
40
  import type { Logger } from "@/utils/logger/Logger.js";
45
41
  import type { AgentModel } from "@/model/CityModelAdapter.js";
42
+ import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
43
+ import { normalize_session_user_parts } from "@/session/recorder/SessionRecorder.js";
44
+ import {
45
+ from_ui_assistant_parts,
46
+ from_ui_user_parts,
47
+ to_executor_ui_message,
48
+ } from "@/session/recorder/SessionMessageCodec.js";
49
+ import fs from "fs-extra";
50
+ import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
51
+ import type { SessionMessage } from "@/types/session/SessionMessage.js";
46
52
 
47
53
  type SessionStateServiceOptions = {
48
54
  /**
@@ -63,7 +69,10 @@ type SessionStateServiceOptions = {
63
69
  /**
64
70
  * 当前 session 历史事实源。
65
71
  */
66
- history_store: JsonlSessionHistoryStore;
72
+ history_store: SessionHistoryStore;
73
+
74
+ /** 当前 Session Message Recorder。 */
75
+ recorder: SessionRecorder;
67
76
 
68
77
  /**
69
78
  * 当前 session 执行器。
@@ -91,7 +100,7 @@ type SessionStateServiceOptions = {
91
100
  /**
92
101
  * 发布 session 事件。
93
102
  */
94
- publish_event: (event: AgentSessionEvent) => void;
103
+ publish_event: (mutation: SessionMutation) => void;
95
104
  };
96
105
 
97
106
  type SessionSetOptions = {
@@ -114,7 +123,8 @@ export class SessionStateService {
114
123
  private readonly agent_id: string;
115
124
  private readonly project_root: string;
116
125
  private readonly session_id: string;
117
- private readonly history_store: JsonlSessionHistoryStore;
126
+ private readonly history_store: SessionHistoryStore;
127
+ private readonly recorder: SessionRecorder;
118
128
  private readonly executor: Executor;
119
129
  private readonly state: SessionLocalState;
120
130
  private readonly logger: Logger;
@@ -127,6 +137,7 @@ export class SessionStateService {
127
137
  this.project_root = options.project_root;
128
138
  this.session_id = options.session_id;
129
139
  this.history_store = options.history_store;
140
+ this.recorder = options.recorder;
130
141
  this.executor = options.executor;
131
142
  this.state = options.state;
132
143
  this.logger = options.logger;
@@ -197,6 +208,7 @@ export class SessionStateService {
197
208
  : {}),
198
209
  ...(metadata.modelId ? { modelId: metadata.modelId } : {}),
199
210
  };
211
+ await this.restore_persisted_model();
200
212
  })();
201
213
  await this.state.initializePromise;
202
214
  }
@@ -206,7 +218,6 @@ export class SessionStateService {
206
218
  */
207
219
  async ensure_ready_for_execution(): Promise<void> {
208
220
  await this.initialize();
209
- await this.restore_persisted_model();
210
221
  if (this.state.ensureConfiguredPromise) {
211
222
  await this.state.ensureConfiguredPromise;
212
223
  return;
@@ -239,6 +250,9 @@ export class SessionStateService {
239
250
  async set(input: AgentSessionSetInput, options?: SessionSetOptions): Promise<void> {
240
251
  const should_emit_action = options?.emit_action !== false;
241
252
  const previous_model_label = this.state.sessionConfig.modelLabel;
253
+ const previous_model_id = String(
254
+ this.state.sessionConfig.modelId || previous_model_label || "",
255
+ ).trim();
242
256
  const requested_model_id = String(input.modelId || "").trim();
243
257
  const next_model = input.model || (requested_model_id
244
258
  ? await this.resolve_model_by_id(requested_model_id)
@@ -251,19 +265,18 @@ export class SessionStateService {
251
265
  next_model &&
252
266
  should_emit_action &&
253
267
  this.state.sessionConfig.model &&
254
- previous_model_label &&
255
- next_model_label &&
256
- previous_model_label !== next_model_label,
268
+ previous_model_id &&
269
+ next_model_id &&
270
+ previous_model_id !== next_model_id,
257
271
  );
272
+ const previous_model_name = previous_model_label || previous_model_id;
273
+ const next_model_name = next_model_label || next_model_id;
258
274
  const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
259
275
 
260
276
  if (should_emit_model_switch_action) {
261
277
  await this.emit_action_event({
262
278
  id: action_id,
263
- title: "Switching session model",
264
- description: next_model_label
265
- ? `Switching to ${next_model_label}.`
266
- : undefined,
279
+ title: `Switching session model from ${previous_model_name} to ${next_model_name}`,
267
280
  state: "running",
268
281
  });
269
282
  }
@@ -287,7 +300,7 @@ export class SessionStateService {
287
300
  if (should_emit_model_switch_action) {
288
301
  await this.emit_action_event({
289
302
  id: action_id,
290
- title: "Session model switch failed",
303
+ title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
291
304
  description: error instanceof Error ? error.message : String(error),
292
305
  state: "failed",
293
306
  });
@@ -298,10 +311,7 @@ export class SessionStateService {
298
311
  if (should_emit_model_switch_action) {
299
312
  await this.emit_action_event({
300
313
  id: action_id,
301
- title: "Session model switched",
302
- description: this.state.sessionConfig.modelLabel
303
- ? `Using ${this.state.sessionConfig.modelLabel}.`
304
- : undefined,
314
+ title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
305
315
  state: "completed",
306
316
  });
307
317
  }
@@ -331,7 +341,20 @@ export class SessionStateService {
331
341
  message?: SessionRecordV1 | null;
332
342
  text?: string;
333
343
  }): Promise<void> {
334
- await this.executor.append_user_message(params);
344
+ const parts = params.message && "role" in params.message
345
+ ? from_ui_user_parts(params.message.parts)
346
+ : [{
347
+ part_id: `external-user-text:${Date.now()}`,
348
+ type: "text" as const,
349
+ text: String(params.text || "").trim(),
350
+ state: "done" as const,
351
+ }];
352
+ if (parts.length === 0) return;
353
+ await this.recorder.append_user_message({
354
+ turn_id: `external:${this.session_id}:${Date.now()}`,
355
+ input_type: "prompt",
356
+ parts,
357
+ });
335
358
  await this.ensure_title_from_history({ generate: true });
336
359
  await this.touch_metadata();
337
360
  }
@@ -343,7 +366,17 @@ export class SessionStateService {
343
366
  message?: SessionRecordV1 | null;
344
367
  fallbackText?: string;
345
368
  }): Promise<void> {
346
- await this.executor.append_assistant_message(params);
369
+ const parts = params.message && "role" in params.message
370
+ ? from_ui_assistant_parts(params.message.parts)
371
+ : [{
372
+ part_id: `external-assistant-text:${Date.now()}`,
373
+ sequence: 1,
374
+ type: "text" as const,
375
+ text: String(params.fallbackText || "").trim(),
376
+ state: "done" as const,
377
+ }];
378
+ if (parts.length === 0) return;
379
+ await this.recorder.append_completed_assistant_message({ parts });
347
380
  await this.touch_metadata();
348
381
  }
349
382
 
@@ -351,11 +384,29 @@ export class SessionStateService {
351
384
  * 仅刷新当前 session metadata。
352
385
  */
353
386
  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);
399
+ const preview_text = resolve_message_preview(
400
+ message_page.items[message_page.items.length - 1],
401
+ ).slice(0, 180);
354
402
  await touchSessionMetadata({
355
403
  projectRoot: this.project_root,
356
404
  agentId: this.agent_id,
357
405
  sessionId: this.session_id,
358
406
  sessionConfig: this.state.sessionConfig,
407
+ message_count: message_page.total,
408
+ history_bytes,
409
+ ...(preview_text ? { preview_text } : {}),
359
410
  });
360
411
  }
361
412
 
@@ -390,8 +441,11 @@ export class SessionStateService {
390
441
  const next_title = String(next_metadata.title || "").trim();
391
442
  if (!next_title || next_title === before_title) return;
392
443
  this.publish_event({
393
- type: "session-title",
394
- sessionId: this.session_id,
444
+ mutation_id: generateId(),
445
+ variant: "session",
446
+ type: "title",
447
+ session_id: this.session_id,
448
+ created_at: Date.now(),
395
449
  title: next_title,
396
450
  });
397
451
  }
@@ -402,14 +456,8 @@ export class SessionStateService {
402
456
  async persist_assistant_result(
403
457
  assistant_message?: SessionMessageRecordV1 | null,
404
458
  ): Promise<void> {
405
- await persistSdkAssistantResult({
406
- projectRoot: this.project_root,
407
- agentId: this.agent_id,
408
- sessionId: this.session_id,
409
- sessionConfig: this.state.sessionConfig,
410
- executor: this.executor,
411
- assistantMessage: assistant_message,
412
- });
459
+ void assistant_message;
460
+ await this.touch_metadata();
413
461
  }
414
462
 
415
463
  /**
@@ -418,8 +466,23 @@ export class SessionStateService {
418
466
  async persist_action_event(
419
467
  event: SessionActionRecordV1,
420
468
  ): Promise<void> {
421
- const message = to_session_action_record(event, this.session_id);
422
- await this.history_store.write_record(message);
469
+ const existing = this.recorder.get_message(event.id);
470
+ if (!existing) {
471
+ const writer = await this.recorder.open_action_message({
472
+ message_id: event.id,
473
+ turn_id: event.metadata.turnId,
474
+ action_type: infer_action_type(event.id),
475
+ title: event.title,
476
+ description: event.description,
477
+ });
478
+ if (event.state === "completed") await writer.complete();
479
+ if (event.state === "failed") await writer.fail(event.description || event.title);
480
+ } else if (existing.type === "action" && event.state !== "running") {
481
+ await this.recorder.update_action_message(event.id, event.state, {
482
+ title: event.title,
483
+ description: event.description,
484
+ });
485
+ }
423
486
  await this.touch_metadata();
424
487
  }
425
488
 
@@ -427,21 +490,26 @@ export class SessionStateService {
427
490
  * 写入并发布一条 action。
428
491
  */
429
492
  async emit_action_event(input: EmitActionInput): Promise<void> {
430
- const event = to_session_action_record(
431
- {
432
- ...input,
433
- id:
434
- String(input.id || "").trim() ||
435
- `action:${this.session_id}:${Date.now()}:${generateId()}`,
493
+ const action_id = String(input.id || "").trim() ||
494
+ `action:${this.session_id}:${Date.now()}`;
495
+ const turn_id = "turnId" in input
496
+ ? input.turnId
497
+ : input.metadata?.turnId;
498
+ await this.persist_action_event({
499
+ type: "action",
500
+ id: action_id,
501
+ title: input.title,
502
+ ...(input.description ? { description: input.description } : {}),
503
+ state: input.state,
504
+ metadata: {
505
+ v: 1,
506
+ ts: input.metadata?.ts || Date.now(),
507
+ sessionId: this.session_id,
508
+ ...(turn_id
509
+ ? { turnId: turn_id }
510
+ : {}),
436
511
  },
437
- this.session_id,
438
- );
439
- try {
440
- await this.persist_action_event(event);
441
- } catch {
442
- // action 持久化失败不应阻断宿主操作。
443
- }
444
- this.publish_event(event);
512
+ });
445
513
  }
446
514
 
447
515
  /**
@@ -449,43 +517,22 @@ export class SessionStateService {
449
517
  */
450
518
  async create_and_persist_user_prompt_message(
451
519
  input: AgentSessionPromptInput,
520
+ turn_id: string,
521
+ input_type: "prompt" | "steer" = "prompt",
452
522
  ): Promise<SessionUserMessageV1> {
453
523
  const query = input.query;
454
- if (typeof query === "string") {
455
- const message = this.history_store.userText({
456
- text: query.trim(),
457
- metadata: {
458
- sessionId: this.session_id,
459
- },
460
- }) as SessionUserMessageV1;
461
- await this.executor.append_user_message({
462
- message,
463
- });
464
- await this.ensure_title_from_history({ generate: true });
465
- await this.touch_metadata();
466
- return message;
467
- }
468
-
469
- // query 是 parts 数组,先把本地图片附件转换为模型可消费的 data URL。
470
- const parts = await hydrateUserPromptFileParts(
471
- Array.isArray(query) ? query : [],
472
- this.project_root,
473
- );
474
- const message: SessionUserMessageV1 = {
475
- id: `u:${this.session_id}:${generateId()}`,
476
- role: "user",
477
- parts,
478
- metadata: {
479
- v: 1,
480
- ts: Date.now(),
481
- sessionId: this.session_id,
482
- source: "ingress",
483
- kind: "normal",
484
- },
485
- };
486
- await this.executor.append_user_message({
487
- message,
524
+ const ui_parts = typeof query === "string"
525
+ ? [{ type: "text" as const, text: query.trim() }]
526
+ : await hydrateUserPromptFileParts(
527
+ Array.isArray(query) ? query : [],
528
+ this.project_root,
529
+ );
530
+ const canonical = await this.recorder.append_user_message({
531
+ turn_id,
532
+ input_type,
533
+ parts: normalize_session_user_parts(ui_parts),
488
534
  });
535
+ const message = to_executor_ui_message(canonical) as SessionUserMessageV1;
489
536
  await this.ensure_title_from_history({ generate: true });
490
537
  await this.touch_metadata();
491
538
  return message;
@@ -502,10 +549,36 @@ export class SessionStateService {
502
549
  : [];
503
550
  if (normalized_messages.length <= 0) return;
504
551
  for (const message of normalized_messages) {
505
- await this.executor.append_user_message({
506
- message,
552
+ await this.recorder.append_user_message({
553
+ turn_id: String(message.metadata?.turnId || `deferred:${this.session_id}:${Date.now()}`),
554
+ input_type: "steer",
555
+ parts: from_ui_user_parts(message.parts),
507
556
  });
508
557
  }
509
558
  await this.touch_metadata();
510
559
  }
511
560
  }
561
+
562
+ function resolve_message_preview(message: SessionMessage | undefined): string {
563
+ if (!message) return "";
564
+ if (message.type === "user") {
565
+ return message.parts
566
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
567
+ .join("")
568
+ .trim();
569
+ }
570
+ if (message.type === "assistant") {
571
+ return message.parts
572
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
573
+ .join("")
574
+ .trim();
575
+ }
576
+ if (message.type === "action") {
577
+ return [message.title, message.description].filter(Boolean).join("\n");
578
+ }
579
+ return message.message.trim();
580
+ }
581
+
582
+ function infer_action_type(message_id: string): string {
583
+ return String(message_id || "").split(":")[0] || "action";
584
+ }