@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
@@ -0,0 +1,246 @@
1
+ /**
2
+ * SessionMessage 的 JSONL 快照存储。
3
+ *
4
+ * messages.jsonl 只保存完整 Message 快照;运行中的唯一 Assistant 使用
5
+ * assistant_message.json 原子覆盖,避免把每个 delta 写入历史文件。
6
+ */
7
+
8
+ import fs from "fs-extra";
9
+ import { open, readFile, stat } from "node:fs/promises";
10
+ import path from "node:path";
11
+ import { generateId } from "@/utils/Id.js";
12
+ import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
13
+
14
+ /** JSONL 快照存储构造参数。 */
15
+ export interface JsonlSessionMessageStoreOptions {
16
+ /** 当前 Session 标识。 */
17
+ session_id: string;
18
+ /** messages.jsonl 的绝对或相对路径。 */
19
+ file_path: string;
20
+ /** Assistant 运行中快照路径,默认与 messages.jsonl 同目录。 */
21
+ assistant_message_file_path?: string;
22
+ }
23
+
24
+ /** 在写锁内计算新 Message 所需的状态。 */
25
+ export interface SessionMessageCommitState {
26
+ /** 下一条新 Message 的线性顺序。 */
27
+ message_sequence: number;
28
+ /** 当前已持久化的完整 Message 快照。 */
29
+ messages: SessionMessage[];
30
+ }
31
+
32
+ /** 单一 JSONL 快照存储。 */
33
+ export class JsonlSessionMessageStore {
34
+ readonly session_id: string;
35
+ readonly messages_file_path: string;
36
+ readonly assistant_message_file_path: string;
37
+ private readonly lock_file_path: string;
38
+
39
+ constructor(options: JsonlSessionMessageStoreOptions) {
40
+ this.session_id = String(options.session_id || "").trim();
41
+ this.messages_file_path = path.resolve(options.file_path);
42
+ this.assistant_message_file_path = path.resolve(
43
+ options.assistant_message_file_path ||
44
+ path.join(path.dirname(this.messages_file_path), "assistant_message.json"),
45
+ );
46
+ this.lock_file_path = `${this.messages_file_path}.lock`;
47
+ if (!this.session_id) throw new Error("JsonlSessionMessageStore requires session_id");
48
+ }
49
+
50
+ /** 创建目录和文件,并验证已有快照。 */
51
+ async initialize(): Promise<void> {
52
+ await this.ensure_layout();
53
+ const messages = await this.read_messages_unsafe();
54
+ const draft = await this.read_assistant_message();
55
+ if (draft && draft.session_id !== this.session_id) {
56
+ throw new Error("assistant_message.json session_id mismatch");
57
+ }
58
+ const finalized = draft && messages.find((message) => message.message_id === draft.message_id);
59
+ if (draft && finalized && finalized.revision >= draft.revision && finalized.type === "assistant" && finalized.status !== "streaming") {
60
+ await fs.remove(this.assistant_message_file_path);
61
+ }
62
+ }
63
+
64
+ /** 读取 JSONL 中每一行完整快照,并按 Message revision 折叠。 */
65
+ async list_messages(): Promise<SessionMessage[]> {
66
+ await this.ensure_layout();
67
+ const by_id = new Map<string, SessionMessage>();
68
+ const raw = await fs.readFile(this.messages_file_path, "utf8").catch(() => "");
69
+ for (const [index, line] of raw.split("\n").entries()) {
70
+ const value = line.trim();
71
+ if (!value) continue;
72
+ try {
73
+ const message = JSON.parse(value) as SessionMessage;
74
+ this.validate_message(message);
75
+ const previous = by_id.get(message.message_id);
76
+ if (!previous || message.revision > previous.revision) by_id.set(message.message_id, message);
77
+ } catch (error) {
78
+ throw new Error(`Invalid session message at line ${String(index + 1)}: ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ }
81
+ const draft = await this.read_assistant_message();
82
+ if (draft) {
83
+ const previous = by_id.get(draft.message_id);
84
+ if (!previous || draft.revision > previous.revision) by_id.set(draft.message_id, draft);
85
+ }
86
+ return [...by_id.values()].sort((left, right) => left.sequence - right.sequence);
87
+ }
88
+
89
+ /** 读取当前运行中的 Assistant 草稿。 */
90
+ async read_assistant_message(): Promise<SessionAssistantMessage | null> {
91
+ try {
92
+ const value = await fs.readJson(this.assistant_message_file_path) as SessionAssistantMessage;
93
+ this.validate_message(value);
94
+ if (value.type !== "assistant" || value.status !== "streaming") return null;
95
+ return value;
96
+ } catch (error) {
97
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
98
+ if (error instanceof SyntaxError) throw error;
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /** 原子覆盖运行中的 Assistant 草稿。 */
104
+ async write_assistant_message(message: SessionAssistantMessage): Promise<void> {
105
+ this.validate_message(message);
106
+ if (message.status !== "streaming") throw new Error("Assistant draft must be streaming");
107
+ await this.with_write_lock(async () => {
108
+ const current = await this.read_assistant_message();
109
+ if (!current || current.message_id !== message.message_id) {
110
+ throw new Error(`Assistant draft does not exist: ${message.message_id}`);
111
+ }
112
+ if (message.revision !== current.revision + 1) {
113
+ throw new Error(`Invalid Assistant draft revision: ${message.message_id}`);
114
+ }
115
+ await this.write_assistant_message_unsafe(message);
116
+ });
117
+ }
118
+
119
+ /** 在写锁内分配 Message sequence 并创建唯一 Assistant 草稿。 */
120
+ async create_assistant_message(
121
+ build_message: (state: SessionMessageCommitState) => SessionAssistantMessage,
122
+ ): Promise<SessionAssistantMessage> {
123
+ return await this.with_write_lock(async () => {
124
+ const current = await this.read_assistant_message();
125
+ if (current) throw new Error(`Assistant draft already exists: ${current.message_id}`);
126
+ const messages = await this.read_messages_unsafe();
127
+ const message = build_message({
128
+ message_sequence: messages.reduce((max, item) => Math.max(max, item.sequence + 1), 1),
129
+ messages,
130
+ });
131
+ this.validate_message(message);
132
+ if (message.status !== "streaming" || message.revision !== 1) {
133
+ throw new Error("New Assistant draft must be streaming at revision 1");
134
+ }
135
+ await this.write_assistant_message_unsafe(message);
136
+ return message;
137
+ });
138
+ }
139
+
140
+ /** 创建新 Message 或追加已有 Message revision 快照。 */
141
+ async append_message(
142
+ build_message: (state: SessionMessageCommitState) => SessionMessage,
143
+ ): Promise<SessionMessage> {
144
+ return await this.with_write_lock(async () => {
145
+ const messages = await this.read_messages_unsafe();
146
+ const draft = await this.read_assistant_message();
147
+ const current_messages = draft ? [...messages, draft] : messages;
148
+ const message = build_message({
149
+ message_sequence: current_messages.reduce((max, item) => Math.max(max, item.sequence + 1), 1),
150
+ messages: current_messages,
151
+ });
152
+ this.validate_message(message);
153
+ await fs.appendFile(this.messages_file_path, `${JSON.stringify(message)}\n`, "utf8");
154
+ return message;
155
+ });
156
+ }
157
+
158
+ /** 将 Assistant 草稿最终追加为历史快照,并删除草稿文件。 */
159
+ async finalize_assistant_message(message: SessionAssistantMessage): Promise<void> {
160
+ this.validate_message(message);
161
+ if (message.status === "streaming") throw new Error("Final Assistant message cannot be streaming");
162
+ await this.with_write_lock(async () => {
163
+ const current = await this.read_assistant_message();
164
+ if (!current || current.message_id !== message.message_id) {
165
+ throw new Error(`Assistant draft does not exist: ${message.message_id}`);
166
+ }
167
+ if (message.revision !== current.revision + 1) {
168
+ throw new Error(`Invalid final Assistant revision: ${message.message_id}`);
169
+ }
170
+ await fs.appendFile(this.messages_file_path, `${JSON.stringify(message)}\n`, "utf8");
171
+ await fs.remove(this.assistant_message_file_path);
172
+ });
173
+ }
174
+
175
+ private async read_messages_unsafe(): Promise<SessionMessage[]> {
176
+ const raw = await fs.readFile(this.messages_file_path, "utf8").catch(() => "");
177
+ const by_id = new Map<string, SessionMessage>();
178
+ for (const line of raw.split("\n")) {
179
+ if (!line.trim()) continue;
180
+ const message = JSON.parse(line) as SessionMessage;
181
+ this.validate_message(message);
182
+ const previous = by_id.get(message.message_id);
183
+ if (!previous || message.revision > previous.revision) by_id.set(message.message_id, message);
184
+ }
185
+ return [...by_id.values()].sort((left, right) => left.sequence - right.sequence);
186
+ }
187
+
188
+ private async write_assistant_message_unsafe(message: SessionAssistantMessage): Promise<void> {
189
+ const temporary_path = `${this.assistant_message_file_path}.${process.pid}.${Date.now()}.tmp`;
190
+ await fs.ensureDir(path.dirname(this.assistant_message_file_path));
191
+ await fs.writeJson(temporary_path, message, { spaces: 2 });
192
+ await fs.move(temporary_path, this.assistant_message_file_path, { overwrite: true });
193
+ }
194
+
195
+ private validate_message(message: SessionMessage): void {
196
+ if (!message || typeof message !== "object") throw new Error("message must be an object");
197
+ if (message.session_id !== this.session_id) throw new Error("session_id mismatch");
198
+ if (!message.message_id || !Number.isInteger(message.sequence) || !Number.isInteger(message.revision)) {
199
+ throw new Error("message identity, sequence and revision are required");
200
+ }
201
+ if (message.type === "assistant") {
202
+ const sequences = new Set<number>();
203
+ for (const part of message.parts) {
204
+ if (!Number.isInteger(part.sequence) || sequences.has(part.sequence)) {
205
+ throw new Error(`invalid Assistant part sequence: ${part.part_id}`);
206
+ }
207
+ sequences.add(part.sequence);
208
+ }
209
+ }
210
+ }
211
+
212
+ private async ensure_layout(): Promise<void> {
213
+ await fs.ensureDir(path.dirname(this.messages_file_path));
214
+ await fs.ensureFile(this.messages_file_path);
215
+ }
216
+
217
+ private async with_write_lock<T>(callback: () => Promise<T>): Promise<T> {
218
+ await this.ensure_layout();
219
+ const token = `${process.pid}:${Date.now()}:${generateId()}`;
220
+ const stale_ms = 30_000;
221
+ const started_at = Date.now();
222
+ while (true) {
223
+ try {
224
+ const file = await open(this.lock_file_path, "wx");
225
+ await file.writeFile(token, "utf8");
226
+ await file.close();
227
+ break;
228
+ } catch (error) {
229
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
230
+ try {
231
+ const lock_stat = await stat(this.lock_file_path);
232
+ if (Date.now() - lock_stat.mtimeMs > stale_ms) await fs.remove(this.lock_file_path);
233
+ } catch { /* 锁文件可能刚被释放。 */ }
234
+ if (Date.now() - started_at > stale_ms * 2) throw new Error(`Session message lock timeout: ${this.lock_file_path}`);
235
+ await new Promise((resolve) => setTimeout(resolve, 40));
236
+ }
237
+ }
238
+ try {
239
+ return await callback();
240
+ } finally {
241
+ try {
242
+ if ((await readFile(this.lock_file_path, "utf8")).trim() === token) await fs.remove(this.lock_file_path);
243
+ } catch { /* 锁已被清理。 */ }
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,284 @@
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
+ sequence: index + 1,
113
+ type: type as "text" | "reasoning",
114
+ text: String(candidate.text || ""),
115
+ state: "done" as const,
116
+ }];
117
+ }
118
+ if (type === "file") {
119
+ return [{
120
+ part_id: `file:${index + 1}`,
121
+ sequence: index + 1,
122
+ type: "file" as const,
123
+ url: String(candidate.url || ""),
124
+ media_type: String(candidate.mediaType || "application/octet-stream"),
125
+ ...(candidate.filename ? { filename: String(candidate.filename) } : {}),
126
+ }];
127
+ }
128
+ if (type === "dynamic-tool" || type.startsWith("tool-")) {
129
+ const state = String(candidate.state || "");
130
+ return [{
131
+ part_id: String(candidate.toolCallId || `tool:${index + 1}`),
132
+ sequence: index + 1,
133
+ type: "tool" as const,
134
+ tool_call_id: String(candidate.toolCallId || `tool:${index + 1}`),
135
+ tool_name:
136
+ String(candidate.toolName || "").trim() ||
137
+ type.replace(/^tool-/, "") ||
138
+ "unknown",
139
+ state:
140
+ state === "output-available"
141
+ ? "completed" as const
142
+ : state === "output-error" || state === "output-denied"
143
+ ? "failed" as const
144
+ : state === "approval-requested"
145
+ ? "approval-required" as const
146
+ : state === "input-streaming"
147
+ ? "input-streaming" as const
148
+ : state === "input-available"
149
+ ? "ready" as const
150
+ : "running" as const,
151
+ ...(candidate.input !== undefined
152
+ ? { input: to_json_value(candidate.input) }
153
+ : {}),
154
+ ...(candidate.output !== undefined
155
+ ? { output: to_json_value(candidate.output) }
156
+ : {}),
157
+ ...(candidate.errorText ? { error: String(candidate.errorText) } : {}),
158
+ }];
159
+ }
160
+ return [];
161
+ });
162
+ }
163
+
164
+ /** 提取 Assistant Message 的最终可见文本。 */
165
+ export function extract_session_assistant_text(
166
+ message: SessionAssistantMessage | null | undefined,
167
+ ): string {
168
+ if (!message) return "";
169
+ return message.parts
170
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
171
+ .join("")
172
+ .trim();
173
+ }
174
+
175
+ function to_ui_user_parts(parts: SessionUserMessagePart[]): UIMessage["parts"] {
176
+ return parts.map((part) => {
177
+ if (part.type === "text") {
178
+ return { type: "text", text: part.text, state: part.state };
179
+ }
180
+ if (part.type === "file") {
181
+ return {
182
+ type: "file",
183
+ url: part.url,
184
+ mediaType: part.media_type,
185
+ ...(part.filename ? { filename: part.filename } : {}),
186
+ };
187
+ }
188
+ return {
189
+ type: part.data_type,
190
+ data: part.data,
191
+ } as UIMessage["parts"][number];
192
+ });
193
+ }
194
+
195
+ function to_ui_assistant_parts(
196
+ parts: SessionAssistantMessagePart[],
197
+ ): UIMessage["parts"] {
198
+ return parts.map((part) => {
199
+ if (part.type === "text" || part.type === "reasoning") {
200
+ return { type: part.type, text: part.text, state: part.state };
201
+ }
202
+ if (part.type === "file") {
203
+ return {
204
+ type: "file",
205
+ url: part.url,
206
+ mediaType: part.media_type,
207
+ ...(part.filename ? { filename: part.filename } : {}),
208
+ };
209
+ }
210
+ if (part.type === "data") {
211
+ return {
212
+ type: part.data_type,
213
+ data: part.data,
214
+ } as UIMessage["parts"][number];
215
+ }
216
+ if (!("tool_call_id" in part)) {
217
+ throw new Error(`Unsupported Assistant part: ${part.type}`);
218
+ }
219
+ const input = part.input ?? parse_json(part.input_text);
220
+ if (part.state === "completed") {
221
+ return {
222
+ type: "dynamic-tool",
223
+ toolName: part.tool_name,
224
+ toolCallId: part.tool_call_id,
225
+ state: "output-available",
226
+ input,
227
+ output: part.output,
228
+ };
229
+ }
230
+ if (part.state === "failed") {
231
+ return {
232
+ type: "dynamic-tool",
233
+ toolName: part.tool_name,
234
+ toolCallId: part.tool_call_id,
235
+ state: "output-error",
236
+ input,
237
+ errorText: part.error || "Tool failed",
238
+ };
239
+ }
240
+ if (part.state === "approval-required") {
241
+ return {
242
+ type: "dynamic-tool",
243
+ toolName: part.tool_name,
244
+ toolCallId: part.tool_call_id,
245
+ state: "approval-requested",
246
+ input,
247
+ approval: { id: part.approval_id || `approval:${part.tool_call_id}` },
248
+ };
249
+ }
250
+ return {
251
+ type: "dynamic-tool",
252
+ toolName: part.tool_name,
253
+ toolCallId: part.tool_call_id,
254
+ state: part.state === "input-streaming" ? "input-streaming" : "input-available",
255
+ input,
256
+ };
257
+ }) as UIMessage["parts"];
258
+ }
259
+
260
+ function parse_json(input: string | undefined): unknown {
261
+ const value = String(input || "").trim();
262
+ if (!value) return {};
263
+ try {
264
+ return JSON.parse(value);
265
+ } catch {
266
+ return value;
267
+ }
268
+ }
269
+
270
+ function to_json_value(input: unknown) {
271
+ if (input === undefined || input === null) return null;
272
+ if (
273
+ typeof input === "string" ||
274
+ typeof input === "number" ||
275
+ typeof input === "boolean"
276
+ ) {
277
+ return input;
278
+ }
279
+ try {
280
+ return JSON.parse(JSON.stringify(input));
281
+ } catch {
282
+ return String(input);
283
+ }
284
+ }