@downcity/agent 1.1.222 → 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 (166) 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 +42 -0
  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 +24 -45
  17. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  18. package/bin/agent/remote/RemoteSession.js +74 -121
  19. package/bin/agent/remote/RemoteSession.js.map +1 -1
  20. package/bin/agent/remote/RemoteTransport.d.ts +11 -27
  21. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  22. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +7 -21
  23. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  24. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -47
  25. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  26. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +7 -20
  27. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  28. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -17
  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/index.d.ts +3 -3
  34. package/bin/index.d.ts.map +1 -1
  35. package/bin/index.js +1 -1
  36. package/bin/index.js.map +1 -1
  37. package/bin/rpc/Client.d.ts +7 -36
  38. package/bin/rpc/Client.d.ts.map +1 -1
  39. package/bin/rpc/Client.js +28 -78
  40. package/bin/rpc/Client.js.map +1 -1
  41. package/bin/session/Session.d.ts +16 -8
  42. package/bin/session/Session.d.ts.map +1 -1
  43. package/bin/session/Session.js +66 -21
  44. package/bin/session/Session.js.map +1 -1
  45. package/bin/session/browse/Browse.d.ts.map +1 -1
  46. package/bin/session/browse/Browse.js +49 -18
  47. package/bin/session/browse/Browse.js.map +1 -1
  48. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +30 -24
  49. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  50. package/bin/session/recorder/JsonlSessionMessageStore.js +163 -59
  51. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  52. package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -1
  53. package/bin/session/recorder/SessionMessageCodec.js +6 -1
  54. package/bin/session/recorder/SessionMessageCodec.js.map +1 -1
  55. package/bin/session/recorder/SessionRecorder.d.ts +20 -11
  56. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  57. package/bin/session/recorder/SessionRecorder.js +148 -103
  58. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  59. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  60. package/bin/session/recorder/SessionRecorderCompaction.js +1 -0
  61. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  62. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +5 -5
  63. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  64. package/bin/session/recorder/SessionRecorderHistoryStore.js +5 -5
  65. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  66. package/bin/session/runtime/SessionEventHub.d.ts +12 -5
  67. package/bin/session/runtime/SessionEventHub.d.ts.map +1 -1
  68. package/bin/session/runtime/SessionEventHub.js +11 -6
  69. package/bin/session/runtime/SessionEventHub.js.map +1 -1
  70. package/bin/session/runtime/SessionPromptRuntime.d.ts +2 -2
  71. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  72. package/bin/session/runtime/SessionPromptRuntime.js +35 -17
  73. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  74. package/bin/session/services/SessionStateService.d.ts +2 -2
  75. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  76. package/bin/session/services/SessionStateService.js +6 -2
  77. package/bin/session/services/SessionStateService.js.map +1 -1
  78. package/bin/session/services/SessionTurnService.d.ts +2 -7
  79. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  80. package/bin/session/services/SessionTurnService.js +2 -8
  81. package/bin/session/services/SessionTurnService.js.map +1 -1
  82. package/bin/session/storage/Paths.d.ts +1 -1
  83. package/bin/session/storage/Paths.d.ts.map +1 -1
  84. package/bin/session/storage/Paths.js +2 -2
  85. package/bin/session/storage/Paths.js.map +1 -1
  86. package/bin/session/storage/RuntimeSessionPort.d.ts +3 -4
  87. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  88. package/bin/session/storage/RuntimeSessionPort.js +2 -2
  89. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  90. package/bin/types/agent/SessionActor.d.ts +11 -7
  91. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  92. package/bin/types/rpc/RpcProtocol.d.ts +35 -62
  93. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  94. package/bin/types/runtime/agent/AgentContext.d.ts +4 -4
  95. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  96. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  97. package/bin/types/session/SessionApproval.d.ts +97 -0
  98. package/bin/types/session/SessionApproval.d.ts.map +1 -0
  99. package/bin/types/session/SessionApproval.js +7 -0
  100. package/bin/types/session/SessionApproval.js.map +1 -0
  101. package/bin/types/session/SessionMessage.d.ts +10 -4
  102. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  103. package/bin/types/session/SessionMutation.d.ts +116 -0
  104. package/bin/types/session/SessionMutation.d.ts.map +1 -0
  105. package/bin/types/session/SessionMutation.js +19 -0
  106. package/bin/types/session/SessionMutation.js.map +1 -0
  107. package/bin/types/session/SessionOptions.d.ts +9 -0
  108. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  109. package/package.json +3 -3
  110. package/scripts/remote-session-reconnect.test.mjs +7 -3
  111. package/scripts/session-history-inflight.test.mjs +2 -2
  112. package/scripts/session-prompt-runtime.test.mjs +3 -3
  113. package/scripts/session-recorder.test.mjs +100 -146
  114. package/src/agent/local/Agent.ts +1 -55
  115. package/src/agent/local/services/AgentAssemblyService.ts +39 -3
  116. package/src/agent/local/services/AgentSessions.ts +47 -0
  117. package/src/agent/remote/RemoteAgent.ts +1 -54
  118. package/src/agent/remote/RemoteSession.ts +105 -163
  119. package/src/agent/remote/RemoteTransport.ts +23 -34
  120. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +33 -90
  121. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +22 -39
  122. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -2
  123. package/src/executor/types/SessionHistoryPaths.ts +1 -1
  124. package/src/index.ts +20 -12
  125. package/src/rpc/Client.ts +48 -106
  126. package/src/session/Session.ts +91 -34
  127. package/src/session/browse/Browse.ts +51 -22
  128. package/src/session/recorder/JsonlSessionMessageStore.ts +170 -86
  129. package/src/session/recorder/SessionMessageCodec.ts +6 -1
  130. package/src/session/recorder/SessionRecorder.ts +183 -124
  131. package/src/session/recorder/SessionRecorderCompaction.ts +1 -0
  132. package/src/session/recorder/SessionRecorderHistoryStore.ts +5 -5
  133. package/src/session/runtime/SessionEventHub.ts +25 -12
  134. package/src/session/runtime/SessionPromptRuntime.ts +37 -19
  135. package/src/session/services/SessionStateService.ts +8 -6
  136. package/src/session/services/SessionTurnService.ts +3 -14
  137. package/src/session/storage/Paths.ts +2 -2
  138. package/src/session/storage/RuntimeSessionPort.ts +8 -11
  139. package/src/types/agent/SessionActor.ts +23 -20
  140. package/src/types/rpc/RpcProtocol.ts +34 -71
  141. package/src/types/runtime/agent/AgentContext.ts +7 -11
  142. package/src/types/session/SessionApproval.ts +108 -0
  143. package/src/types/session/SessionMessage.ts +10 -4
  144. package/src/types/session/SessionMutation.ts +141 -0
  145. package/src/types/session/SessionOptions.ts +25 -0
  146. package/tsconfig.tsbuildinfo +1 -1
  147. package/bin/session/recorder/SessionMessageReducer.d.ts +0 -12
  148. package/bin/session/recorder/SessionMessageReducer.d.ts.map +0 -1
  149. package/bin/session/recorder/SessionMessageReducer.js +0 -109
  150. package/bin/session/recorder/SessionMessageReducer.js.map +0 -1
  151. package/bin/session/runtime/SessionMessageEventHub.d.ts +0 -15
  152. package/bin/session/runtime/SessionMessageEventHub.d.ts.map +0 -1
  153. package/bin/session/runtime/SessionMessageEventHub.js +0 -28
  154. package/bin/session/runtime/SessionMessageEventHub.js.map +0 -1
  155. package/bin/types/sdk/AgentSessionEvent.d.ts +0 -303
  156. package/bin/types/sdk/AgentSessionEvent.d.ts.map +0 -1
  157. package/bin/types/sdk/AgentSessionEvent.js +0 -9
  158. package/bin/types/sdk/AgentSessionEvent.js.map +0 -1
  159. package/bin/types/session/SessionMessageMutation.d.ts +0 -92
  160. package/bin/types/session/SessionMessageMutation.d.ts.map +0 -1
  161. package/bin/types/session/SessionMessageMutation.js +0 -21
  162. package/bin/types/session/SessionMessageMutation.js.map +0 -1
  163. package/src/session/recorder/SessionMessageReducer.ts +0 -155
  164. package/src/session/runtime/SessionMessageEventHub.ts +0 -35
  165. package/src/types/sdk/AgentSessionEvent.ts +0 -372
  166. package/src/types/session/SessionMessageMutation.ts +0 -121
