@downcity/agent 1.1.317 → 1.1.323

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 (138) hide show
  1. package/bin/agent/Agent.d.ts.map +1 -1
  2. package/bin/agent/Agent.js +1 -0
  3. package/bin/agent/Agent.js.map +1 -1
  4. package/bin/agent/AgentSessions.d.ts +2 -0
  5. package/bin/agent/AgentSessions.d.ts.map +1 -1
  6. package/bin/agent/AgentSessions.js +9 -0
  7. package/bin/agent/AgentSessions.js.map +1 -1
  8. package/bin/executor/messages/SessionAttachmentMapper.d.ts +15 -7
  9. package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
  10. package/bin/executor/messages/SessionAttachmentMapper.js +22 -32
  11. package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
  12. package/bin/executor/types/SessionHistoryMeta.d.ts +2 -0
  13. package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
  14. package/bin/index.d.ts +2 -1
  15. package/bin/index.d.ts.map +1 -1
  16. package/bin/index.js.map +1 -1
  17. package/bin/remote/RemoteSession.d.ts +2 -2
  18. package/bin/remote/RemoteSession.d.ts.map +1 -1
  19. package/bin/remote/RemoteSession.js +2 -2
  20. package/bin/remote/RemoteSession.js.map +1 -1
  21. package/bin/remote/RemoteTransport.d.ts +2 -2
  22. package/bin/remote/RemoteTransport.d.ts.map +1 -1
  23. package/bin/remote/transports/HttpRemoteAgentTransport.d.ts +2 -2
  24. package/bin/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  25. package/bin/remote/transports/HttpRemoteAgentTransport.js +2 -2
  26. package/bin/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  27. package/bin/remote/transports/RpcClient.d.ts +2 -2
  28. package/bin/remote/transports/RpcClient.d.ts.map +1 -1
  29. package/bin/remote/transports/RpcClient.js +2 -2
  30. package/bin/remote/transports/RpcClient.js.map +1 -1
  31. package/bin/remote/transports/RpcRemoteAgentTransport.d.ts +2 -2
  32. package/bin/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  33. package/bin/remote/transports/RpcRemoteAgentTransport.js +2 -2
  34. package/bin/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  35. package/bin/session/Session.d.ts +6 -4
  36. package/bin/session/Session.d.ts.map +1 -1
  37. package/bin/session/Session.js +56 -31
  38. package/bin/session/Session.js.map +1 -1
  39. package/bin/session/SessionLoop.d.ts.map +1 -1
  40. package/bin/session/SessionLoop.js +6 -4
  41. package/bin/session/SessionLoop.js.map +1 -1
  42. package/bin/session/SessionMessages.d.ts +9 -3
  43. package/bin/session/SessionMessages.d.ts.map +1 -1
  44. package/bin/session/SessionMessages.js +35 -21
  45. package/bin/session/SessionMessages.js.map +1 -1
  46. package/bin/session/SessionState.d.ts +23 -10
  47. package/bin/session/SessionState.d.ts.map +1 -1
  48. package/bin/session/SessionState.js +129 -51
  49. package/bin/session/SessionState.js.map +1 -1
  50. package/bin/session/SessionTitle.d.ts +4 -0
  51. package/bin/session/SessionTitle.d.ts.map +1 -1
  52. package/bin/session/SessionTitle.js +5 -1
  53. package/bin/session/SessionTitle.js.map +1 -1
  54. package/bin/session/messages/SessionAssistantMessageWriter.d.ts +1 -1
  55. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  56. package/bin/session/messages/SessionAssistantMessageWriter.js +4 -4
  57. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  58. package/bin/session/runtime/SessionTitleTask.d.ts +35 -0
  59. package/bin/session/runtime/SessionTitleTask.d.ts.map +1 -0
  60. package/bin/session/runtime/SessionTitleTask.js +72 -0
  61. package/bin/session/runtime/SessionTitleTask.js.map +1 -0
  62. package/bin/session/storage/Metadata.d.ts.map +1 -1
  63. package/bin/session/storage/Metadata.js +7 -0
  64. package/bin/session/storage/Metadata.js.map +1 -1
  65. package/bin/types/agent/SessionActor.d.ts +3 -3
  66. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  67. package/bin/types/agent/SessionTypes.d.ts +20 -0
  68. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  69. package/bin/types/rpc/RpcProtocol.d.ts +2 -1
  70. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  71. package/bin/types/sdk/AgentSessionPrompt.d.ts +22 -0
  72. package/bin/types/sdk/AgentSessionPrompt.d.ts.map +1 -1
  73. package/bin/types/sdk/AgentSessionPrompt.js.map +1 -1
  74. package/bin/types/session/SessionCommand.d.ts +2 -0
  75. package/bin/types/session/SessionCommand.d.ts.map +1 -1
  76. package/bin/types/session/SessionLocalState.d.ts +3 -0
  77. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  78. package/bin/types/session/SessionMessages.d.ts +5 -0
  79. package/bin/types/session/SessionMessages.d.ts.map +1 -1
  80. package/bin/types/session/SessionOptions.d.ts +2 -0
  81. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  82. package/bin/types/store/SessionAttachmentStore.d.ts +29 -0
  83. package/bin/types/store/SessionAttachmentStore.d.ts.map +1 -0
  84. package/bin/types/store/SessionAttachmentStore.js +15 -0
  85. package/bin/types/store/SessionAttachmentStore.js.map +1 -0
  86. package/bin/types/store/SessionStore.d.ts +3 -0
  87. package/bin/types/store/SessionStore.d.ts.map +1 -1
  88. package/bin/workspace/store/LocalSessionAttachmentStore.d.ts +25 -0
  89. package/bin/workspace/store/LocalSessionAttachmentStore.d.ts.map +1 -0
  90. package/bin/workspace/store/LocalSessionAttachmentStore.js +72 -0
  91. package/bin/workspace/store/LocalSessionAttachmentStore.js.map +1 -0
  92. package/bin/workspace/store/LocalSessionStore.d.ts +3 -0
  93. package/bin/workspace/store/LocalSessionStore.d.ts.map +1 -1
  94. package/bin/workspace/store/LocalSessionStore.js +8 -1
  95. package/bin/workspace/store/LocalSessionStore.js.map +1 -1
  96. package/bin/workspace/store/LocalStorePaths.d.ts +6 -0
  97. package/bin/workspace/store/LocalStorePaths.d.ts.map +1 -1
  98. package/bin/workspace/store/LocalStorePaths.js +8 -0
  99. package/bin/workspace/store/LocalStorePaths.js.map +1 -1
  100. package/package.json +6 -6
  101. package/scripts/remote-session-reconnect.test.mjs +25 -0
  102. package/scripts/session-attachments.test.mjs +101 -0
  103. package/scripts/session-config-turn-boundary.test.mjs +185 -0
  104. package/scripts/session-list-title.test.mjs +6 -0
  105. package/scripts/session-title-event.test.mjs +67 -3
  106. package/scripts/session-turn-failure.test.mjs +34 -1
  107. package/src/agent/Agent.ts +1 -0
  108. package/src/agent/AgentSessions.ts +10 -0
  109. package/src/executor/messages/SessionAttachmentMapper.ts +24 -37
  110. package/src/executor/types/SessionHistoryMeta.ts +2 -0
  111. package/src/index.ts +2 -0
  112. package/src/remote/RemoteSession.ts +6 -2
  113. package/src/remote/RemoteTransport.ts +6 -1
  114. package/src/remote/transports/HttpRemoteAgentTransport.ts +3 -1
  115. package/src/remote/transports/RpcClient.ts +3 -1
  116. package/src/remote/transports/RpcRemoteAgentTransport.ts +3 -1
  117. package/src/session/Session.ts +65 -32
  118. package/src/session/SessionLoop.ts +6 -3
  119. package/src/session/SessionMessages.ts +61 -20
  120. package/src/session/SessionState.ts +150 -59
  121. package/src/session/SessionTitle.ts +13 -1
  122. package/src/session/messages/SessionAssistantMessageWriter.ts +7 -3
  123. package/src/session/runtime/SessionTitleTask.ts +86 -0
  124. package/src/session/storage/Metadata.ts +10 -0
  125. package/src/types/agent/SessionActor.ts +3 -2
  126. package/src/types/agent/SessionTypes.ts +22 -0
  127. package/src/types/rpc/RpcProtocol.ts +6 -1
  128. package/src/types/sdk/AgentSessionPrompt.ts +22 -0
  129. package/src/types/session/SessionCommand.ts +2 -0
  130. package/src/types/session/SessionLocalState.ts +4 -0
  131. package/src/types/session/SessionMessages.ts +5 -0
  132. package/src/types/session/SessionOptions.ts +3 -0
  133. package/src/types/store/SessionAttachmentStore.ts +29 -0
  134. package/src/types/store/SessionStore.ts +4 -0
  135. package/src/workspace/store/LocalSessionAttachmentStore.ts +96 -0
  136. package/src/workspace/store/LocalSessionStore.ts +13 -0
  137. package/src/workspace/store/LocalStorePaths.ts +16 -0
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -21,6 +21,7 @@ import type {
21
21
  AgentSessionInfo,
22
22
  AgentSessionStatus,
23
23
  AgentSessionSetInput,
24
+ AgentSessionSetOptions,
24
25
  AgentSessionSystemBlock,
25
26
  AgentSessionSystemSnapshot,
26
27
  } from "@/types/agent/SessionTypes.js";
