@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,10 +21,21 @@ import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
21
21
  import { generate_id } from "@/utils/Id.js";
22
22
  import type { Logger } from "@/utils/logger/Logger.js";
23
23
  import { SessionMessages } from "@/session/SessionMessages.js";
24
+ import { SessionTitleTask } from "@/session/runtime/SessionTitleTask.js";
24
25
  import { to_executor_history } from "@/session/messages/SessionMessageCodec.js";
25
26
  import type { SessionMessage } from "@/types/session/SessionMessage.js";
27
+ import { is_session_message_record } from "@/executor/types/SessionRecords.js";
26
28
  import type { SessionStateOptions } from "@/types/session/SessionState.js";
27
29
  import type { SessionStore } from "@/types/store/SessionStore.js";
30
+ import type { SessionApprovalMode } from "@/types/session/SessionInteraction.js";
31
+
32
+ /** Session 模型配置写入结果。 */
33
+ export interface SessionModelSetResult {
34
+ /** 已包含最新运行时模型实例的 configured 快照。 */
35
+ config: AgentSessionConfigSnapshot;
36
+ /** 持久化模型身份是否发生真实变化。 */
37
+ changed: boolean;
38
+ }
28
39
 
29
40
  /**
30
41
  * 本地 Session 配置与 Metadata 状态管理器。
@@ -39,6 +50,8 @@ export class SessionState {
39
50
  private readonly ensure_configured_hook?: SessionStateOptions["ensure_configured_hook"];
40
51
  private readonly get_model: SessionStateOptions["get_model"];
41
52
  private readonly publish_event: SessionStateOptions["publish_event"];
53
+ private readonly title_task: SessionTitleTask;
54
+ private metadata_mutation_chain: Promise<void> = Promise.resolve();
42
55
 
43
56
  constructor(options: SessionStateOptions) {
44
57
  this.agent_id = options.agent_id;
@@ -50,6 +63,10 @@ export class SessionState {
50
63
  this.ensure_configured_hook = options.ensure_configured_hook;
51
64
  this.get_model = options.get_model;
52
65
  this.publish_event = options.publish_event;
66
+ this.title_task = new SessionTitleTask({
67
+ session_id: this.session_id,
68
+ logger: this.logger,
69
+ });
53
70
  }
54
71
 
55
72
  /**
@@ -61,6 +78,11 @@ export class SessionState {
61
78
  };
62
79
  }
63
80
 
81
+ /** 读取当前 Session 已接受的 Shell 审批模式。 */
82
+ get_approval_mode(): SessionApprovalMode {
83
+ return this.state.configured_approval_mode;
84
+ }
85
+
64
86
  /**
65
87
  * 读取当前 session 创建时间。
66
88
  */
@@ -99,10 +121,13 @@ export class SessionState {
99
121
  });
100
122
  this.state.created_at = created_at;
101
123
  this.state.timezone = timezone;
102
- this.state.session_config = {};
124
+ this.state.session_config = {
125
+ ...(metadata.model_label ? { model_label: metadata.model_label } : {}),
126
+ };
103
127
  this.state.effective_session_config = {
104
128
  ...this.state.session_config,
105
129
  };
130
+ this.state.configured_approval_mode = metadata.approval_mode || "ask";
106
131
  })();
107
132
  await this.state.initialize_promise;
108
133
  }