@@ -109,6 +109,7 @@ export function from_ui_assistant_parts(
109
109
  if (type === "text" || type === "reasoning") {
110
110
  return [{
111
111
  part_id: `${type}:${index + 1}`,
112
+ sequence: index + 1,
112
113
  type: type as "text" | "reasoning",
113
114
  text: String(candidate.text || ""),
114
115
  state: "done" as const,
@@ -117,6 +118,7 @@ export function from_ui_assistant_parts(
117
118
  if (type === "file") {
118
119
  return [{
119
120
  part_id: `file:${index + 1}`,
121
+ sequence: index + 1,
120
122
  type: "file" as const,
121
123
  url: String(candidate.url || ""),
122
124
  media_type: String(candidate.mediaType || "application/octet-stream"),
@@ -127,6 +129,7 @@ export function from_ui_assistant_parts(
127
129
  const state = String(candidate.state || "");
128
130
  return [{
129
131
  part_id: String(candidate.toolCallId || `tool:${index + 1}`),
132
+ sequence: index + 1,
130
133
  type: "tool" as const,
131
134
  tool_call_id: String(candidate.toolCallId || `tool:${index + 1}`),
132
135
  tool_name:
@@ -142,7 +145,9 @@ export function from_ui_assistant_parts(
142
145
  ? "approval-required" as const
143
146
  : state === "input-streaming"
144
147
  ? "input-streaming" as const
145
- : "running" as const,
148
+ : state === "input-available"
149
+ ? "ready" as const
150
+ : "running" as const,
146
151
  ...(candidate.input !== undefined
147
152
  ? { input: to_json_value(candidate.input) }
148
153
  : {}),
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * SessionRecorder:Session Message 的唯一持久化与发布入口。
3
3
  *
4
- * Mutation 先写入单一 messages.jsonl,成功后更新内存 snapshot,最后发布同一个对象。
4
+ * 完整 Message Assistant 草稿先持久化,成功后再发布实时 Mutation。
5
5
  */
6
6
 
7
7
  import type { UIMessage, UIMessageChunk } from "ai";
8
8
  import { generateId } from "@/utils/Id.js";
9
9
  import { JsonlSessionMessageStore } from "@/session/recorder/JsonlSessionMessageStore.js";
10
- import { reduce_session_message } from "@/session/recorder/SessionMessageReducer.js";
11
10
  import type { JsonObject, JsonValue } from "@/types/common/Json.js";
12
11
  import type {
13
12
  ListSessionMessagesInput,
@@ -22,23 +21,18 @@ import type {
22
21
  SessionUserMessagePart,
23
22
  } from "@/types/session/SessionMessage.js";
24
23
  import type {
25
- ListSessionMessageChangesInput,
26
- SessionMessageMutation,
27
- SessionMessageMutationPage,
28
- SessionMessageMutationSubscriber,
29
- SessionMessageMutationUnsubscribe,
30
- SessionMessageSnapshotMutation,
31
- SessionPartSnapshotMutation,
32
- } from "@/types/session/SessionMessageMutation.js";
24
+ SessionMutation,
25
+ SessionMessageMutation as SessionMessageSnapshotMutation,
26
+ } from "@/types/session/SessionMutation.js";
33
27
 
34
28
  /** SessionRecorder 构造参数。 */
35
29
  export interface SessionRecorderOptions {
36
30
  /** 当前 Session 标识。 */
37
31
  session_id: string;
38
- /** 单一 JSONL Mutation store。 */
32
+ /** Message 快照 store。 */
39
33
  store: JsonlSessionMessageStore;
40
- /** Mutation 成功提交后的发布函数。 */
41
- publish: (mutation: SessionMessageMutation) => void;
34
+ /** 持久化成功后的实时 Mutation 发布函数。 */
35
+ publish: (mutation: SessionMutation) => void;
42
36
  }
43
37
 
44
38
  /** User Message 创建参数。 */
@@ -120,7 +114,6 @@ export class SessionRecorder {
120
114
  readonly session_id: string;
121
115
  private readonly store: JsonlSessionMessageStore;
122
116
  private readonly publish: SessionRecorderOptions["publish"];
123
- private readonly subscribers = new Set<SessionMessageMutationSubscriber>();
124
117
  private readonly messages_by_id = new Map<string, SessionMessage>();
125
118
  private initialized = false;
126
119
 
@@ -131,7 +124,7 @@ export class SessionRecorder {
131
124
  if (!this.session_id) throw new Error("SessionRecorder requires session_id");
132
125
  }
133
126
 
134
- /** 重放已有 Mutation,并收口进程中断遗留的运行状态。 */
127
+ /** 恢复已有 Message,并收口进程中断遗留的运行状态。 */
135
128
  async initialize(): Promise<void> {
136
129
  if (this.initialized) return;
137
130
  await this.store.initialize();
@@ -202,7 +195,7 @@ export class SessionRecorder {
202
195
  ...(input.summary_through_message_id
203
196
  ? { summary_through_message_id: input.summary_through_message_id }
204
197
  : {}),
205
- }))) as SessionAssistantMessage;
198
+ }), true)) as SessionAssistantMessage;
206
199
  return new SessionAssistantMessageWriter(this, message.message_id);
207
200
  }
208
201
 
@@ -256,10 +249,10 @@ export class SessionRecorder {
256
249
  status: "running" | "completed" | "failed",
257
250
  changes?: { title?: string; description?: string; data?: JsonObject },
258
251
  ): Promise<SessionActionMessage> {
259
- const mutation = await this.store.commit((state) => {
252
+ const message = await this.store.append_message((state) => {
260
253
  const current = require_message(state.messages, message_id, "action");
261
254
  const created_at = Date.now();
262
- const message: SessionActionMessage = {
255
+ return {
263
256
  ...current,
264
257
  status,
265
258
  ...(changes?.title ? { title: changes.title } : {}),
@@ -269,11 +262,10 @@ export class SessionRecorder {
269
262
  ...(changes?.data ? { data: structuredClone(changes.data) } : {}),
270
263
  revision: current.revision + 1,
271
264
  updated_at: created_at,
272
- };
273
- return this.build_message_mutation(state.commit_sequence, message);
265
+ } satisfies SessionActionMessage;
274
266
  });
275
- this.accept_mutation(mutation);
276
- return this.get_message(message_id) as SessionActionMessage;
267
+ this.accept_message(message);
268
+ return message as SessionActionMessage;
277
269
  }
278
270
 
279
271
  /** 创建用户可见 Error Message。 */
@@ -302,9 +294,6 @@ export class SessionRecorder {
302
294
  input?: ListSessionMessagesInput,
303
295
  ): Promise<SessionMessagePage> {
304
296
  await this.ensure_initialized();
305
- const mutations = await this.store.list_mutations();
306
- const latest_commit_sequence =
307
- mutations[mutations.length - 1]?.commit_sequence || 0;
308
297
  const include_internal = input?.include_internal === true;
309
298
  const through_sequence = Number.isFinite(input?.through_sequence)
310
299
  ? Number(input?.through_sequence)
@@ -329,38 +318,9 @@ export class SessionRecorder {
329
318
  total: all.length,
330
319
  ...(next_offset < all.length ? { next_cursor: encode_cursor(next_offset) } : {}),
331
320
  has_more: next_offset < all.length,
332
- latest_commit_sequence,
333
- };
334
- }
335
-
336
- /** 从指定 commit_sequence 增量读取 Mutation。 */
337
- async list_message_changes(
338
- input: ListSessionMessageChangesInput,
339
- ): Promise<SessionMessageMutationPage> {
340
- await this.ensure_initialized();
341
- const after = Math.max(0, Math.floor(input.after_commit_sequence || 0));
342
- const limit = normalize_limit(input.limit, 200, 1000);
343
- const all = await this.store.list_mutations();
344
- const matching = all.filter((item) => item.commit_sequence > after);
345
- const items = matching.slice(0, limit);
346
- const latest_commit_sequence = all[all.length - 1]?.commit_sequence || 0;
347
- return {
348
- items,
349
- has_more: matching.length > items.length,
350
- next_commit_sequence:
351
- items[items.length - 1]?.commit_sequence || after,
352
- latest_commit_sequence,
353
321
  };
354
322
  }
355
323
 
356
- /** 订阅已成功持久化的未来 Mutation。 */
357
- subscribe(
358
- subscriber: SessionMessageMutationSubscriber,
359
- ): SessionMessageMutationUnsubscribe {
360
- this.subscribers.add(subscriber);
361
- return () => this.subscribers.delete(subscriber);
362
- }
363
-
364
324
  /** 向当前 Session 导入 fork 来源 Message,并重新分配全部身份和顺序。 */
365
325
  async import_messages(messages: SessionMessage[]): Promise<void> {
366
326
  const turn_ids = new Map<string, string>();
@@ -400,23 +360,37 @@ export class SessionRecorder {
400
360
  delta: string,
401
361
  ): Promise<void> {
402
362
  if (!delta) return;
403
- const mutation = await this.store.commit((state) => {
404
- const current = require_message(state.messages, message_id, "assistant");
405
- return {
406
- mutation_id: generateId(),
407
- variant: "delta",
408
- type,
409
- commit_sequence: state.commit_sequence,
410
- message_id,
411
- revision: current.revision + 1,
412
- session_id: this.session_id,
413
- turn_id: current.turn_id,
414
- created_at: Date.now(),
415
- part_id,
416
- delta,
417
- } satisfies SessionMessageMutation;
418
- });
419
- this.accept_mutation(mutation);
363
+ const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
364
+ require_streaming_assistant(current);
365
+ const part = current.parts.find((item) => item.part_id === part_id);
366
+ if (!part || (part.type !== "text" && part.type !== "reasoning")) {
367
+ throw new Error(`Delta target Part does not exist: ${part_id}`);
368
+ }
369
+ if (part.type !== type) throw new Error(`Delta type changed for Part: ${part_id}`);
370
+ const created_at = Date.now();
371
+ const message: SessionAssistantMessage = {
372
+ ...current,
373
+ revision: current.revision + 1,
374
+ updated_at: created_at,
375
+ parts: current.parts.map((item) =>
376
+ item.part_id === part_id && (item.type === "text" || item.type === "reasoning")
377
+ ? { ...item, text: item.text + delta }
378
+ : item,
379
+ ),
380
+ };
381
+ await this.store.write_assistant_message(message);
382
+ this.accept_mutation({
383
+ mutation_id: generateId(),
384
+ variant: "delta",
385
+ type,
386
+ message_id,
387
+ revision: message.revision,
388
+ session_id: this.session_id,
389
+ turn_id: message.turn_id,
390
+ created_at,
391
+ part_id,
392
+ delta,
393
+ }, message);
420
394
  }
421
395
 
422
396
  /** @internal 写入 Assistant 完整 part。 */
@@ -424,23 +398,36 @@ export class SessionRecorder {
424
398
  message_id: string,
425
399
  part: SessionAssistantMessagePart,
426
400
  ): Promise<void> {
427
- const mutation = await this.store.commit((state) => {
428
- const current = require_message(state.messages, message_id, "assistant");
429
- return {
430
- mutation_id: generateId(),
431
- variant: "part",
432
- type: part.type,
433
- commit_sequence: state.commit_sequence,
434
- message_id,
435
- revision: current.revision + 1,
436
- session_id: this.session_id,
437
- turn_id: current.turn_id,
438
- created_at: Date.now(),
439
- part_id: part.part_id,
440
- part: structuredClone(part),
441
- } as SessionPartSnapshotMutation;
442
- });
443
- this.accept_mutation(mutation);
401
+ const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
402
+ require_streaming_assistant(current);
403
+ const existing = current.parts.find((item) => item.part_id === part.part_id);
404
+ if (existing && existing.sequence !== part.sequence) {
405
+ throw new Error(`Assistant Part sequence changed: ${part.part_id}`);
406
+ }
407
+ const created_at = Date.now();
408
+ const next_part = structuredClone(part);
409
+ const message: SessionAssistantMessage = {
410
+ ...current,
411
+ revision: current.revision + 1,
412
+ updated_at: created_at,
413
+ parts: (existing
414
+ ? current.parts.map((item) => item.part_id === part.part_id ? next_part : item)
415
+ : [...current.parts, next_part]
416
+ ).sort((left, right) => left.sequence - right.sequence),
417
+ };
418
+ await this.store.write_assistant_message(message);
419
+ this.accept_mutation({
420
+ mutation_id: generateId(),
421
+ variant: "part",
422
+ type: next_part.type,
423
+ message_id,
424
+ revision: message.revision,
425
+ session_id: this.session_id,
426
+ turn_id: message.turn_id,
427
+ created_at,
428
+ part_id: next_part.part_id,
429
+ part: next_part,
430
+ } as SessionMutation, message);
444
431
  }
445
432
 
446
433
  /** @internal 收口 Assistant Message。 */
@@ -448,45 +435,100 @@ export class SessionRecorder {
448
435
  message_id: string,
449
436
  status: "completed" | "stopped" | "failed",
450
437
  ): Promise<void> {
451
- const mutation = await this.store.commit((state) => {
452
- const current = require_message(state.messages, message_id, "assistant");
453
- const created_at = Date.now();
454
- const message: SessionAssistantMessage = {
455
- ...current,
456
- revision: current.revision + 1,
457
- status,
458
- updated_at: created_at,
459
- parts: current.parts.map((part) =>
460
- part.type === "text" || part.type === "reasoning"
461
- ? { ...part, state: "done" as const }
462
- : part,
463
- ),
464
- };
465
- return this.build_message_mutation(state.commit_sequence, message);
438
+ const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
439
+ require_streaming_assistant(current);
440
+ const created_at = Date.now();
441
+ const message: SessionAssistantMessage = {
442
+ ...current,
443
+ revision: current.revision + 1,
444
+ status,
445
+ updated_at: created_at,
446
+ parts: current.parts.map((part) =>
447
+ part.type === "text" || part.type === "reasoning"
448
+ ? { ...part, state: "done" as const }
449
+ : part,
450
+ ),
451
+ };
452
+ await this.store.finalize_assistant_message(message);
453
+ this.accept_message(message);
454
+ }
455
+
456
+ /** @internal 将 Shell Runtime 审批请求更新到当前 Tool Part。 */
457
+ async require_tool_approval(input: {
458
+ tool_call_id: string;
459
+ approval_id: string;
460
+ }): Promise<void> {
461
+ const tool = this.find_streaming_tool(input.tool_call_id);
462
+ await this.update_assistant_part(tool.message_id, {
463
+ ...tool.part,
464
+ state: "approval-required",
465
+ approval_id: input.approval_id,
466
+ });
467
+ }
468
+
469
+ /** @internal 将审批结果更新到当前 Tool Part。 */
470
+ async resolve_tool_approval(input: {
471
+ tool_call_id: string;
472
+ approval_id: string;
473
+ decision: "approved" | "denied" | "expired";
474
+ }): Promise<void> {
475
+ const tool = this.find_streaming_tool(input.tool_call_id);
476
+ if (tool.part.approval_id && tool.part.approval_id !== input.approval_id) {
477
+ throw new Error(`Tool approval identity mismatch: ${input.approval_id}`);
478
+ }
479
+ await this.update_assistant_part(tool.message_id, {
480
+ ...tool.part,
481
+ state: input.decision === "approved" ? "running" : "failed",
482
+ ...(input.decision === "approved"
483
+ ? {}
484
+ : { error: input.decision === "expired" ? "Approval expired" : "Approval denied" }),
466
485
  });
467
- this.accept_mutation(mutation);
468
486
  }
469
487
 
470
488
  private async create_message(
471
489
  factory: (sequence: number, created_at: number) => SessionMessage,
490
+ draft = false,
472
491
  ): Promise<SessionMessage> {
473
- const mutation = await this.store.commit((state) => {
474
- const message = factory(state.message_sequence, Date.now());
475
- return this.build_message_mutation(state.commit_sequence, message);
476
- });
477
- this.accept_mutation(mutation);
478
- return (mutation as SessionMessageSnapshotMutation).message;
492
+ if (draft) {
493
+ const message = await this.store.create_assistant_message((state) => {
494
+ const candidate = factory(state.message_sequence, Date.now());
495
+ if (candidate.type !== "assistant" || candidate.status !== "streaming") {
496
+ throw new Error("Draft Message must be a streaming Assistant");
497
+ }
498
+ return candidate;
499
+ });
500
+ this.accept_message(message);
501
+ return message;
502
+ }
503
+ const message = await this.store.append_message((state) =>
504
+ factory(state.message_sequence, Date.now()),
505
+ );
506
+ this.accept_message(message);
507
+ return message;
508
+ }
509
+
510
+ private find_streaming_tool(tool_call_id: string): {
511
+ message_id: string;
512
+ part: SessionAssistantToolPart;
513
+ } {
514
+ for (const message of this.messages_by_id.values()) {
515
+ if (message.type !== "assistant" || message.status !== "streaming") continue;
516
+ const part = message.parts.find(
517
+ (item): item is SessionAssistantToolPart =>
518
+ item.type === "tool" && item.tool_call_id === tool_call_id,
519
+ );
520
+ if (part) return { message_id: message.message_id, part };
521
+ }
522
+ throw new Error(`Streaming Tool Part not found: ${tool_call_id}`);
479
523
  }
480
524
 
481
525
  private build_message_mutation(
482
- commit_sequence: number,
483
526
  message: SessionMessage,
484
527
  ): SessionMessageSnapshotMutation {
485
528
  return {
486
529
  mutation_id: generateId(),
487
530
  variant: "message",
488
531
  type: message.type,
489
- commit_sequence,
490
532
  message_id: message.message_id,
491
533
  sequence: message.sequence,
492
534
  revision: message.revision,
@@ -497,18 +539,13 @@ export class SessionRecorder {
497
539
  } as SessionMessageSnapshotMutation;
498
540
  }
499
541
 
500
- private accept_mutation(mutation: SessionMessageMutation): void {
501
- const current = this.messages_by_id.get(mutation.message_id);
502
- const next = reduce_session_message(current, mutation);
503
- this.messages_by_id.set(next.message_id, next);
542
+ private accept_message(message: SessionMessage): void {
543
+ this.accept_mutation(this.build_message_mutation(message), message);
544
+ }
545
+
546
+ private accept_mutation(mutation: SessionMutation, message: SessionMessage): void {
547
+ this.messages_by_id.set(message.message_id, structuredClone(message));
504
548
  this.publish(mutation);
505
- for (const subscriber of this.subscribers) {
506
- try {
507
- subscriber(mutation);
508
- } catch {
509
- // 单个订阅者异常不能影响已经完成的提交。
510
- }
511
- }
512
549
  }
513
550
 
514
551
  private async ensure_initialized(): Promise<void> {
@@ -536,6 +573,7 @@ export class SessionAssistantMessageWriter {
536
573
  case "reasoning-start":
537
574
  await this.upsert_part({
538
575
  part_id: `${chunk.type === "text-start" ? "text" : "reasoning"}:${chunk.id}`,
576
+ sequence: this.next_part_sequence(),
539
577
  type: chunk.type === "text-start" ? "text" : "reasoning",
540
578
  text: "",
541
579
  state: "streaming",
@@ -578,7 +616,7 @@ export class SessionAssistantMessageWriter {
578
616
  case "tool-input-available":
579
617
  await this.upsert_tool(chunk.toolCallId, {
580
618
  tool_name: chunk.toolName,
581
- state: "running",
619
+ state: "ready",
582
620
  input: to_json_value(chunk.input),
583
621
  });
584
622
  return;
@@ -624,6 +662,7 @@ export class SessionAssistantMessageWriter {
624
662
  case "file":
625
663
  await this.upsert_part({
626
664
  part_id: `file:${generateId()}`,
665
+ sequence: this.next_part_sequence(),
627
666
  type: "file",
628
667
  media_type: chunk.mediaType,
629
668
  url: chunk.url,
@@ -672,6 +711,13 @@ export class SessionAssistantMessageWriter {
672
711
  );
673
712
  }
674
713
 
714
+ private next_part_sequence(): number {
715
+ return this.current_message().parts.reduce(
716
+ (value, part) => Math.max(value, part.sequence + 1),
717
+ 1,
718
+ );
719
+ }
720
+
675
721
  private async upsert_tool(
676
722
  tool_call_id: string,
677
723
  changes: Pick<SessionAssistantToolPart, "tool_name" | "state"> &
@@ -681,6 +727,7 @@ export class SessionAssistantMessageWriter {
681
727
  await this.upsert_part({
682
728
  ...(current || {}),
683
729
  part_id: `tool:${tool_call_id}`,
730
+ sequence: current?.sequence || this.next_part_sequence(),
684
731
  type: "tool",
685
732
  tool_call_id,
686
733
  ...changes,
@@ -771,6 +818,18 @@ function require_message<TType extends SessionMessage["type"]>(
771
818
  return message as Extract<SessionMessage, { type: TType }>;
772
819
  }
773
820
 
821
+ function require_streaming_assistant(
822
+ message: SessionMessage,
823
+ ): SessionAssistantMessage {
824
+ if (message.type !== "assistant") {
825
+ throw new Error(`Session Message is not assistant: ${message.message_id}`);
826
+ }
827
+ if (message.status !== "streaming") {
828
+ throw new Error(`Assistant Message is already closed: ${message.message_id}`);
829
+ }
830
+ return message;
831
+ }
832
+
774
833
  function to_json_value(input: unknown): JsonValue {
775
834
  if (input === undefined || input === null) return null;
776
835
  if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") return input;
@@ -102,6 +102,7 @@ export async function compact_session_recorder_messages(input: {
102
102
  summary_through_message_id: boundary.message_id,
103
103
  parts: [{
104
104
  part_id: `summary-text:${generateId()}`,
105
+ sequence: 1,
105
106
  type: "text",
106
107
  text: summary,
107
108
  state: "done",
@@ -2,9 +2,9 @@
2
2
  * SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
3
3
  *
4
4
  * 关键点(中文)
5
- * - Recorder messages.jsonl 是唯一持久化事实源。
5
+ * - Recorder messages.jsonl 与 assistant_message.json 共同组成持久化事实源。
6
6
  * - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
7
- * - inflight 写入已经由 AssistantMessageWriter 取代,因此对应接口只提供投影兼容。
7
+ * - 流式草稿写入由 AssistantMessageWriter 负责,Executor 的旧接口只提供投影兼容。
8
8
  */
9
9
 
10
10
  import { nanoid } from "nanoid";
@@ -109,7 +109,7 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
109
109
  });
110
110
  }
111
111
 
112
- /** Recorder 已经逐 delta 保存 assistant,不再维护第二份 inflight。 */
112
+ /** Recorder 当前快照投影运行中的 Assistant。 */
113
113
  async read_inflight(): Promise<SessionRecordV1 | null> {
114
114
  const page = await this.recorder.list_messages({
115
115
  limit: 500,
@@ -124,10 +124,10 @@ export class SessionRecorderHistoryStore implements SessionHistoryStore {
124
124
  return assistant ? to_executor_ui_message(assistant) : null;
125
125
  }
126
126
 
127
- /** inflight 写入口已废弃;流式内容由 Recorder writer 负责。 */
127
+ /** Executor 草稿写入口不持久化;流式内容由 Recorder writer 负责。 */
128
128
  async write_inflight(_message: SessionRecordV1): Promise<void> {}
129
129
 
130
- /** inflight 收口入口已废弃;Assistant writer 负责 complete。 */
130
+ /** Executor 草稿收口入口不持久化;Assistant writer 负责 complete。 */
131
131
  async finalize_inflight(_message?: SessionRecordV1 | null): Promise<void> {}
132
132
 
133
133
  /** 返回 Context Composer 使用的 UIMessage 投影。 */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SessionEventHubSession 级事件分发器。
2
+ * SessionEventHub:统一 Session Mutation 分发器。
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 收口 `Session.subscribe()` 的订阅/取消订阅能力。
@@ -7,24 +7,36 @@
7
7
  */
8
8
 
9
9
  import type {
10
- AgentSessionEvent,
11
- AgentSessionSubscriber,
12
- AgentSessionUnsubscribe,
13
- } from "@/types/sdk/AgentSessionEvent.js";
10
+ SessionMutation,
11
+ SessionMutationReply,
12
+ SessionMutationSubscriber,
13
+ SessionMutationUnsubscribe,
14
+ } from "@/types/session/SessionMutation.js";
15
+
16
+ /** SessionEventHub 构造参数。 */
17
+ export interface SessionEventHubOptions {
18
+ /** 为当前 Mutation 创建绑定 Session 和 Tool Part 的回复入口。 */
19
+ create_reply: (mutation: SessionMutation) => SessionMutationReply;
20
+ }
14
21
 
15
22
  /**
16
23
  * SessionEventHub:最小事件总线实现。
17
24
  */
18
25
  export class SessionEventHub {
19
- private readonly subscribers = new Map<number, AgentSessionSubscriber>();
20
- private nextSubscriberId = 1;
26
+ private readonly subscribers = new Map<number, SessionMutationSubscriber>();
27
+ private readonly create_reply: SessionEventHubOptions["create_reply"];
28
+ private next_subscriber_id = 1;
29
+
30
+ constructor(options: SessionEventHubOptions) {
31
+ this.create_reply = options.create_reply;
32
+ }
21
33
 
22
34
  /**
23
35
  * 注册一个新的 Session 事件订阅者。
24
36
  */
25
- subscribe(subscriber: AgentSessionSubscriber): AgentSessionUnsubscribe {
26
- const id = this.nextSubscriberId;
27
- this.nextSubscriberId += 1;
37
+ subscribe(subscriber: SessionMutationSubscriber): SessionMutationUnsubscribe {
38
+ const id = this.next_subscriber_id;
39
+ this.next_subscriber_id += 1;
28
40
  this.subscribers.set(id, subscriber);
29
41
 
30
42
  return () => {
@@ -35,10 +47,11 @@ export class SessionEventHub {
35
47
  /**
36
48
  * 广播一条 Session 事件。
37
49
  */
38
- publish(event: AgentSessionEvent): void {
50
+ publish(mutation: SessionMutation): void {
51
+ const reply = this.create_reply(mutation);
39
52
  for (const subscriber of this.subscribers.values()) {
40
53
  try {
41
- subscriber(event);
54
+ Promise.resolve(subscriber(mutation, reply)).catch(() => undefined);
42
55
  } catch {
43
56
  // ignore single subscriber failures
44
57
  }