@downcity/agent 1.1.217 → 1.1.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  2. package/bin/agent/local/services/AgentSessions.js +1 -4
  3. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  4. package/bin/agent/remote/RemoteSession.d.ts +9 -3
  5. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  6. package/bin/agent/remote/RemoteSession.js +19 -4
  7. package/bin/agent/remote/RemoteSession.js.map +1 -1
  8. package/bin/agent/remote/RemoteTransport.d.ts +7 -3
  9. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  10. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +5 -2
  11. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  12. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +25 -11
  13. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  14. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +5 -2
  15. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  16. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +5 -2
  17. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  18. package/bin/executor/types/SessionRecords.d.ts +2 -0
  19. package/bin/executor/types/SessionRecords.d.ts.map +1 -1
  20. package/bin/executor/types/SessionRecords.js.map +1 -1
  21. package/bin/index.d.ts +5 -2
  22. package/bin/index.d.ts.map +1 -1
  23. package/bin/index.js +1 -0
  24. package/bin/index.js.map +1 -1
  25. package/bin/rpc/Client.d.ts +11 -4
  26. package/bin/rpc/Client.d.ts.map +1 -1
  27. package/bin/rpc/Client.js +14 -3
  28. package/bin/rpc/Client.js.map +1 -1
  29. package/bin/session/Session.d.ts +13 -3
  30. package/bin/session/Session.d.ts.map +1 -1
  31. package/bin/session/Session.js +47 -18
  32. package/bin/session/Session.js.map +1 -1
  33. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +45 -0
  34. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
  35. package/bin/session/recorder/JsonlSessionMessageStore.js +127 -0
  36. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -0
  37. package/bin/session/recorder/SessionMessageCodec.d.ts +20 -0
  38. package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -0
  39. package/bin/session/recorder/SessionMessageCodec.js +252 -0
  40. package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
  41. package/bin/session/recorder/SessionMessageReducer.d.ts +12 -0
  42. package/bin/session/recorder/SessionMessageReducer.d.ts.map +1 -0
  43. package/bin/session/recorder/SessionMessageReducer.js +109 -0
  44. package/bin/session/recorder/SessionMessageReducer.js.map +1 -0
  45. package/bin/session/recorder/SessionRecorder.d.ts +177 -0
  46. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
  47. package/bin/session/recorder/SessionRecorder.js +610 -0
  48. package/bin/session/recorder/SessionRecorder.js.map +1 -0
  49. package/bin/session/recorder/SessionRecorderCompaction.d.ts +18 -0
  50. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -0
  51. package/bin/session/recorder/SessionRecorderCompaction.js +141 -0
  52. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -0
  53. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +66 -0
  54. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -0
  55. package/bin/session/recorder/SessionRecorderHistoryStore.js +195 -0
  56. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -0
  57. package/bin/session/runtime/SessionMessageEventHub.d.ts +15 -0
  58. package/bin/session/runtime/SessionMessageEventHub.d.ts.map +1 -0
  59. package/bin/session/runtime/SessionMessageEventHub.js +28 -0
  60. package/bin/session/runtime/SessionMessageEventHub.js.map +1 -0
  61. package/bin/session/runtime/SessionPromptRuntime.d.ts +7 -13
  62. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  63. package/bin/session/runtime/SessionPromptRuntime.js +13 -13
  64. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  65. package/bin/session/services/SessionStateService.d.ts +7 -3
  66. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  67. package/bin/session/services/SessionStateService.js +132 -72
  68. package/bin/session/services/SessionStateService.js.map +1 -1
  69. package/bin/session/services/SessionTurnService.d.ts +4 -0
  70. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  71. package/bin/session/services/SessionTurnService.js +82 -52
  72. package/bin/session/services/SessionTurnService.js.map +1 -1
  73. package/bin/session/services/SessionViewService.d.ts +8 -3
  74. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  75. package/bin/session/services/SessionViewService.js +10 -5
  76. package/bin/session/services/SessionViewService.js.map +1 -1
  77. package/bin/session/storage/Persistence.d.ts +6 -0
  78. package/bin/session/storage/Persistence.d.ts.map +1 -1
  79. package/bin/session/storage/Persistence.js +7 -0
  80. package/bin/session/storage/Persistence.js.map +1 -1
  81. package/bin/session/storage/RuntimeSessionPort.d.ts +3 -2
  82. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  83. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  84. package/bin/types/agent/SessionActor.d.ts +10 -4
  85. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  86. package/bin/types/rpc/RpcProtocol.d.ts +20 -6
  87. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  88. package/bin/types/runtime/agent/AgentContext.d.ts +4 -3
  89. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  90. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  91. package/bin/types/sdk/AgentSessionEvent.d.ts +2 -1
  92. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
  93. package/bin/types/session/SessionMessage.d.ts +217 -0
  94. package/bin/types/session/SessionMessage.d.ts.map +1 -0
  95. package/bin/types/session/SessionMessage.js +8 -0
  96. package/bin/types/session/SessionMessage.js.map +1 -0
  97. package/bin/types/session/SessionMessageMutation.d.ts +92 -0
  98. package/bin/types/session/SessionMessageMutation.d.ts.map +1 -0
  99. package/bin/types/session/SessionMessageMutation.js +21 -0
  100. package/bin/types/session/SessionMessageMutation.js.map +1 -0
  101. package/package.json +3 -3
  102. package/scripts/agent-env.test.mjs +4 -4
  103. package/scripts/session-action-message.test.mjs +98 -27
  104. package/scripts/session-list-title.test.mjs +6 -19
  105. package/scripts/session-prompt-runtime.test.mjs +4 -16
  106. package/scripts/session-recorder.test.mjs +245 -0
  107. package/scripts/session-title-event.test.mjs +10 -18
  108. package/src/agent/local/services/AgentSessions.ts +1 -3
  109. package/src/agent/remote/RemoteSession.ts +35 -7
  110. package/src/agent/remote/RemoteTransport.ts +17 -6
  111. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +45 -13
  112. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +19 -6
  113. package/src/executor/types/SessionRecords.ts +2 -0
  114. package/src/index.ts +23 -5
  115. package/src/rpc/Client.ts +29 -8
  116. package/src/session/Session.ts +65 -31
  117. package/src/session/recorder/JsonlSessionMessageStore.ts +162 -0
  118. package/src/session/recorder/SessionMessageCodec.ts +279 -0
  119. package/src/session/recorder/SessionMessageReducer.ts +155 -0
  120. package/src/session/recorder/SessionRecorder.ts +812 -0
  121. package/src/session/recorder/SessionRecorderCompaction.ts +187 -0
  122. package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
  123. package/src/session/runtime/SessionMessageEventHub.ts +35 -0
  124. package/src/session/runtime/SessionPromptRuntime.ts +23 -24
  125. package/src/session/services/SessionStateService.ts +151 -80
  126. package/src/session/services/SessionTurnService.ts +88 -59
  127. package/src/session/services/SessionViewService.ts +25 -13
  128. package/src/session/storage/Persistence.ts +13 -0
  129. package/src/session/storage/RuntimeSessionPort.ts +6 -4
  130. package/src/types/agent/SessionActor.ts +26 -6
  131. package/src/types/rpc/RpcProtocol.ts +20 -6
  132. package/src/types/runtime/agent/AgentContext.ts +8 -2
  133. package/src/types/sdk/AgentSessionEvent.ts +2 -0
  134. package/src/types/session/SessionMessage.ts +247 -0
  135. package/src/types/session/SessionMessageMutation.ts +121 -0
  136. package/tsconfig.tsbuildinfo +1 -1
  137. package/bin/session/SessionEventMapper.d.ts +0 -32
  138. package/bin/session/SessionEventMapper.d.ts.map +0 -1
  139. package/bin/session/SessionEventMapper.js +0 -131
  140. package/bin/session/SessionEventMapper.js.map +0 -1
  141. package/bin/types/sdk/InternalUiChunkEvent.d.ts +0 -100
  142. package/bin/types/sdk/InternalUiChunkEvent.d.ts.map +0 -1
  143. package/bin/types/sdk/InternalUiChunkEvent.js +0 -9
  144. package/bin/types/sdk/InternalUiChunkEvent.js.map +0 -1
  145. package/scripts/session-history-archive.test.mjs +0 -188
  146. package/src/session/SessionEventMapper.ts +0 -148
  147. package/src/types/sdk/InternalUiChunkEvent.ts +0 -108
