@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
@@ -0,0 +1,279 @@
1
+ /**
2
+ * SessionMessage 与 AI SDK UIMessage 的内部投影转换。
3
+ *
4
+ * 关键点(中文)
5
+ * - SessionMessage 是唯一持久化事实;UIMessage 只在 Executor/Composer 边界临时构造。
6
+ * - action/error 不转换为模型消息。
7
+ * - part_id 只服务 Session Message identity,不传入模型协议。
8
+ */
9
+
10
+ import type { UIMessage } from "ai";
11
+ import type {
12
+ SessionAssistantMessage,
13
+ SessionAssistantMessagePart,
14
+ SessionMessage,
15
+ SessionUserMessage,
16
+ SessionUserMessagePart,
17
+ } from "@/types/session/SessionMessage.js";
18
+ import type {
19
+ SessionMessageRecordV1,
20
+ SessionMetadataV1,
21
+ } from "@/executor/types/SessionRecords.js";
22
+
23
+ /** 把 Session Message 投影成 Executor 可消费的 UIMessage。 */
24
+ export function to_executor_ui_message(
25
+ message: SessionMessage,
26
+ ): SessionMessageRecordV1 | null {
27
+ if (message.type !== "user" && message.type !== "assistant") return null;
28
+ const metadata: SessionMetadataV1 = {
29
+ v: 1,
30
+ ts: message.updated_at,
31
+ sessionId: message.session_id,
32
+ turnId: message.turn_id,
33
+ source:
34
+ message.type === "user"
35
+ ? "ingress"
36
+ : message.kind === "summary"
37
+ ? "compact"
38
+ : "egress",
39
+ kind:
40
+ message.type === "assistant" && message.kind === "summary"
41
+ ? "summary"
42
+ : "normal",
43
+ extra: {
44
+ sequence: message.sequence,
45
+ revision: message.revision,
46
+ visibility: message.visibility,
47
+ ...(message.type === "user"
48
+ ? { inputType: message.input_type }
49
+ : { segmentIndex: message.segment_index, status: message.status }),
50
+ },
51
+ };
52
+ return {
53
+ id: message.message_id,
54
+ role: message.type,
55
+ metadata,
56
+ parts:
57
+ message.type === "user"
58
+ ? to_ui_user_parts(message.parts)
59
+ : to_ui_assistant_parts(message.parts),
60
+ } as SessionMessageRecordV1;
61
+ }
62
+
63
+ /** 把 AI SDK user parts 转换成可持久化 Session parts。 */
64
+ export function from_ui_user_parts(
65
+ parts: UIMessage["parts"] | null | undefined,
66
+ ): SessionUserMessagePart[] {
67
+ if (!Array.isArray(parts)) return [];
68
+ return parts.flatMap<SessionUserMessagePart>((part, index) => {
69
+ if (!part || typeof part !== "object") return [];
70
+ const candidate = part as Record<string, unknown>;
71
+ if (candidate.type === "text") {
72
+ return [{
73
+ part_id: `user-text:${index + 1}`,
74
+ type: "text" as const,
75
+ text: String(candidate.text || ""),
76
+ state: "done" as const,
77
+ }];
78
+ }
79
+ if (candidate.type === "file") {
80
+ return [{
81
+ part_id: `user-file:${index + 1}`,
82
+ type: "file" as const,
83
+ url: String(candidate.url || ""),
84
+ media_type: String(candidate.mediaType || "application/octet-stream"),
85
+ ...(candidate.filename ? { filename: String(candidate.filename) } : {}),
86
+ }];
87
+ }
88
+ if (String(candidate.type || "").startsWith("data-")) {
89
+ return [{
90
+ part_id: `user-data:${index + 1}`,
91
+ type: "data" as const,
92
+ data_type: String(candidate.type),
93
+ data: to_json_value(candidate.data),
94
+ }];
95
+ }
96
+ return [];
97
+ });
98
+ }
99
+
100
+ /** 把最终 AI SDK assistant parts 转换成 Session parts。 */
101
+ export function from_ui_assistant_parts(
102
+ parts: UIMessage["parts"] | null | undefined,
103
+ ): SessionAssistantMessagePart[] {
104
+ if (!Array.isArray(parts)) return [];
105
+ return parts.flatMap<SessionAssistantMessagePart>((part, index) => {
106
+ if (!part || typeof part !== "object") return [];
107
+ const candidate = part as Record<string, unknown>;
108
+ const type = String(candidate.type || "");
109
+ if (type === "text" || type === "reasoning") {
110
+ return [{
111
+ part_id: `${type}:${index + 1}`,
112
+ type: type as "text" | "reasoning",
113
+ text: String(candidate.text || ""),
114
+ state: "done" as const,
115
+ }];
116
+ }
117
+ if (type === "file") {
118
+ return [{
119
+ part_id: `file:${index + 1}`,
120
+ type: "file" as const,
121
+ url: String(candidate.url || ""),
122
+ media_type: String(candidate.mediaType || "application/octet-stream"),
123
+ ...(candidate.filename ? { filename: String(candidate.filename) } : {}),
124
+ }];
125
+ }
126
+ if (type === "dynamic-tool" || type.startsWith("tool-")) {
127
+ const state = String(candidate.state || "");
128
+ return [{
129
+ part_id: String(candidate.toolCallId || `tool:${index + 1}`),
130
+ type: "tool" as const,
131
+ tool_call_id: String(candidate.toolCallId || `tool:${index + 1}`),
132
+ tool_name:
133
+ String(candidate.toolName || "").trim() ||
134
+ type.replace(/^tool-/, "") ||
135
+ "unknown",
136
+ state:
137
+ state === "output-available"
138
+ ? "completed" as const
139
+ : state === "output-error" || state === "output-denied"
140
+ ? "failed" as const
141
+ : state === "approval-requested"
142
+ ? "approval-required" as const
143
+ : state === "input-streaming"
144
+ ? "input-streaming" as const
145
+ : "running" as const,
146
+ ...(candidate.input !== undefined
147
+ ? { input: to_json_value(candidate.input) }
148
+ : {}),
149
+ ...(candidate.output !== undefined
150
+ ? { output: to_json_value(candidate.output) }
151
+ : {}),
152
+ ...(candidate.errorText ? { error: String(candidate.errorText) } : {}),
153
+ }];
154
+ }
155
+ return [];
156
+ });
157
+ }
158
+
159
+ /** 提取 Assistant Message 的最终可见文本。 */
160
+ export function extract_session_assistant_text(
161
+ message: SessionAssistantMessage | null | undefined,
162
+ ): string {
163
+ if (!message) return "";
164
+ return message.parts
165
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
166
+ .join("")
167
+ .trim();
168
+ }
169
+
170
+ function to_ui_user_parts(parts: SessionUserMessagePart[]): UIMessage["parts"] {
171
+ return parts.map((part) => {
172
+ if (part.type === "text") {
173
+ return { type: "text", text: part.text, state: part.state };
174
+ }
175
+ if (part.type === "file") {
176
+ return {
177
+ type: "file",
178
+ url: part.url,
179
+ mediaType: part.media_type,
180
+ ...(part.filename ? { filename: part.filename } : {}),
181
+ };
182
+ }
183
+ return {
184
+ type: part.data_type,
185
+ data: part.data,
186
+ } as UIMessage["parts"][number];
187
+ });
188
+ }
189
+
190
+ function to_ui_assistant_parts(
191
+ parts: SessionAssistantMessagePart[],
192
+ ): UIMessage["parts"] {
193
+ return parts.map((part) => {
194
+ if (part.type === "text" || part.type === "reasoning") {
195
+ return { type: part.type, text: part.text, state: part.state };
196
+ }
197
+ if (part.type === "file") {
198
+ return {
199
+ type: "file",
200
+ url: part.url,
201
+ mediaType: part.media_type,
202
+ ...(part.filename ? { filename: part.filename } : {}),
203
+ };
204
+ }
205
+ if (part.type === "data") {
206
+ return {
207
+ type: part.data_type,
208
+ data: part.data,
209
+ } as UIMessage["parts"][number];
210
+ }
211
+ if (!("tool_call_id" in part)) {
212
+ throw new Error(`Unsupported Assistant part: ${part.type}`);
213
+ }
214
+ const input = part.input ?? parse_json(part.input_text);
215
+ if (part.state === "completed") {
216
+ return {
217
+ type: "dynamic-tool",
218
+ toolName: part.tool_name,
219
+ toolCallId: part.tool_call_id,
220
+ state: "output-available",
221
+ input,
222
+ output: part.output,
223
+ };
224
+ }
225
+ if (part.state === "failed") {
226
+ return {
227
+ type: "dynamic-tool",
228
+ toolName: part.tool_name,
229
+ toolCallId: part.tool_call_id,
230
+ state: "output-error",
231
+ input,
232
+ errorText: part.error || "Tool failed",
233
+ };
234
+ }
235
+ if (part.state === "approval-required") {
236
+ return {
237
+ type: "dynamic-tool",
238
+ toolName: part.tool_name,
239
+ toolCallId: part.tool_call_id,
240
+ state: "approval-requested",
241
+ input,
242
+ approval: { id: part.approval_id || `approval:${part.tool_call_id}` },
243
+ };
244
+ }
245
+ return {
246
+ type: "dynamic-tool",
247
+ toolName: part.tool_name,
248
+ toolCallId: part.tool_call_id,
249
+ state: part.state === "input-streaming" ? "input-streaming" : "input-available",
250
+ input,
251
+ };
252
+ }) as UIMessage["parts"];
253
+ }
254
+
255
+ function parse_json(input: string | undefined): unknown {
256
+ const value = String(input || "").trim();
257
+ if (!value) return {};
258
+ try {
259
+ return JSON.parse(value);
260
+ } catch {
261
+ return value;
262
+ }
263
+ }
264
+
265
+ function to_json_value(input: unknown) {
266
+ if (input === undefined || input === null) return null;
267
+ if (
268
+ typeof input === "string" ||
269
+ typeof input === "number" ||
270
+ typeof input === "boolean"
271
+ ) {
272
+ return input;
273
+ }
274
+ try {
275
+ return JSON.parse(JSON.stringify(input));
276
+ } catch {
277
+ return String(input);
278
+ }
279
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Session Message Mutation reducer。
3
+ *
4
+ * 历史重放与实时消费者共享 message、part、delta 三种归并语义。
5
+ */
6
+
7
+ import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
8
+ import type { SessionMessageMutation } from "@/types/session/SessionMessageMutation.js";
9
+
10
+ /** 将单条 Mutation 应用到已有 Message。 */
11
+ export function reduce_session_message(
12
+ current_message: SessionMessage | undefined,
13
+ mutation: SessionMessageMutation,
14
+ ): SessionMessage {
15
+ if (mutation.variant === "message") {
16
+ validate_message_snapshot(current_message, mutation);
17
+ return structuredClone(mutation.message);
18
+ }
19
+ if (!current_message) {
20
+ throw new Error(`Session Message does not exist: ${mutation.message_id}`);
21
+ }
22
+ validate_next_revision(current_message, mutation);
23
+ const assistant = require_streaming_assistant(current_message);
24
+
25
+ if (mutation.variant === "part") {
26
+ if (mutation.part.part_id !== mutation.part_id) {
27
+ throw new Error(`Part Mutation identity mismatch: ${mutation.part_id}`);
28
+ }
29
+ if (mutation.part.type !== mutation.type) {
30
+ throw new Error(`Part Mutation type mismatch: ${mutation.part_id}`);
31
+ }
32
+ const exists = assistant.parts.some(
33
+ (part) => part.part_id === mutation.part_id,
34
+ );
35
+ const parts = exists
36
+ ? assistant.parts.map((part) =>
37
+ part.part_id === mutation.part_id
38
+ ? structuredClone(mutation.part)
39
+ : part,
40
+ )
41
+ : [...assistant.parts, structuredClone(mutation.part)];
42
+ return with_revision(assistant, mutation, { parts });
43
+ }
44
+
45
+ const existing_part = assistant.parts.find(
46
+ (part) => part.part_id === mutation.part_id,
47
+ );
48
+ if (
49
+ !existing_part ||
50
+ (existing_part.type !== "text" && existing_part.type !== "reasoning")
51
+ ) {
52
+ throw new Error(`Delta target Part does not exist: ${mutation.part_id}`);
53
+ }
54
+ if (existing_part.type !== mutation.type) {
55
+ throw new Error(`Delta type changed for Part: ${mutation.part_id}`);
56
+ }
57
+ const parts = assistant.parts.map((part) =>
58
+ part.part_id === mutation.part_id &&
59
+ (part.type === "text" || part.type === "reasoning")
60
+ ? { ...part, text: part.text + mutation.delta }
61
+ : part,
62
+ );
63
+ return with_revision(assistant, mutation, { parts });
64
+ }
65
+
66
+ /** 按 commit_sequence 重放 Mutation,恢复线性 Message snapshot。 */
67
+ export function reduce_session_messages(
68
+ mutations: SessionMessageMutation[],
69
+ ): SessionMessage[] {
70
+ const messages = new Map<string, SessionMessage>();
71
+ const ordered = [...mutations].sort(
72
+ (left, right) => left.commit_sequence - right.commit_sequence,
73
+ );
74
+ let previous_commit_sequence = 0;
75
+ for (const mutation of ordered) {
76
+ if (mutation.commit_sequence <= previous_commit_sequence) {
77
+ throw new Error(`Duplicate commit_sequence: ${String(mutation.commit_sequence)}`);
78
+ }
79
+ messages.set(
80
+ mutation.message_id,
81
+ reduce_session_message(messages.get(mutation.message_id), mutation),
82
+ );
83
+ previous_commit_sequence = mutation.commit_sequence;
84
+ }
85
+ return [...messages.values()].sort(
86
+ (left, right) => left.sequence - right.sequence,
87
+ );
88
+ }
89
+
90
+ function validate_message_snapshot(
91
+ current_message: SessionMessage | undefined,
92
+ mutation: Extract<SessionMessageMutation, { variant: "message" }>,
93
+ ): void {
94
+ if (mutation.message.message_id !== mutation.message_id) {
95
+ throw new Error(`Message Mutation identity mismatch: ${mutation.message_id}`);
96
+ }
97
+ if (mutation.message.type !== mutation.type) {
98
+ throw new Error(`Message Mutation type mismatch: ${mutation.message_id}`);
99
+ }
100
+ if (mutation.message.sequence !== mutation.sequence) {
101
+ throw new Error(`Message Mutation sequence mismatch: ${mutation.message_id}`);
102
+ }
103
+ if (mutation.message.revision !== mutation.revision) {
104
+ throw new Error(`Message Mutation revision mismatch: ${mutation.message_id}`);
105
+ }
106
+ if (!current_message) {
107
+ if (mutation.revision !== 1) {
108
+ throw new Error(`New Message revision must be 1: ${mutation.message_id}`);
109
+ }
110
+ return;
111
+ }
112
+ validate_next_revision(current_message, mutation);
113
+ if (current_message.type !== mutation.type) {
114
+ throw new Error(`Message type changed: ${mutation.message_id}`);
115
+ }
116
+ if (current_message.sequence !== mutation.sequence) {
117
+ throw new Error(`Message sequence changed: ${mutation.message_id}`);
118
+ }
119
+ }
120
+
121
+ function validate_next_revision(
122
+ current_message: SessionMessage,
123
+ mutation: SessionMessageMutation,
124
+ ): void {
125
+ if (mutation.revision !== current_message.revision + 1) {
126
+ throw new Error(
127
+ `Invalid Message revision for ${mutation.message_id}: expected ${String(current_message.revision + 1)}, received ${String(mutation.revision)}`,
128
+ );
129
+ }
130
+ }
131
+
132
+ function require_streaming_assistant(
133
+ message: SessionMessage,
134
+ ): SessionAssistantMessage {
135
+ if (message.type !== "assistant") {
136
+ throw new Error(`Session Message is not assistant: ${message.message_id}`);
137
+ }
138
+ if (message.status !== "streaming") {
139
+ throw new Error(`Assistant Message is already closed: ${message.message_id}`);
140
+ }
141
+ return message;
142
+ }
143
+
144
+ function with_revision<TMessage extends SessionMessage>(
145
+ message: TMessage,
146
+ mutation: SessionMessageMutation,
147
+ changes: object,
148
+ ): TMessage {
149
+ return {
150
+ ...message,
151
+ ...changes,
152
+ revision: mutation.revision,
153
+ updated_at: mutation.created_at,
154
+ } as TMessage;
155
+ }