@@ -141,23 +166,44 @@ export class SessionState {
141
166
  /**
142
167
  * 写入当前 session 配置。
143
168
  */
144
- async set_model(model: AgentModel): Promise<AgentSessionConfigSnapshot> {
169
+ async set_model(model: AgentModel): Promise<SessionModelSetResult> {
145
170
  const next_model_label = infer_agent_model_label(model);
171
+ const changed = next_model_label !== this.state.session_config.model_label;
146
172
  const next_config: AgentSessionConfigSnapshot = {
147
173
  ...this.state.session_config,
148
174
  model,
149
175
  model_label: next_model_label,
150
176
  model_context_window: read_agent_model_context_window(model),
151
177
  };
152
- const metadata = await this.store.read_metadata();
153
- await this.store.write_metadata({
154
- ...metadata,
155
- agent_id: this.agent_id,
156
- updated_at: Date.now(),
157
- ...(next_model_label ? { model_label: next_model_label } : {}),
158
- });
178
+ if (changed) {
179
+ await this.run_metadata_mutation(async () => {
180
+ const metadata = await this.store.read_metadata();
181
+ await this.store.write_metadata({
182
+ ...metadata,
183
+ agent_id: this.agent_id,
184
+ updated_at: Date.now(),
185
+ ...(next_model_label ? { model_label: next_model_label } : {}),
186
+ });
187
+ });
188
+ }
159
189
  this.state.session_config = next_config;
160
- return next_config;
190
+ return { config: next_config, changed };
191
+ }
192
+
193
+ /** 接受并持久化当前 Session 的 Shell 审批模式。 */
194
+ async set_approval_mode(mode: SessionApprovalMode): Promise<boolean> {
195
+ if (mode === this.state.configured_approval_mode) return false;
196
+ await this.run_metadata_mutation(async () => {
197
+ const metadata = await this.store.read_metadata();
198
+ await this.store.write_metadata({
199
+ ...metadata,
200
+ agent_id: this.agent_id,
201
+ updated_at: Date.now(),
202
+ approval_mode: mode,
203
+ });
204
+ });
205
+ this.state.configured_approval_mode = mode;
206
+ return true;
161
207
  }
162
208
 
163
209
  /** 在 Session Step 检查点提交模型配置。 */
@@ -175,62 +221,107 @@ export class SessionState {
175
221
  const preview_text = resolve_message_preview(
176
222
  stats.latest_message || undefined,
177
223
  ).slice(0, 180);
178
- const metadata = await this.store.read_metadata();
179
- await this.store.write_metadata({
180
- ...metadata,
181
- agent_id: this.agent_id,
182
- updated_at: Date.now(),
183
- ...(this.state.session_config.model_label
184
- ? { model_label: this.state.session_config.model_label }
185
- : {}),
186
- message_count: stats.message_count,
187
- historyBytes: stats.history_bytes,
188
- ...(preview_text ? { preview_text: preview_text } : {}),
224
+ await this.run_metadata_mutation(async () => {
225
+ const metadata = await this.store.read_metadata();
226
+ await this.store.write_metadata({
227
+ ...metadata,
228
+ agent_id: this.agent_id,
229
+ updated_at: Date.now(),
230
+ ...(this.state.session_config.model_label
231
+ ? { model_label: this.state.session_config.model_label }
232
+ : {}),
233
+ message_count: stats.message_count,
234
+ historyBytes: stats.history_bytes,
235
+ ...(preview_text ? { preview_text: preview_text } : {}),
236
+ });
189
237
  });
190
238
  }
191
239
 
192
- /**
193
- * 确保当前 session 已持久化 title。
194
- */
195
- async ensure_title_from_history(input?: {
196
- /**
197
- * 是否允许调用模型生成标题。
198
- */
199
- generate?: boolean;
200
- }): Promise<void> {
201
- const messages = to_executor_history(
202
- this.session_id,
203
- await this.messages.context_snapshot(),
204
- );
205
- const before_metadata = await this.store.read_metadata();
206
- const before_title = String(before_metadata.title || "").trim();
207
- const next_metadata = await ensure_session_title({
208
- session_id: this.session_id,
209
- store: this.store,
210
- messages,
211
- ...(input?.generate
212
- ? {
213
- model: this.get_model(),
214
- }
215
- : {}),
216
- ...(this.state.session_config.model_label
217
- ? { model_label: this.state.session_config.model_label }
218
- : {}),
219
- logger: this.logger,
220
- generate: input?.generate === true,
240
+ /** 在后台刷新 Session metadata,不阻塞当前 Turn。 */
241
+ touch_metadata_in_background(): void {
242
+ void this.touch_metadata().catch(async (error) => {
243
+ try {
244
+ await this.logger.log("warn", "[agent] session_metadata.background_update_failed", {
245
+ session_id: this.session_id,
246
+ error: error instanceof Error ? error.message : String(error),
247
+ });
248
+ } catch {
249
+ // metadata 诊断日志失败不能影响当前 Turn。
250
+ }
221
251
  });
222
- const next_title = String(next_metadata.title || "").trim();
223
- if (!next_title || next_title === before_title) return;
224
- this.publish_event({
225
- mutation_id: generate_id(),
226
- variant: "session",
227
- type: "title",
228
- session_id: this.session_id,
229
- created_at: Date.now(),
230
- title: next_title,
252
+ }
253
+
254
+ /** 异步调度首条用户消息的 Session 标题生成。 */
255
+ schedule_title_generation(): void {
256
+ this.title_task.schedule(async (signal) => {
257
+ const before_metadata = await this.store.read_metadata();
258
+ if (String(before_metadata.title || "").trim()) return;
259
+ const records = to_executor_history(
260
+ this.session_id,
261
+ await this.messages.context_snapshot(),
262
+ );
263
+ const first_user_message = records.find(
264
+ (record) => is_session_message_record(record) && record.role === "user",
265
+ );
266
+ if (!first_user_message) return;
267
+ const first_user_message_id = first_user_message.id;
268
+ const before_title = String(before_metadata.title || "").trim();
269
+ const next_metadata = await ensure_session_title({
270
+ session_id: this.session_id,
271
+ store: this.store,
272
+ messages: records,
273
+ model: this.get_model(),
274
+ model_label: this.state.session_config.model_label,
275
+ logger: this.logger,
276
+ generate: true,
277
+ signal,
278
+ commit_title: async (title) => await this.run_metadata_mutation(async () => {
279
+ const latest_metadata = await this.store.read_metadata();
280
+ if (signal.aborted) return latest_metadata;
281
+ if (String(latest_metadata.title || "").trim()) return latest_metadata;
282
+ const latest_records = to_executor_history(
283
+ this.session_id,
284
+ await this.messages.context_snapshot(),
285
+ );
286
+ const source_exists = latest_records.some(
287
+ (record) =>
288
+ is_session_message_record(record) &&
289
+ record.id === first_user_message_id,
290
+ );
291
+ if (!source_exists || signal.aborted) return latest_metadata;
292
+ const next_metadata = { ...latest_metadata, title };
293
+ await this.store.write_metadata(next_metadata);
294
+ return next_metadata;
295
+ }),
296
+ });
297
+ const next_title = String(next_metadata.title || "").trim();
298
+ if (!next_title || next_title === before_title || signal.aborted) return;
299
+ this.publish_event({
300
+ mutation_id: generate_id(),
301
+ variant: "session",
302
+ type: "title",
303
+ session_id: this.session_id,
304
+ created_at: Date.now(),
305
+ title: next_title,
306
+ });
231
307
  });
232
308
  }
233
309
 
310
+ /** 取消并释放当前 Session 的标题后台任务。 */
311
+ dispose_title_generation(): void {
312
+ this.title_task.dispose();
313
+ }
314
+
315
+ /** 串行提交 Session metadata,避免后台标题覆盖其他字段。 */
316
+ private async run_metadata_mutation<T>(mutation: () => Promise<T>): Promise<T> {
317
+ const task = this.metadata_mutation_chain.then(mutation, mutation);
318
+ this.metadata_mutation_chain = task.then(
319
+ () => undefined,
320
+ () => undefined,
321
+ );
322
+ return await task;
323
+ }
324
+
234
325
  }
235
326
 
236
327
  function resolve_message_preview(message: SessionMessage | undefined): string {
@@ -53,6 +53,12 @@ export interface EnsureSessionTitleParams {
53
53
  * 是否允许调用模型生成标题。
54
54
  */
55
55
  generate?: boolean;
56
+
57
+ /** 可选的标题请求取消信号。 */
58
+ signal?: AbortSignal;
59
+
60
+ /** 可选的标题提交入口,用于与其他 metadata mutation 串行化。 */
61
+ commit_title?: (title: string) => Promise<SessionHistoryMetaV1>;
56
62
  }
57
63
 
58
64
  function truncateTitle(input: string, maxChars: number): string {
@@ -192,6 +198,9 @@ async function generateSessionTitle(input: {
192
198
  * 当前 session 运行日志器。
193
199
  */
194
200
  logger?: Logger;
201
+
202
+ /** 标题请求取消信号。 */
203
+ signal?: AbortSignal;
195
204
  }): Promise<string | undefined> {
196
205
  let observedStreamError: unknown;
197
206
  try {
@@ -208,6 +217,7 @@ async function generateSessionTitle(input: {
208
217
  onError: ({ error }) => {
209
218
  observedStreamError = error;
210
219
  },
220
+ abortSignal: input.signal,
211
221
  });
212
222
  const text = await result.text;
213
223
  const generatedTitle = normalizeGeneratedTitle(text);
@@ -277,11 +287,13 @@ export async function ensure_session_title(
277
287
  model_label: input.model_label,
278
288
  firstUserText,
279
289
  logger: input.logger,
290
+ signal: input.signal,
280
291
  });
281
292
  if (!generatedTitle) return current;
282
293
 
294
+ if (input.commit_title) return await input.commit_title(generatedTitle);
283
295
  const generatedMeta: SessionHistoryMetaV1 = {
284
- ...current,
296
+ ...(await input.store.read_metadata()),
285
297
  title: generatedTitle,
286
298
  };
287
299
  await input.store.write_metadata(generatedMeta);
@@ -559,9 +559,12 @@ export class SessionAssistantMessageWriter {
559
559
  }
560
560
 
561
561
  /** 以失败状态关闭当前 Assistant Message。 */
562
- async fail(_error: unknown): Promise<void> {
562
+ async fail(error: unknown): Promise<void> {
563
563
  await this.enqueue_write(async () => {
564
- await this.close_serialized("failed");
564
+ await this.close_serialized(
565
+ "failed",
566
+ error instanceof Error ? error.message : String(error || ""),
567
+ );
565
568
  });
566
569
  }
567
570
 
@@ -786,13 +789,14 @@ export class SessionAssistantMessageWriter {
786
789
  /** 在队列内关闭当前 Assistant Message。 */
787
790
  private async close_serialized(
788
791
  status: "completed" | "stopped" | "failed",
792
+ error?: string,
789
793
  ): Promise<void> {
790
794
  if (this.closed) return;
791
795
  this.tool_part_gate.close(
792
796
  `Assistant Message writer closed with status ${status}`,
793
797
  );
794
798
  this.reset_step_state();
795
- await this.recorder.complete_assistant_message(this.message_id, status);
799
+ await this.recorder.complete_assistant_message(this.message_id, status, error);
796
800
  this.closed = true;
797
801
  }
798
802
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Session 标题后台任务协调器。
3
+ *
4
+ * 关键点(中文)
5
+ * - 标题生成属于 metadata 增强任务,不得阻塞 Turn 主链路。
6
+ * - 同一个 Session 同时只允许一个标题任务运行。
7
+ * - 任务失败只记录日志,不能反向影响 Prompt 或 Executor。
8
+ */
9
+
10
+ import type { Logger } from "@/utils/logger/Logger.js";
11
+
12
+ /** 标题后台任务执行函数。 */
13
+ export type SessionTitleTaskRunner = (signal: AbortSignal) => Promise<void>;
14
+
15
+ /** Session 标题后台任务协调器配置。 */
16
+ export interface SessionTitleTaskOptions {
17
+ /** 当前 Session 的稳定标识。 */
18
+ session_id: string;
19
+
20
+ /** 用于记录后台任务失败的统一日志器。 */
21
+ logger: Logger;
22
+ }
23
+
24
+ /** Session 标题后台任务协调器。 */
25
+ export class SessionTitleTask {
26
+ private readonly session_id: string;
27
+ private readonly logger: Logger;
28
+ private active_task: Promise<void> | null = null;
29
+ private abort_controller: AbortController | null = null;
30
+ private pending_runner: SessionTitleTaskRunner | null = null;
31
+ private disposed = false;
32
+
33
+ constructor(options: SessionTitleTaskOptions) {
34
+ this.session_id = String(options.session_id || "").trim();
35
+ this.logger = options.logger;
36
+ if (!this.session_id) {
37
+ throw new Error("SessionTitleTask requires a non-empty session_id");
38
+ }
39
+ }
40
+
41
+ /** 调度标题任务;已有任务或已释放时直接忽略。 */
42
+ schedule(runner: SessionTitleTaskRunner): void {
43
+ if (this.disposed) return;
44
+ if (this.active_task) {
45
+ this.pending_runner = runner;
46
+ return;
47
+ }
48
+ const abort_controller = new AbortController();
49
+ this.abort_controller = abort_controller;
50
+ const task = Promise.resolve()
51
+ .then(async () => await runner(abort_controller.signal))
52
+ .catch(async (error) => {
53
+ if (abort_controller.signal.aborted) return;
54
+ try {
55
+ await this.logger.log("warn", "[agent] session_title.task_failed", {
56
+ session_id: this.session_id,
57
+ error: error instanceof Error ? error.message : String(error),
58
+ });
59
+ } catch {
60
+ // 标题诊断日志失败不能影响 Session 主流程。
61
+ }
62
+ })
63
+ .finally(() => {
64
+ if (this.active_task === task) {
65
+ this.active_task = null;
66
+ this.abort_controller = null;
67
+ const pending_runner = this.pending_runner;
68
+ this.pending_runner = null;
69
+ if (pending_runner && !this.disposed) this.schedule(pending_runner);
70
+ }
71
+ });
72
+ this.active_task = task;
73
+ }
74
+
75
+ /** 取消当前标题任务并阻止后续结果提交。 */
76
+ cancel(): void {
77
+ this.abort_controller?.abort();
78
+ }
79
+
80
+ /** 释放协调器;释放后不再接受新的标题任务。 */
81
+ dispose(): void {
82
+ this.disposed = true;
83
+ this.pending_runner = null;
84
+ this.cancel();
85
+ }
86
+ }
@@ -14,6 +14,13 @@ function normalizeModelLabel(input: unknown): string | undefined {
14
14
  return label || undefined;
15
15
  }
16
16
 
17
+ /** 归一化 Session Shell 审批模式。 */
18
+ function normalize_approval_mode(
19
+ input: unknown,
20
+ ): "ask" | "always-allow" | undefined {
21
+ return input === "ask" || input === "always-allow" ? input : undefined;
22
+ }
23
+
17
24
  /**
18
25
  * 归一化 session 标题。
19
26
  */
@@ -106,6 +113,9 @@ export function normalize_session_metadata(
106
113
  ...(normalizeModelLabel(raw.model_label)
107
114
  ? { model_label: normalizeModelLabel(raw.model_label) }
108
115
  : {}),
116
+ ...(normalize_approval_mode(raw.approval_mode)
117
+ ? { approval_mode: normalize_approval_mode(raw.approval_mode) }
118
+ : {}),
109
119
  ...(normalize_message_count(raw.message_count) !== undefined
110
120
  ? { message_count: normalize_message_count(raw.message_count) }
111
121
  : {}),
@@ -18,6 +18,7 @@ import type {
18
18
  AgentSessionForkInput,
19
19
  AgentSessionInfo,
20
20
  AgentSessionSetInput,
21
+ AgentSessionSetOptions,
21
22
  AgentSessionStatus,
22
23
  AgentSessionSummaryPage,
23
24
  AgentSessionSystemSnapshot,
@@ -121,7 +122,7 @@ export interface AgentSession extends AgentSessionActor {
121
122
  readonly config: AgentSessionConfigSnapshot;
122
123
 
123
124
  /** 写入当前 session 默认配置。 */
124
- set(input: AgentSessionSetInput): Promise<void>;
125
+ set(input: AgentSessionSetInput, options?: AgentSessionSetOptions): Promise<void>;
125
126
 
126
127
  /** 把当前 Session 首次生成后固定的完整 system 显式固化到 instruction.md。 */
127
128
  snapshot(): Promise<void>;
@@ -138,7 +139,7 @@ export interface AgentSession extends AgentSessionActor {
138
139
  */
139
140
  export interface RemoteAgentSession extends AgentSessionActor {
140
141
  /** 写入当前远程 Session 的可序列化动态配置。 */
141
- set(input: RemoteSessionSetInput): Promise<void>;
142
+ set(input: RemoteSessionSetInput, options?: AgentSessionSetOptions): Promise<void>;
142
143
 
143
144
  /** 从当前远程 session 创建一个分叉会话。 */
144
145
  fork(input?: AgentSessionForkInput | string): Promise<RemoteAgentSession>;
@@ -80,6 +80,28 @@ export interface AgentSessionSetInput {
80
80
  security?: AgentSessionSecurityConfig;
81
81
  }
82
82
 
83
+ /** Session 配置提交后的 Action 可观测性选项。 */
84
+ export interface AgentSessionSetOptions {
85
+ /**
86
+ * 是否把真实配置变化持久化为 completed Action Message。
87
+ *
88
+ * 关键点(中文)
89
+ * - 默认值为 true。
90
+ * - 设为 false 只关闭 Action Message,不影响配置写入与检查点提交。
91
+ * - 适用于 Session 初始化、恢复与其他不应进入时间线的宿主装配流程。
92
+ */
93
+ persist_action?: boolean;
94
+
95
+ /**
96
+ * 是否在 Action Message 持久化成功后发布对应 Session Mutation。
97
+ *
98
+ * 关键点(中文)
99
+ * - 默认跟随 `persist_action`。
100
+ * - 只有 `persist_action=true` 时才允许设为 true,避免发布不存在于 canonical Message 历史中的事件。
101
+ */
102
+ publish_mutation?: boolean;
103
+ }
104
+
83
105
  /** 可通过本地或远程 Session 动态更新的安全策略。 */
84
106
  export interface AgentSessionSecurityConfig {
85
107
  /** 高风险操作是否需要用户逐次审批。 */
@@ -15,6 +15,7 @@ import type {
15
15
  AgentArchiveSessionResult,
16
16
  AgentArchiveSessionsResult,
17
17
  AgentCleanArchiveResult,
18
+ AgentSessionSetOptions,
18
19
  RemoteSessionSetInput,
19
20
  AgentSessionSystemSnapshot,
20
21
  } from "@/types/agent/SessionTypes.js";
@@ -155,7 +156,11 @@ export type RpcRequest =
155
156
  /** 更新指定 Session 的可序列化动态配置。 */
156
157
  method: "sdk.sessions.set";
157
158
  /** Session 与动态配置参数。 */
158
- params: { session_id: string; input: RemoteSessionSetInput };
159
+ params: {
160
+ session_id: string;
161
+ input: RemoteSessionSetInput;
162
+ options?: AgentSessionSetOptions;
163
+ };
159
164
  }
160
165
  | {
161
166
  /** 请求 id,用于匹配响应。 */
@@ -19,6 +19,28 @@ export type SessionUserMessagePart = UIMessagePart<UIDataTypes, UITools>;
19
19
 
20
20
  /**
21
21
  * Session prompt 输入。
22
+ *
23
+ * 输入示例(中文):
24
+ * ```ts
25
+ * session.prompt({
26
+ * query: [
27
+ * { type: "text", text: "请分析这个附件" },
28
+ * {
29
+ * type: "file",
30
+ * mediaType: "image/png",
31
+ * url: "data:image/png;base64,...",
32
+ * filename: "image.png",
33
+ * },
34
+ * ],
35
+ * });
36
+ * ```
37
+ *
38
+ * 生命周期说明(中文):
39
+ * - `string` 会直接成为新的文本 User Message。
40
+ * - `file` part 如果携带 Data URL,会在 Message 入库前解码到 Session 的 attachments 目录。
41
+ * - 持久化 Message 里的 `url` 变成相对 Workspace 根目录的文件路径,而不是 Base64 内容。
42
+ * - 模型执行前,Executor 再按该路径读取文件,并转换成 AI SDK / Provider 可消费的 ModelMessage。
43
+ * - 远程 Session 中,Data URL 在服务端落盘;调用方本地路径必须对服务端可访问。
22
44
  */
23
45
  export interface AgentSessionPromptInput {
24
46
  /**
@@ -24,4 +24,6 @@ export interface SessionCommandCompletion {
24
24
  title: string;
25
25
  /** Action Message 展示的可选结果说明。 */
26
26
  description?: string;
27
+ /** Action Message 持久化后是否发布对应 Session Mutation。 */
28
+ publish_mutation?: boolean;
27
29
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { AgentSessionConfigSnapshot } from "@/types/agent/SessionTypes.js";
11
+ import type { SessionApprovalMode } from "@/types/session/SessionInteraction.js";
11
12
 
12
13
  /**
13
14
  * 本地 Session 内存状态。
@@ -27,6 +28,9 @@ export interface SessionLocalState {
27
28
  */
28
29
  effective_session_config: AgentSessionConfigSnapshot;
29
30
 
31
+ /** 当前 Session 已接受并持久化的 Shell 审批模式。 */
32
+ configured_approval_mode: SessionApprovalMode;
33
+
30
34
  /**
31
35
  * 当前 session 创建时间(毫秒时间戳)。
32
36
  */
@@ -7,6 +7,7 @@
7
7
  import type { JsonObject } from "@/types/common/Json.js";
8
8
  import type { SessionRecordV1 } from "@/executor/types/SessionRecords.js";
9
9
  import type { SessionMessageStore } from "@/types/store/SessionStore.js";
10
+ import type { SessionAttachmentStore } from "@/types/store/SessionAttachmentStore.js";
10
11
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
11
12
  import type {
12
13
  SessionAssistantMessagePart,
@@ -20,6 +21,8 @@ export interface SessionMessagesOptions {
20
21
  session_id: string;
21
22
  /** Message 快照持久化 Store。 */
22
23
  store: SessionMessageStore;
24
+ /** 当前 Session 的附件持久化能力。 */
25
+ attachment_store: SessionAttachmentStore;
23
26
  /** 持久化成功后的实时 Mutation 发布函数。 */
24
27
  publish: (mutation: SessionMutation) => void;
25
28
  }
@@ -80,6 +83,8 @@ export interface OpenSessionActionMessageInput {
80
83
  description?: string;
81
84
  /** 当前 Action 附加数据。 */
82
85
  data?: JsonObject;
86
+ /** Action 创建或更新后是否发布对应 Session Mutation。 */
87
+ publish_mutation?: boolean;
83
88
  }
84
89
 
85
90
  /** Error Message 创建参数。 */
@@ -31,6 +31,9 @@ export interface AgentManagedSession extends AgentSession {
31
31
  */
32
32
  get_runtime_port(): SessionPort;
33
33
 
34
+ /** 取消并释放当前 Session 的标题后台任务。 */
35
+ dispose_title_generation?(): void;
36
+
34
37
  /**
35
38
  * 返回当前 session 是否正在执行。
36
39
  */
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Session 附件持久化能力。
3
+ *
4
+ * Session Message 只保存附件引用,附件内容由该 Store 在 Session 生命周期内持有。
5
+ *
6
+ * 数据边界(中文):
7
+ * - 输入边界:`session.prompt()` 接收 `type: "file"`、`mediaType` 和 Data URL。
8
+ * - 存储边界:Store 解码 Data URL,写入 `.downcity/.../attachments/att_<id>.<ext>`。
9
+ * - Message 边界:Session Message 只记录相对 Workspace 根目录的附件路径。
10
+ * - 模型边界:模型转换器读取该路径,在内存中恢复成模型需要的文件内容。
11
+ *
12
+ * 因此,Data URL 不会直接进入 JSONL Message;Session Message 是附件路径的唯一引用。
13
+ */
14
+
15
+ /** 可持久化的 Data URL 附件。 */
16
+ export interface SessionAttachmentStore {
17
+ /**
18
+ * 保存 Data URL,并返回相对 Workspace 根目录的稳定文件路径。
19
+ * 调用成功后,返回路径对应的附件文件必须已经完整落盘。
20
+ */
21
+ persist_data_url(input: {
22
+ /** 完整的 Data URL。 */
23
+ data_url: string;
24
+ /** 调用侧声明的 MIME 类型。 */
25
+ media_type: string;
26
+ /** 可选的原始文件名,仅用于推导扩展名。 */
27
+ filename?: string;
28
+ }): Promise<string>;
29
+ }