@@ -19,10 +19,9 @@ import {
19
19
  } from "@/session/storage/Metadata.js";
20
20
  import { touchSessionMetadata } from "@/session/storage/Persistence.js";
21
21
  import { ensureSessionTitle } from "@/session/SessionTitle.js";
22
- import { persistSdkAssistantResult } from "@/session/storage/Persistence.js";
23
22
  import { hydrateUserPromptFileParts } from "@executor/messages/SessionAttachmentMapper.js";
24
23
  import type { Executor } from "@executor/Executor.js";
25
- import type { JsonlSessionHistoryStore } from "@/executor/store/history/jsonl/JsonlSessionHistoryStore.js";
24
+ import type { SessionHistoryStore } from "@/executor/store/history/SessionHistoryStore.js";
26
25
  import type {
27
26
  AgentSessionConfigSnapshot,
28
27
  AgentSessionSetInput,
@@ -38,11 +37,20 @@ import type {
38
37
  SessionMessageRecordV1,
39
38
  SessionUserMessageV1,
40
39
  } from "@/executor/types/SessionRecords.js";
41
- import { to_session_action_record } from "@/executor/types/SessionRecords.js";
42
40
  import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
43
41
  import { generateId } from "@/utils/Id.js";
44
42
  import type { Logger } from "@/utils/logger/Logger.js";
45
43
  import type { AgentModel } from "@/model/CityModelAdapter.js";
44
+ import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
45
+ import { normalize_session_user_parts } from "@/session/recorder/SessionRecorder.js";
46
+ import {
47
+ from_ui_assistant_parts,
48
+ from_ui_user_parts,
49
+ to_executor_ui_message,
50
+ } from "@/session/recorder/SessionMessageCodec.js";
51
+ import fs from "fs-extra";
52
+ import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
53
+ import type { SessionMessage } from "@/types/session/SessionMessage.js";
46
54
 
47
55
  type SessionStateServiceOptions = {
48
56
  /**
@@ -63,7 +71,10 @@ type SessionStateServiceOptions = {
63
71
  /**
64
72
  * 当前 session 历史事实源。
65
73
  */
66
- history_store: JsonlSessionHistoryStore;
74
+ history_store: SessionHistoryStore;
75
+
76
+ /** 当前 Session Message Recorder。 */
77
+ recorder: SessionRecorder;
67
78
 
68
79
  /**
69
80
  * 当前 session 执行器。
@@ -114,7 +125,8 @@ export class SessionStateService {
114
125
  private readonly agent_id: string;
115
126
  private readonly project_root: string;
116
127
  private readonly session_id: string;
117
- private readonly history_store: JsonlSessionHistoryStore;
128
+ private readonly history_store: SessionHistoryStore;
129
+ private readonly recorder: SessionRecorder;
118
130
  private readonly executor: Executor;
119
131
  private readonly state: SessionLocalState;
120
132
  private readonly logger: Logger;
@@ -127,6 +139,7 @@ export class SessionStateService {
127
139
  this.project_root = options.project_root;
128
140
  this.session_id = options.session_id;
129
141
  this.history_store = options.history_store;
142
+ this.recorder = options.recorder;
130
143
  this.executor = options.executor;
131
144
  this.state = options.state;
132
145
  this.logger = options.logger;
@@ -197,6 +210,7 @@ export class SessionStateService {
197
210
  : {}),
198
211
  ...(metadata.modelId ? { modelId: metadata.modelId } : {}),
199
212
  };
213
+ await this.restore_persisted_model();
200
214
  })();
201
215
  await this.state.initializePromise;
202
216
  }
@@ -206,7 +220,6 @@ export class SessionStateService {
206
220
  */
207
221
  async ensure_ready_for_execution(): Promise<void> {
208
222
  await this.initialize();
209
- await this.restore_persisted_model();
210
223
  if (this.state.ensureConfiguredPromise) {
211
224
  await this.state.ensureConfiguredPromise;
212
225
  return;
@@ -239,6 +252,9 @@ export class SessionStateService {
239
252
  async set(input: AgentSessionSetInput, options?: SessionSetOptions): Promise<void> {
240
253
  const should_emit_action = options?.emit_action !== false;
241
254
  const previous_model_label = this.state.sessionConfig.modelLabel;
255
+ const previous_model_id = String(
256
+ this.state.sessionConfig.modelId || previous_model_label || "",
257
+ ).trim();
242
258
  const requested_model_id = String(input.modelId || "").trim();
243
259
  const next_model = input.model || (requested_model_id
244
260
  ? await this.resolve_model_by_id(requested_model_id)
@@ -251,19 +267,18 @@ export class SessionStateService {
251
267
  next_model &&
252
268
  should_emit_action &&
253
269
  this.state.sessionConfig.model &&
254
- previous_model_label &&
255
- next_model_label &&
256
- previous_model_label !== next_model_label,
270
+ previous_model_id &&
271
+ next_model_id &&
272
+ previous_model_id !== next_model_id,
257
273
  );
274
+ const previous_model_name = previous_model_label || previous_model_id;
275
+ const next_model_name = next_model_label || next_model_id;
258
276
  const action_id = `model-switching:${this.session_id}:${Date.now()}:${generateId()}`;
259
277
 
260
278
  if (should_emit_model_switch_action) {
261
279
  await this.emit_action_event({
262
280
  id: action_id,
263
- title: "Switching session model",
264
- description: next_model_label
265
- ? `Switching to ${next_model_label}.`
266
- : undefined,
281
+ title: `Switching session model from ${previous_model_name} to ${next_model_name}`,
267
282
  state: "running",
268
283
  });
269
284
  }
@@ -287,7 +302,7 @@ export class SessionStateService {
287
302
  if (should_emit_model_switch_action) {
288
303
  await this.emit_action_event({
289
304
  id: action_id,
290
- title: "Session model switch failed",
305
+ title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
291
306
  description: error instanceof Error ? error.message : String(error),
292
307
  state: "failed",
293
308
  });
@@ -298,10 +313,7 @@ export class SessionStateService {
298
313
  if (should_emit_model_switch_action) {
299
314
  await this.emit_action_event({
300
315
  id: action_id,
301
- title: "Session model switched",
302
- description: this.state.sessionConfig.modelLabel
303
- ? `Using ${this.state.sessionConfig.modelLabel}.`
304
- : undefined,
316
+ title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
305
317
  state: "completed",
306
318
  });
307
319
  }
@@ -331,7 +343,20 @@ export class SessionStateService {
331
343
  message?: SessionRecordV1 | null;
332
344
  text?: string;
333
345
  }): Promise<void> {
334
- await this.executor.append_user_message(params);
346
+ const parts = params.message && "role" in params.message
347
+ ? from_ui_user_parts(params.message.parts)
348
+ : [{
349
+ part_id: `external-user-text:${Date.now()}`,
350
+ type: "text" as const,
351
+ text: String(params.text || "").trim(),
352
+ state: "done" as const,
353
+ }];
354
+ if (parts.length === 0) return;
355
+ await this.recorder.append_user_message({
356
+ turn_id: `external:${this.session_id}:${Date.now()}`,
357
+ input_type: "prompt",
358
+ parts,
359
+ });
335
360
  await this.ensure_title_from_history({ generate: true });
336
361
  await this.touch_metadata();
337
362
  }
@@ -343,7 +368,16 @@ export class SessionStateService {
343
368
  message?: SessionRecordV1 | null;
344
369
  fallbackText?: string;
345
370
  }): Promise<void> {
346
- await this.executor.append_assistant_message(params);
371
+ const parts = params.message && "role" in params.message
372
+ ? from_ui_assistant_parts(params.message.parts)
373
+ : [{
374
+ part_id: `external-assistant-text:${Date.now()}`,
375
+ type: "text" as const,
376
+ text: String(params.fallbackText || "").trim(),
377
+ state: "done" as const,
378
+ }];
379
+ if (parts.length === 0) return;
380
+ await this.recorder.append_completed_assistant_message({ parts });
347
381
  await this.touch_metadata();
348
382
  }
349
383
 
@@ -351,11 +385,29 @@ export class SessionStateService {
351
385
  * 仅刷新当前 session metadata。
352
386
  */
353
387
  async touch_metadata(): Promise<void> {
388
+ const message_page = await this.recorder.list_messages({
389
+ limit: 500,
390
+ include_internal: true,
391
+ });
392
+ const messages_path = getSdkAgentSessionMessagesPath(
393
+ this.project_root,
394
+ this.agent_id,
395
+ this.session_id,
396
+ );
397
+ const history_bytes = await fs.stat(messages_path)
398
+ .then((file_stat) => file_stat.size)
399
+ .catch(() => 0);
400
+ const preview_text = resolve_message_preview(
401
+ message_page.items[message_page.items.length - 1],
402
+ ).slice(0, 180);
354
403
  await touchSessionMetadata({
355
404
  projectRoot: this.project_root,
356
405
  agentId: this.agent_id,
357
406
  sessionId: this.session_id,
358
407
  sessionConfig: this.state.sessionConfig,
408
+ message_count: message_page.total,
409
+ history_bytes,
410
+ ...(preview_text ? { preview_text } : {}),
359
411
  });
360
412
  }
361
413
 
@@ -402,14 +454,8 @@ export class SessionStateService {
402
454
  async persist_assistant_result(
403
455
  assistant_message?: SessionMessageRecordV1 | null,
404
456
  ): Promise<void> {
405
- await persistSdkAssistantResult({
406
- projectRoot: this.project_root,
407
- agentId: this.agent_id,
408
- sessionId: this.session_id,
409
- sessionConfig: this.state.sessionConfig,
410
- executor: this.executor,
411
- assistantMessage: assistant_message,
412
- });
457
+ void assistant_message;
458
+ await this.touch_metadata();
413
459
  }
414
460
 
415
461
  /**
@@ -418,8 +464,23 @@ export class SessionStateService {
418
464
  async persist_action_event(
419
465
  event: SessionActionRecordV1,
420
466
  ): Promise<void> {
421
- const message = to_session_action_record(event, this.session_id);
422
- await this.history_store.write_record(message);
467
+ const existing = this.recorder.get_message(event.id);
468
+ if (!existing) {
469
+ const writer = await this.recorder.open_action_message({
470
+ message_id: event.id,
471
+ turn_id: event.metadata.turnId,
472
+ action_type: infer_action_type(event.id),
473
+ title: event.title,
474
+ description: event.description,
475
+ });
476
+ if (event.state === "completed") await writer.complete();
477
+ if (event.state === "failed") await writer.fail(event.description || event.title);
478
+ } else if (existing.type === "action" && event.state !== "running") {
479
+ await this.recorder.update_action_message(event.id, event.state, {
480
+ title: event.title,
481
+ description: event.description,
482
+ });
483
+ }
423
484
  await this.touch_metadata();
424
485
  }
425
486
 
@@ -427,21 +488,26 @@ export class SessionStateService {
427
488
  * 写入并发布一条 action。
428
489
  */
429
490
  async emit_action_event(input: EmitActionInput): Promise<void> {
430
- const event = to_session_action_record(
431
- {
432
- ...input,
433
- id:
434
- String(input.id || "").trim() ||
435
- `action:${this.session_id}:${Date.now()}:${generateId()}`,
491
+ const action_id = String(input.id || "").trim() ||
492
+ `action:${this.session_id}:${Date.now()}`;
493
+ const turn_id = "turnId" in input
494
+ ? input.turnId
495
+ : input.metadata?.turnId;
496
+ await this.persist_action_event({
497
+ type: "action",
498
+ id: action_id,
499
+ title: input.title,
500
+ ...(input.description ? { description: input.description } : {}),
501
+ state: input.state,
502
+ metadata: {
503
+ v: 1,
504
+ ts: input.metadata?.ts || Date.now(),
505
+ sessionId: this.session_id,
506
+ ...(turn_id
507
+ ? { turnId: turn_id }
508
+ : {}),
436
509
  },
437
- this.session_id,
438
- );
439
- try {
440
- await this.persist_action_event(event);
441
- } catch {
442
- // action 持久化失败不应阻断宿主操作。
443
- }
444
- this.publish_event(event);
510
+ });
445
511
  }
446
512
 
447
513
  /**
@@ -449,43 +515,22 @@ export class SessionStateService {
449
515
  */
450
516
  async create_and_persist_user_prompt_message(
451
517
  input: AgentSessionPromptInput,
518
+ turn_id: string,
519
+ input_type: "prompt" | "steer" = "prompt",
452
520
  ): Promise<SessionUserMessageV1> {
453
521
  const query = input.query;
454
- if (typeof query === "string") {
455
- const message = this.history_store.userText({
456
- text: query.trim(),
457
- metadata: {
458
- sessionId: this.session_id,
459
- },
460
- }) as SessionUserMessageV1;
461
- await this.executor.append_user_message({
462
- message,
463
- });
464
- await this.ensure_title_from_history({ generate: true });
465
- await this.touch_metadata();
466
- return message;
467
- }
468
-
469
- // query 是 parts 数组,先把本地图片附件转换为模型可消费的 data URL。
470
- const parts = await hydrateUserPromptFileParts(
471
- Array.isArray(query) ? query : [],
472
- this.project_root,
473
- );
474
- const message: SessionUserMessageV1 = {
475
- id: `u:${this.session_id}:${generateId()}`,
476
- role: "user",
477
- parts,
478
- metadata: {
479
- v: 1,
480
- ts: Date.now(),
481
- sessionId: this.session_id,
482
- source: "ingress",
483
- kind: "normal",
484
- },
485
- };
486
- await this.executor.append_user_message({
487
- message,
522
+ const ui_parts = typeof query === "string"
523
+ ? [{ type: "text" as const, text: query.trim() }]
524
+ : await hydrateUserPromptFileParts(
525
+ Array.isArray(query) ? query : [],
526
+ this.project_root,
527
+ );
528
+ const canonical = await this.recorder.append_user_message({
529
+ turn_id,
530
+ input_type,
531
+ parts: normalize_session_user_parts(ui_parts),
488
532
  });
533
+ const message = to_executor_ui_message(canonical) as SessionUserMessageV1;
489
534
  await this.ensure_title_from_history({ generate: true });
490
535
  await this.touch_metadata();
491
536
  return message;
@@ -502,10 +547,36 @@ export class SessionStateService {
502
547
  : [];
503
548
  if (normalized_messages.length <= 0) return;
504
549
  for (const message of normalized_messages) {
505
- await this.executor.append_user_message({
506
- message,
550
+ await this.recorder.append_user_message({
551
+ turn_id: String(message.metadata?.turnId || `deferred:${this.session_id}:${Date.now()}`),
552
+ input_type: "steer",
553
+ parts: from_ui_user_parts(message.parts),
507
554
  });
508
555
  }
509
556
  await this.touch_metadata();
510
557
  }
511
558
  }
559
+
560
+ function resolve_message_preview(message: SessionMessage | undefined): string {
561
+ if (!message) return "";
562
+ if (message.type === "user") {
563
+ return message.parts
564
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
565
+ .join("")
566
+ .trim();
567
+ }
568
+ if (message.type === "assistant") {
569
+ return message.parts
570
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
571
+ .join("")
572
+ .trim();
573
+ }
574
+ if (message.type === "action") {
575
+ return [message.title, message.description].filter(Boolean).join("\n");
576
+ }
577
+ return message.message.trim();
578
+ }
579
+
580
+ function infer_action_type(message_id: string): string {
581
+ return String(message_id || "").split(":")[0] || "action";
582
+ }
@@ -23,11 +23,15 @@ import type {
23
23
  SessionMessageRecordV1,
24
24
  SessionUserMessageV1,
25
25
  } from "@/executor/types/SessionRecords.js";
26
- import { mapAgentEventToSessionEvent, mapUiMessageChunkToAgentEvent } from "@/session/SessionEventMapper.js";
27
26
  import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
28
27
  import { SessionPromptRuntime } from "@/session/runtime/SessionPromptRuntime.js";
29
28
  import type { SessionRunContext } from "@/types/executor/SessionRunContext.js";
30
29
  import { SessionStateService } from "@/session/services/SessionStateService.js";
30
+ import {
31
+ SessionAssistantMessageWriter,
32
+ SessionRecorder,
33
+ } from "@/session/recorder/SessionRecorder.js";
34
+ import { from_ui_assistant_parts } from "@/session/recorder/SessionMessageCodec.js";
31
35
 
32
36
  type SessionTurnServiceOptions = {
33
37
  /**
@@ -54,6 +58,9 @@ type SessionTurnServiceOptions = {
54
58
  * 当前 session 共享事件总线。
55
59
  */
56
60
  event_hub: SessionEventHub;
61
+
62
+ /** 当前 Session Message Recorder。 */
63
+ recorder: SessionRecorder;
57
64
  };
58
65
 
59
66
  /**
@@ -65,6 +72,7 @@ export class SessionTurnService {
65
72
  private readonly executor: Executor;
66
73
  private readonly state_service: SessionStateService;
67
74
  private readonly event_hub: SessionEventHub;
75
+ private readonly recorder: SessionRecorder;
68
76
  private readonly prompt_runtime: SessionPromptRuntime;
69
77
 
70
78
  constructor(options: SessionTurnServiceOptions) {
@@ -73,23 +81,26 @@ export class SessionTurnService {
73
81
  this.executor = options.executor;
74
82
  this.state_service = options.state_service;
75
83
  this.event_hub = options.event_hub;
84
+ this.recorder = options.recorder;
76
85
  this.prompt_runtime = new SessionPromptRuntime({
77
86
  sessionId: this.session_id,
78
87
  publish: (event) => {
79
88
  this.publish_event(event);
80
89
  },
81
- createAndPersistUserMessage: async (input) => {
90
+ createAndPersistUserMessage: async (input, turn_id, input_type) => {
82
91
  return await this.state_service.create_and_persist_user_prompt_message(
83
92
  input,
93
+ turn_id,
94
+ input_type,
84
95
  );
85
96
  },
86
- emit_steer_action: async ({ turn_id, message }) => {
87
- await this.state_service.emit_action_event({
88
- id: `steer-message:${message.id}`,
89
- title: "Session steer message sent",
90
- description: "Merged a new user message into the active turn.",
91
- state: "completed",
92
- turnId: turn_id,
97
+ appendErrorMessage: async ({ turn_id, message }) => {
98
+ await this.recorder.append_error_message({
99
+ scope: "turn",
100
+ turn_id,
101
+ code: "turn_execution_failed",
102
+ message,
103
+ recoverable: true,
93
104
  });
94
105
  },
95
106
  executeTurn: async ({ turnId, promptInput, onStepMerge, abortSignal }) => {
@@ -157,64 +168,38 @@ export class SessionTurnService {
157
168
  assistantMessage?: SessionMessageRecordV1 | null;
158
169
  error?: string;
159
170
  }> {
160
- const tool_name_by_call_id = new Map<string, string>();
171
+ const assistant_writer_ref: {
172
+ current: SessionAssistantMessageWriter | null;
173
+ } = { current: null };
174
+ let assistant_segment_index = 0;
175
+ const ensure_assistant_writer = async (): Promise<SessionAssistantMessageWriter> => {
176
+ if (assistant_writer_ref.current) return assistant_writer_ref.current;
177
+ assistant_segment_index += 1;
178
+ assistant_writer_ref.current = await this.recorder.open_assistant_message({
179
+ turn_id: input.turnId,
180
+ segment_index: assistant_segment_index,
181
+ });
182
+ return assistant_writer_ref.current;
183
+ };
161
184
  const run_context: SessionRunContext = {
162
185
  turnId: input.turnId,
163
186
  sessionId: this.session_id,
164
187
  projectRoot: this.project_root,
165
- onStepCallback: input.onStepMerge,
166
- onAssistantStepCallback: async (step) => {
167
- this.publish_event({
168
- type: "assistant-step",
169
- turnId: input.turnId,
170
- text: step.text,
171
- stepIndex: step.stepIndex,
172
- ...(step.visibility ? { visibility: step.visibility } : {}),
173
- });
188
+ onStepCallback: async () => {
189
+ const merged = await input.onStepMerge();
190
+ if (merged.length > 0 && assistant_writer_ref.current) {
191
+ await assistant_writer_ref.current.complete();
192
+ assistant_writer_ref.current = null;
193
+ }
194
+ return merged;
174
195
  },
175
196
  onUiMessageChunkCallback: async (chunk) => {
176
- if (chunk.type === "tool-input-start") {
177
- tool_name_by_call_id.set(chunk.toolCallId, chunk.toolName);
178
- return;
179
- }
180
- const event = mapUiMessageChunkToAgentEvent(chunk);
181
- if (!event) return;
182
- const resolved_event =
183
- (
184
- event.type === "tool-result" ||
185
- event.type === "tool-error"
186
- ) &&
187
- event.toolName === "unknown"
188
- ? {
189
- ...event,
190
- toolName:
191
- tool_name_by_call_id.get(event.toolCallId) || event.toolName,
192
- }
193
- : event;
194
- if (
195
- resolved_event.type === "tool-call" ||
196
- resolved_event.type === "tool-error"
197
- ) {
198
- tool_name_by_call_id.set(
199
- resolved_event.toolCallId,
200
- resolved_event.toolName,
201
- );
202
- }
203
- const session_event = mapAgentEventToSessionEvent({
204
- event: resolved_event,
205
- turnId: input.turnId,
206
- });
207
- if (session_event) {
208
- this.publish_event(session_event);
209
- }
197
+ if (!is_assistant_content_chunk(chunk.type)) return;
198
+ const writer = await ensure_assistant_writer();
199
+ await writer.apply_chunk(chunk);
210
200
  },
211
201
  onActionCallback: async (event) => {
212
- try {
213
- await this.state_service.persist_action_event(event);
214
- } catch {
215
- // action 持久化失败不应阻断当前 turn。
216
- }
217
- this.publish_event(event);
202
+ await this.state_service.persist_action_event(event);
218
203
  },
219
204
  injectedUserMessages: [],
220
205
  deferredPersistedUserMessages: [],
@@ -227,6 +212,37 @@ export class SessionTurnService {
227
212
  query: executor_query,
228
213
  runContext: run_context,
229
214
  });
215
+ const final_assistant_writer = assistant_writer_ref.current;
216
+ if (final_assistant_writer) {
217
+ if (input.abortSignal?.aborted) {
218
+ await final_assistant_writer.stop();
219
+ } else if (result.success) {
220
+ await final_assistant_writer.complete();
221
+ } else {
222
+ await final_assistant_writer.fail(result.error);
223
+ }
224
+ } else if (result.assistantMessage) {
225
+ const parts = from_ui_assistant_parts(result.assistantMessage.parts);
226
+ if (parts.length > 0) {
227
+ assistant_segment_index += 1;
228
+ const fallback_writer = await this.recorder.open_assistant_message({
229
+ turn_id: input.turnId,
230
+ segment_index: assistant_segment_index,
231
+ });
232
+ for (const part of parts) await fallback_writer.upsert_part(part);
233
+ if (result.success) await fallback_writer.complete();
234
+ else await fallback_writer.fail(result.error);
235
+ }
236
+ }
237
+ if (!result.success && !input.abortSignal?.aborted && result.error) {
238
+ await this.recorder.append_error_message({
239
+ scope: "turn",
240
+ turn_id: input.turnId,
241
+ code: "turn_execution_failed",
242
+ message: result.error,
243
+ recoverable: true,
244
+ });
245
+ }
230
246
  await this.state_service.persist_assistant_result(result.assistantMessage);
231
247
  await this.state_service.persist_deferred_user_messages(
232
248
  result.deferredPersistedUserMessages,
@@ -243,3 +259,16 @@ export class SessionTurnService {
243
259
  };
244
260
  }
245
261
  }
262
+
263
+ function is_assistant_content_chunk(type: string): boolean {
264
+ return (
265
+ type === "text-start" ||
266
+ type === "text-delta" ||
267
+ type === "text-end" ||
268
+ type === "reasoning-start" ||
269
+ type === "reasoning-delta" ||
270
+ type === "reasoning-end" ||
271
+ type.startsWith("tool-") ||
272
+ type === "file"
273
+ );
274
+ }