@@ -93,8 +94,6 @@ export class Session implements AgentSession {
93
94
  private readonly events: SessionEventHub;
94
95
  private readonly session_interactions: SessionInteractions;
95
96
  private readonly shell_approval_adapter: SessionShellApprovalAdapter;
96
- /** 已被 Session 接受、等待或已经在 Step 检查点提交的审批模式。 */
97
- private configured_approval_mode: SessionApprovalMode = "ask";
98
97
  private readonly local_state: SessionLocalState;
99
98
  private readonly get_workspace_env: SessionOptions["get_workspace_env"];
100
99
  private readonly get_agent_model: SessionOptions["get_agent_model"];
@@ -102,6 +101,8 @@ export class Session implements AgentSession {
102
101
  private readonly get_instruction_system_blocks:
103
102
  SessionOptions["get_instruction_system_blocks"];
104
103
  private effective_instruction_system_blocks: AgentSessionSystemBlock[];
104
+ /** 当前 Session 的一次性初始化任务,避免缓存实例被重复恢复运行时状态。 */
105
+ private initialize_promise: Promise<void> | null = null;
105
106
  private instruction_initialize_promise: Promise<void> | null = null;
106
107
  private effective_workspace_env: Record<string, string>;
107
108
  private effective_agent_plugins: AgentPluginExecutionRuntime;
@@ -149,6 +150,7 @@ export class Session implements AgentSession {
149
150
  this.session_messages = new SessionMessages({
150
151
  session_id: this.id,
151
152
  store: this.store.messages,
153
+ attachment_store: this.store.attachments,
152
154
  publish: (mutation) => {
153
155
  this.events.publish(mutation);
154
156
  },
@@ -199,11 +201,19 @@ export class Session implements AgentSession {
199
201
  * 初始化当前 session。
200
202
  */
201
203
  async initialize(): Promise<this> {
202
- await Promise.all([
203
- this.initialize_instruction(),
204
- this.session_messages.initialize(),
205
- this.state.initialize(),
206
- ]);
204
+ if (!this.initialize_promise) {
205
+ this.initialize_promise = (async () => {
206
+ await Promise.all([
207
+ this.initialize_instruction(),
208
+ this.session_messages.initialize(),
209
+ this.state.initialize(),
210
+ ]);
211
+ this.shell_approval_adapter.set_effective_mode(
212
+ this.state.get_approval_mode(),
213
+ );
214
+ })();
215
+ }
216
+ await this.initialize_promise;
207
217
  return this;
208
218
  }
209
219
 
@@ -257,7 +267,10 @@ export class Session implements AgentSession {
257
267
  /**
258
268
  * 写入当前 session 默认配置。
259
269
  */
260
- async set(input: AgentSessionSetInput): Promise<void> {
270
+ async set(
271
+ input: AgentSessionSetInput,
272
+ options?: AgentSessionSetOptions,
273
+ ): Promise<void> {
261
274
  if (!input.model && !input.security) {
262
275
  throw new Error("session.set requires model or security");
263
276
  }
@@ -269,38 +282,49 @@ export class Session implements AgentSession {
269
282
  ) {
270
283
  throw new Error("security.approval_mode must be ask or always-allow");
271
284
  }
272
- const model_config = input.model
285
+ const persist_action = options?.persist_action !== false;
286
+ const publish_mutation =
287
+ options?.publish_mutation === undefined
288
+ ? persist_action
289
+ : options.publish_mutation;
290
+ if (!persist_action && publish_mutation) {
291
+ throw new Error(
292
+ "session.set publish_mutation requires persist_action",
293
+ );
294
+ }
295
+ const model_result = input.model
273
296
  ? await this.state.set_model(input.model)
274
297
  : undefined;
275
298
  const next_approval_mode = requested_approval_mode;
276
- const security_changed = Boolean(
277
- next_approval_mode && next_approval_mode !== this.configured_approval_mode,
278
- );
279
- if (next_approval_mode !== undefined) {
280
- this.configured_approval_mode = next_approval_mode;
281
- }
282
- if (!model_config && !security_changed) return;
299
+ const security_changed = next_approval_mode !== undefined
300
+ ? await this.state.set_approval_mode(next_approval_mode)
301
+ : false;
302
+ if (!model_result && !security_changed) return;
283
303
  const changed_fields = [
284
- ...(model_config
285
- ? [`model: ${String(model_config.model_label || "configured")}`]
304
+ ...(model_result?.changed
305
+ ? [`model: ${String(model_result.config.model_label || "configured")}`]
286
306
  : []),
287
307
  ...(security_changed
288
308
  ? [`security.approval_mode: ${String(next_approval_mode)}`]
289
309
  : []),
290
310
  ];
311
+ const completion = changed_fields.length > 0 && persist_action
312
+ ? {
313
+ type: "action" as const,
314
+ id: `session-config:${this.id}:${Date.now()}:${generate_id()}`,
315
+ title: "Session configuration updated",
316
+ description: changed_fields.join("; "),
317
+ publish_mutation,
318
+ }
319
+ : undefined;
291
320
  this.enqueue_command({
292
321
  execute: async () => {
293
- if (model_config) this.state.apply_model_config(model_config);
322
+ if (model_result) this.state.apply_model_config(model_result.config);
294
323
  if (security_changed && next_approval_mode) {
295
324
  this.shell_approval_adapter.set_effective_mode(next_approval_mode);
296
325
  }
297
326
  },
298
- completion: {
299
- type: "action",
300
- id: `session-config:${this.id}:${Date.now()}:${generate_id()}`,
301
- title: "Session configuration updated",
302
- description: changed_fields.join("; "),
303
- },
327
+ ...(completion ? { completion } : {}),
304
328
  });
305
329
  }
306
330
 
@@ -432,7 +456,7 @@ export class Session implements AgentSession {
432
456
  state: active_turn_id || this.executor.is_executing() ? "running" : "idle",
433
457
  ...(active_turn_id ? { active_turn_id } : {}),
434
458
  security: {
435
- approval_mode: this.configured_approval_mode,
459
+ approval_mode: this.state.get_approval_mode(),
436
460
  effective_approval_mode: this.shell_approval_adapter.get_effective_mode(),
437
461
  },
438
462
  };
@@ -453,8 +477,8 @@ export class Session implements AgentSession {
453
477
  text: String(input.text || "").trim(),
454
478
  });
455
479
  if (!appended) return;
456
- await this.state.ensure_title_from_history({ generate: true });
457
- await this.state.touch_metadata();
480
+ this.state.touch_metadata_in_background();
481
+ this.state.schedule_title_generation();
458
482
  }
459
483
 
460
484
  /**
@@ -555,9 +579,12 @@ export class Session implements AgentSession {
555
579
  await forked.initialize();
556
580
  const session_config = this.state.get_config();
557
581
  if (session_config.model) {
558
- const forked_config = await forked.state.set_model(session_config.model);
559
- forked.state.apply_model_config(forked_config);
582
+ const forked_model = await forked.state.set_model(session_config.model);
583
+ forked.state.apply_model_config(forked_model.config);
560
584
  }
585
+ const approval_mode = this.state.get_approval_mode();
586
+ await forked.state.set_approval_mode(approval_mode);
587
+ forked.shell_approval_adapter.set_effective_mode(approval_mode);
561
588
  await forked.session_messages.import_messages(fork_messages);
562
589
  await this.emit_action_event({
563
590
  id: action_id,
@@ -610,8 +637,8 @@ export class Session implements AgentSession {
610
637
  message_params,
611
638
  );
612
639
  if (!appended) return;
613
- await this.state.ensure_title_from_history({ generate: true });
614
- await this.state.touch_metadata();
640
+ this.state.touch_metadata_in_background();
641
+ this.state.schedule_title_generation();
615
642
  },
616
643
  append_assistant_message: async (message_params) => {
617
644
  const appended = await this.session_messages.append_external_assistant_message({
@@ -629,6 +656,11 @@ export class Session implements AgentSession {
629
656
  return this.runtime_port;
630
657
  }
631
658
 
659
+ /** 取消并释放当前 Session 的标题后台任务。 */
660
+ dispose_title_generation(): void {
661
+ this.state.dispose_title_generation();
662
+ }
663
+
632
664
  /**
633
665
  * 在执行前确保 session 已完成初始化与宿主装配。
634
666
  */
@@ -710,6 +742,7 @@ export class Session implements AgentSession {
710
742
  return {
711
743
  session_config: {},
712
744
  effective_session_config: {},
745
+ configured_approval_mode: "ask",
713
746
  created_at: Date.now(),
714
747
  timezone: resolve_system_timezone(),
715
748
  initialize_promise: null,
@@ -279,6 +279,8 @@ export class SessionLoop {
279
279
  session_id: this.session_id,
280
280
  turn_id,
281
281
  },
282
+ }, {
283
+ publish_mutation: completion.publish_mutation !== false,
282
284
  });
283
285
  } catch (error) {
284
286
  try {
@@ -591,16 +593,17 @@ export class SessionLoop {
591
593
  turn_id,
592
594
  input_type,
593
595
  });
594
- await this.state.ensure_title_from_history({ generate: true });
595
- await this.state.touch_metadata();
596
+ this.state.touch_metadata_in_background();
597
+ this.state.schedule_title_generation();
596
598
  return message;
597
599
  }
598
600
 
599
601
  /** 持久化 Executor Action,并同步刷新 Session metadata。 */
600
602
  private async persist_action_event(
601
603
  event: SessionActionRecordV1,
604
+ options?: { publish_mutation?: boolean },
602
605
  ): Promise<void> {
603
- await this.messages.persist_action_record(event);
606
+ await this.messages.persist_action_record(event, options);
604
607
  await this.state.touch_metadata();
605
608
  }
606
609
 
@@ -50,7 +50,7 @@ import {
50
50
  from_ui_user_parts,
51
51
  to_executor_ui_message,
52
52
  } from "@/session/messages/SessionMessageCodec.js";
53
- import { hydrate_user_prompt_file_parts } from "@executor/messages/SessionAttachmentMapper.js";
53
+ import { persist_user_prompt_file_parts } from "@executor/messages/SessionAttachmentMapper.js";
54
54
  import type {
55
55
  AppendCompletedAssistantMessageInput,
56
56
  AppendExternalSessionAssistantMessageInput,
@@ -63,6 +63,7 @@ import type {
63
63
  SessionMessagesOptions,
64
64
  } from "@/types/session/SessionMessages.js";
65
65
  import type { SessionMessageStore } from "@/types/store/SessionStore.js";
66
+ import type { SessionAttachmentStore } from "@/types/store/SessionAttachmentStore.js";
66
67
 
67
68
  export { SessionAssistantMessageWriter } from "@/session/messages/SessionAssistantMessageWriter.js";
68
69
 
@@ -70,6 +71,7 @@ export { SessionAssistantMessageWriter } from "@/session/messages/SessionAssista
70
71
  export class SessionMessages {
71
72
  readonly session_id: string;
72
73
  private readonly store: SessionMessageStore;
74
+ private readonly attachment_store: SessionAttachmentStore;
73
75
  private readonly publish: SessionMessagesOptions["publish"];
74
76
  private readonly messages_by_id = new Map<string, SessionMessage>();
75
77
  /** 按 Assistant Message 隔离的完整写事务链。 */
@@ -81,6 +83,7 @@ export class SessionMessages {
81
83
  constructor(options: SessionMessagesOptions) {
82
84
  this.session_id = String(options.session_id || "").trim();
83
85
  this.store = options.store;
86
+ this.attachment_store = options.attachment_store;
84
87
  this.publish = options.publish;
85
88
  if (!this.session_id) throw new Error("SessionMessages requires session_id");
86
89
  this.interaction_writer = new SessionMessageInteractionWriter({
@@ -282,9 +285,9 @@ export class SessionMessages {
282
285
  const query = input.prompt.query;
283
286
  const ui_parts = typeof query === "string"
284
287
  ? [{ type: "text" as const, text: query.trim() }]
285
- : await hydrate_user_prompt_file_parts(
288
+ : await persist_user_prompt_file_parts(
286
289
  Array.isArray(query) ? query : [],
287
- input.project_root,
290
+ this.attachment_store,
288
291
  );
289
292
  const canonical = await this.append_user_message({
290
293
  turn_id: input.turn_id,
@@ -315,7 +318,11 @@ export class SessionMessages {
315
318
  }
316
319
 
317
320
  /** 按稳定 Action ID 创建或更新 canonical Action Message。 */
318
- async persist_action_record(event: SessionActionRecordV1): Promise<void> {
321
+ async persist_action_record(
322
+ event: SessionActionRecordV1,
323
+ options?: { publish_mutation?: boolean },
324
+ ): Promise<void> {
325
+ const publish_mutation = options?.publish_mutation !== false;
319
326
  const existing = this.get_message(event.id);
320
327
  if (!existing) {
321
328
  const writer = await this.open_action_message({
@@ -324,6 +331,7 @@ export class SessionMessages {
324
331
  action_type: infer_action_type(event.id),
325
332
  title: event.title,
326
333
  description: event.description,
334
+ publish_mutation,
327
335
  });
328
336
  if (event.state === "completed") await writer.complete();
329
337
  if (event.state === "failed") {
@@ -335,7 +343,7 @@ export class SessionMessages {
335
343
  await this.update_action_message(event.id, event.state, {
336
344
  title: event.title,
337
345
  description: event.description,
338
- });
346
+ }, { publish_mutation });
339
347
  }
340
348
  }
341
349
 
@@ -360,8 +368,12 @@ export class SessionMessages {
360
368
  title: input.title,
361
369
  ...(input.description ? { description: input.description } : {}),
362
370
  ...(input.data ? { data: structuredClone(input.data) } : {}),
363
- }))) as SessionActionMessage;
364
- return new SessionActionMessageWriter(this, message.message_id);
371
+ }), false, input.publish_mutation !== false)) as SessionActionMessage;
372
+ return new SessionActionMessageWriter(
373
+ this,
374
+ message.message_id,
375
+ input.publish_mutation !== false,
376
+ );
365
377
  }
366
378
 
367
379
  /** 更新 Action 状态,同时保持 message_id 与 sequence 不变。 */
@@ -369,6 +381,7 @@ export class SessionMessages {
369
381
  message_id: string,
370
382
  status: "running" | "completed" | "failed",
371
383
  changes?: { title?: string; description?: string; data?: JsonObject },
384
+ options?: { publish_mutation?: boolean },
372
385
  ): Promise<SessionActionMessage> {
373
386
  const message = await this.store.append_message((state) => {
374
387
  const current = require_message(state.messages, message_id, "action");
@@ -385,7 +398,7 @@ export class SessionMessages {
385
398
  updated_at: created_at,
386
399
  } satisfies SessionActionMessage;
387
400
  });
388
- this.accept_message(message);
401
+ this.accept_message(message, options?.publish_mutation !== false);
389
402
  return message as SessionActionMessage;
390
403
  }
391
404
 
@@ -711,9 +724,10 @@ export class SessionMessages {
711
724
  async complete_assistant_message(
712
725
  message_id: string,
713
726
  status: "completed" | "stopped" | "failed",
727
+ error?: string,
714
728
  ): Promise<void> {
715
729
  await this.enqueue_assistant_write(message_id, async () => {
716
- await this.complete_assistant_message_serialized(message_id, status);
730
+ await this.complete_assistant_message_serialized(message_id, status, error);
717
731
  });
718
732
  }
719
733
 
@@ -721,6 +735,7 @@ export class SessionMessages {
721
735
  private async complete_assistant_message_serialized(
722
736
  message_id: string,
723
737
  status: "completed" | "stopped" | "failed",
738
+ error?: string,
724
739
  ): Promise<void> {
725
740
  const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
726
741
  require_streaming_assistant(current);
@@ -757,13 +772,18 @@ export class SessionMessages {
757
772
  }
758
773
  if (
759
774
  part.type === "tool" &&
760
- part.state === "waiting-user" &&
761
- interrupted_tool_ids.has(part.tool_call_id)
775
+ part.state !== "completed" &&
776
+ part.state !== "failed"
762
777
  ) {
763
778
  return {
764
779
  ...part,
765
780
  state: "failed" as const,
766
- error: "Interaction cancelled",
781
+ error:
782
+ status === "stopped" &&
783
+ part.state === "waiting-user" &&
784
+ interrupted_tool_ids.has(part.tool_call_id)
785
+ ? "Interaction cancelled"
786
+ : error || "Tool did not complete before Assistant Message closed",
767
787
  };
768
788
  }
769
789
  return part;
@@ -776,6 +796,7 @@ export class SessionMessages {
776
796
  private async create_message(
777
797
  factory: (sequence: number, created_at: number) => SessionMessage,
778
798
  draft = false,
799
+ publish_mutation = true,
779
800
  ): Promise<SessionMessage> {
780
801
  await this.ensure_initialized();
781
802
  if (draft) {
@@ -786,13 +807,13 @@ export class SessionMessages {
786
807
  }
787
808
  return candidate;
788
809
  });
789
- this.accept_message(message);
810
+ this.accept_message(message, publish_mutation);
790
811
  return message;
791
812
  }
792
813
  const message = await this.store.append_message((state) =>
793
814
  factory(state.message_sequence, Date.now()),
794
815
  );
795
- this.accept_message(message);
816
+ this.accept_message(message, publish_mutation);
796
817
  return message;
797
818
  }
798
819
 
@@ -923,7 +944,11 @@ export class SessionMessages {
923
944
  } as SessionMessageSnapshotMutation;
924
945
  }
925
946
 
926
- private accept_message(message: SessionMessage): void {
947
+ private accept_message(message: SessionMessage, publish_mutation = true): void {
948
+ if (!publish_mutation) {
949
+ this.messages_by_id.set(message.message_id, structuredClone(message));
950
+ return;
951
+ }
927
952
  this.accept_mutation(this.build_message_mutation(message), message);
928
953
  }
929
954
 
@@ -941,26 +966,42 @@ export class SessionMessages {
941
966
  export class SessionActionMessageWriter {
942
967
  readonly message_id: string;
943
968
  private readonly messages: SessionMessages;
969
+ private readonly publish_mutation: boolean;
944
970
  private closed = false;
945
971
 
946
- constructor(messages: SessionMessages, message_id: string) {
972
+ constructor(
973
+ messages: SessionMessages,
974
+ message_id: string,
975
+ publish_mutation = true,
976
+ ) {
947
977
  this.messages = messages;
948
978
  this.message_id = message_id;
979
+ this.publish_mutation = publish_mutation;
949
980
  }
950
981
 
951
982
  /** 把 Action 更新为 completed。 */
952
983
  async complete(input?: { title?: string; description?: string; data?: JsonObject }): Promise<void> {
953
984
  if (this.closed) return;
954
- await this.messages.update_action_message(this.message_id, "completed", input);
985
+ await this.messages.update_action_message(
986
+ this.message_id,
987
+ "completed",
988
+ input,
989
+ { publish_mutation: this.publish_mutation },
990
+ );
955
991
  this.closed = true;
956
992
  }
957
993
 
958
994
  /** 把 Action 更新为 failed。 */
959
995
  async fail(error: unknown): Promise<void> {
960
996
  if (this.closed) return;
961
- await this.messages.update_action_message(this.message_id, "failed", {
962
- description: error instanceof Error ? error.message : String(error),
963
- });
997
+ await this.messages.update_action_message(
998
+ this.message_id,
999
+ "failed",
1000
+ {
1001
+ description: error instanceof Error ? error.message : String(error),
1002
+ },
1003
+ { publish_mutation: this.publish_mutation },
1004
+ );
964
1005
  this.closed = true;
965
1006
  }
966
1007
  }