@downcity/agent 1.1.199 → 1.1.200

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 (94) hide show
  1. package/bin/executor/Executor.d.ts +2 -2
  2. package/bin/executor/Executor.d.ts.map +1 -1
  3. package/bin/executor/Executor.js +23 -24
  4. package/bin/executor/Executor.js.map +1 -1
  5. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +5 -5
  6. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  7. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +1 -1
  8. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  9. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +2 -2
  10. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  11. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +23 -30
  12. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  13. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.d.ts.map +1 -1
  14. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js +4 -3
  15. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js.map +1 -1
  16. package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
  17. package/bin/executor/messages/SessionMessageCodec.d.ts +1 -1
  18. package/bin/executor/messages/SessionMessageCodec.js +3 -3
  19. package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
  20. package/bin/executor/messages/SessionStepEventMapper.d.ts +2 -2
  21. package/bin/executor/messages/SessionStepEventMapper.d.ts.map +1 -1
  22. package/bin/executor/messages/SessionStepEventMapper.js.map +1 -1
  23. package/bin/executor/services/ExecutorInflightService.d.ts +2 -2
  24. package/bin/executor/services/ExecutorInflightService.d.ts.map +1 -1
  25. package/bin/executor/services/ExecutorInflightService.js +9 -5
  26. package/bin/executor/services/ExecutorInflightService.js.map +1 -1
  27. package/bin/executor/store/history/SessionHistoryStore.d.ts +11 -11
  28. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  29. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +9 -7
  30. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  31. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +97 -52
  32. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  33. package/bin/executor/types/SessionMessages.d.ts +41 -22
  34. package/bin/executor/types/SessionMessages.d.ts.map +1 -1
  35. package/bin/executor/types/SessionMessages.js +25 -7
  36. package/bin/executor/types/SessionMessages.js.map +1 -1
  37. package/bin/index.d.ts +1 -1
  38. package/bin/index.d.ts.map +1 -1
  39. package/bin/index.js.map +1 -1
  40. package/bin/session/SessionTitle.d.ts.map +1 -1
  41. package/bin/session/SessionTitle.js +5 -0
  42. package/bin/session/SessionTitle.js.map +1 -1
  43. package/bin/session/browse/Browse.d.ts.map +1 -1
  44. package/bin/session/browse/Browse.js +35 -32
  45. package/bin/session/browse/Browse.js.map +1 -1
  46. package/bin/session/services/SessionStateService.d.ts +8 -8
  47. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  48. package/bin/session/services/SessionStateService.js +44 -54
  49. package/bin/session/services/SessionStateService.js.map +1 -1
  50. package/bin/session/services/SessionTurnService.js +3 -3
  51. package/bin/session/services/SessionTurnService.js.map +1 -1
  52. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  53. package/bin/session/services/SessionViewService.js +17 -27
  54. package/bin/session/services/SessionViewService.js.map +1 -1
  55. package/bin/types/agent/SessionTypes.d.ts +10 -10
  56. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  57. package/bin/types/executor/SessionRunContext.d.ts +6 -6
  58. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  59. package/bin/types/sdk/AgentSessionAction.d.ts +67 -0
  60. package/bin/types/sdk/AgentSessionAction.d.ts.map +1 -0
  61. package/bin/types/sdk/AgentSessionAction.js +10 -0
  62. package/bin/types/sdk/AgentSessionAction.js.map +1 -0
  63. package/bin/types/sdk/AgentSessionEvent.d.ts +2 -2
  64. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
  65. package/package.json +3 -3
  66. package/scripts/{session-operation-message.test.mjs → session-action-message.test.mjs} +57 -55
  67. package/src/executor/Executor.ts +28 -29
  68. package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -5
  69. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +1 -1
  70. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +32 -37
  71. package/src/executor/composer/history/jsonl/JsonlSessionHistoryComposer.ts +7 -4
  72. package/src/executor/messages/SessionAttachmentMapper.ts +2 -2
  73. package/src/executor/messages/SessionMessageCodec.ts +3 -3
  74. package/src/executor/messages/SessionStepEventMapper.ts +5 -5
  75. package/src/executor/services/ExecutorInflightService.ts +11 -7
  76. package/src/executor/store/history/SessionHistoryStore.ts +11 -10
  77. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +118 -63
  78. package/src/executor/types/SessionMessages.ts +69 -31
  79. package/src/index.ts +5 -6
  80. package/src/session/SessionTitle.ts +3 -0
  81. package/src/session/browse/Browse.ts +42 -34
  82. package/src/session/services/SessionStateService.ts +54 -64
  83. package/src/session/services/SessionTurnService.ts +3 -3
  84. package/src/session/services/SessionViewService.ts +17 -27
  85. package/src/types/agent/SessionTypes.ts +10 -10
  86. package/src/types/executor/SessionRunContext.ts +11 -11
  87. package/src/types/sdk/AgentSessionAction.ts +79 -0
  88. package/src/types/sdk/AgentSessionEvent.ts +2 -2
  89. package/tsconfig.tsbuildinfo +1 -1
  90. package/bin/types/sdk/AgentSessionOperation.d.ts +0 -99
  91. package/bin/types/sdk/AgentSessionOperation.d.ts.map +0 -1
  92. package/bin/types/sdk/AgentSessionOperation.js +0 -10
  93. package/bin/types/sdk/AgentSessionOperation.js.map +0 -1
  94. package/src/types/sdk/AgentSessionOperation.ts +0 -125
@@ -9,11 +9,12 @@
9
9
 
10
10
  import type { LanguageModel } from "ai";
11
11
  import type {
12
+ SessionActionMessageV1,
12
13
  SessionMessageV1,
13
14
  SessionMetadataV1,
14
15
  } from "@/executor/types/SessionMessages.js";
15
16
  import type { SessionSystemMessage } from "@/executor/types/SessionPrompts.js";
16
- import type { AgentSessionOperationRecord } from "@/types/sdk/AgentSessionOperation.js";
17
+ import type { AgentSessionActionRecord } from "@/types/sdk/AgentSessionAction.js";
17
18
 
18
19
  /**
19
20
  * compact 输入参数。
@@ -50,9 +51,9 @@ export type SessionHistoryCompactInput = {
50
51
  compactRatio: number;
51
52
 
52
53
  /**
53
- * 可选 operation 发布回调。
54
+ * 可选 action 发布回调。
54
55
  */
55
- onOperation?: (operation: AgentSessionOperationRecord) => Promise<void>;
56
+ onAction?: (action: AgentSessionActionRecord) => Promise<void>;
56
57
  };
57
58
 
58
59
  /**
@@ -165,23 +166,23 @@ export interface SessionHistoryStore {
165
166
  }): SessionMessageV1;
166
167
 
167
168
  /**
168
- * 构造 operation 消息。
169
+ * 构造 action 消息。
169
170
  */
170
- operation(input: {
171
+ action(input: {
171
172
  /**
172
- * 当前 operation 结构化记录。
173
+ * 当前 action 结构化记录。
173
174
  */
174
- operation: AgentSessionOperationRecord;
175
+ action: AgentSessionActionRecord;
175
176
 
176
177
  /**
177
- * 消息元信息(除 schema 字段、kind、source 与 operation 字段)。
178
+ * 消息元信息(除 schema 字段)。
178
179
  */
179
- metadata: Omit<SessionMetadataV1, "v" | "ts" | "kind" | "source" | "operation"> &
180
+ metadata: Pick<SessionMetadataV1, "sessionId"> &
180
181
  Partial<Pick<SessionMetadataV1, "ts">>;
181
182
 
182
183
  /**
183
184
  * 可选消息 ID(默认自动生成)。
184
185
  */
185
186
  id?: string;
186
- }): SessionMessageV1;
187
+ }): SessionActionMessageV1;
187
188
  }
@@ -18,10 +18,16 @@ import { generateId } from "@/utils/Id.js";
18
18
  import { getLogger } from "@/utils/logger/Logger.js";
19
19
  import { compactSessionMessagesIfNeeded } from "@executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js";
20
20
  import type {
21
+ SessionActionMessageV1,
21
22
  SessionMessageV1,
22
23
  SessionMetadataV1,
23
24
  } from "@/executor/types/SessionMessages.js";
24
- import type { AgentSessionOperationRecord } from "@/types/sdk/AgentSessionOperation.js";
25
+ import {
26
+ isSessionActionMessage,
27
+ isSessionModelMessage,
28
+ toSessionActionMessage,
29
+ } from "@/executor/types/SessionMessages.js";
30
+ import type { AgentSessionActionRecord } from "@/types/sdk/AgentSessionAction.js";
25
31
  import type { SessionSystemMessage } from "@/executor/types/SessionPrompts.js";
26
32
  import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
27
33
  import type { SessionHistoryPathOverrides } from "@/executor/types/SessionHistoryPaths.js";
@@ -216,17 +222,60 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
216
222
  input: unknown,
217
223
  ): SessionMessageV1 | null {
218
224
  if (!input || typeof input !== "object") return null;
219
- const candidate = input as Partial<SessionMessageV1>;
225
+ if ((input as { type?: unknown }).type === "action") {
226
+ return this.normalizeActionMessage(input);
227
+ }
228
+ const candidate = input as { role?: unknown; parts?: unknown };
220
229
  const role = String(candidate.role || "");
221
- if (role !== "user" && role !== "assistant" && role !== "operation") {
230
+ if (role !== "user" && role !== "assistant") {
222
231
  return null;
223
232
  }
224
233
  if (!Array.isArray(candidate.parts)) return null;
225
- return candidate as SessionMessageV1;
234
+ return input as SessionMessageV1;
235
+ }
236
+
237
+ private normalizeActionMessage(input: unknown): SessionActionMessageV1 | null {
238
+ if (!input || typeof input !== "object") return null;
239
+ const candidate = input as Partial<SessionActionMessageV1>;
240
+ const id = String(candidate.id || "").trim();
241
+ const title = String(candidate.title || "").trim();
242
+ const state = String(candidate.state || "").trim();
243
+ if (!id || !title) return null;
244
+ if (state !== "running" && state !== "completed" && state !== "failed") {
245
+ return null;
246
+ }
247
+ const metadata_input =
248
+ candidate.metadata && typeof candidate.metadata === "object"
249
+ ? candidate.metadata
250
+ : {};
251
+ const metadata_candidate = metadata_input as Partial<SessionActionMessageV1["metadata"]>;
252
+ const session_id =
253
+ String(metadata_candidate.sessionId || "").trim() || this.sessionId;
254
+ const description = String(candidate.description || "").trim();
255
+ return {
256
+ type: "action",
257
+ id,
258
+ title,
259
+ ...(description ? { description } : {}),
260
+ state,
261
+ metadata: {
262
+ v: 1,
263
+ ts:
264
+ typeof metadata_candidate.ts === "number"
265
+ ? metadata_candidate.ts
266
+ : Date.now(),
267
+ sessionId: session_id,
268
+ ...(metadata_candidate.turnId ? { turnId: metadata_candidate.turnId } : {}),
269
+ },
270
+ };
226
271
  }
227
272
 
228
273
  private hasStructuredAssistantParts(message: SessionMessageV1 | null): boolean {
229
- if (!message || message.role !== "assistant" || !Array.isArray(message.parts)) {
274
+ if (
275
+ !isSessionModelMessage(message) ||
276
+ message.role !== "assistant" ||
277
+ !Array.isArray(message.parts)
278
+ ) {
230
279
  return false;
231
280
  }
232
281
  return message.parts.some((part) => {
@@ -240,6 +289,8 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
240
289
  inflight: SessionMessageV1,
241
290
  finalMessage: SessionMessageV1,
242
291
  ): SessionMessageV1 {
292
+ if (!isSessionModelMessage(finalMessage)) return inflight;
293
+ if (!isSessionModelMessage(inflight)) return finalMessage;
243
294
  const final_parts = Array.isArray(finalMessage.parts)
244
295
  ? finalMessage.parts.filter((part) => part && typeof part === "object")
245
296
  : [];
@@ -357,7 +408,10 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
357
408
 
358
409
  private async writeInflightUnsafe(message: SessionMessageV1): Promise<void> {
359
410
  const normalized = this.normalizePersistedMessage(message);
360
- if (!normalized || normalized.role !== "assistant") {
411
+ if (
412
+ !isSessionModelMessage(normalized) ||
413
+ normalized.role !== "assistant"
414
+ ) {
361
415
  throw new Error("inflight assistant must be an assistant UIMessage");
362
416
  }
363
417
  const file = this.getInflightFilePath();
@@ -379,6 +433,44 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
379
433
  );
380
434
  }
381
435
 
436
+ private async rewriteMessagesUnsafe(messages: SessionMessageV1[]): Promise<void> {
437
+ const file = this.getMessagesFilePath();
438
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
439
+ const payload =
440
+ messages.length > 0
441
+ ? messages.map((message) => JSON.stringify(message)).join("\n") + "\n"
442
+ : "";
443
+ await fs.writeFile(temp, payload, "utf8");
444
+ await fs.move(temp, file, { overwrite: true });
445
+ }
446
+
447
+ private async upsertActionMessageUnsafe(
448
+ message: SessionActionMessageV1,
449
+ ): Promise<void> {
450
+ const file = this.getMessagesFilePath();
451
+ const raw = await fs.readFile(file, "utf8").catch(() => "");
452
+ const messages: SessionMessageV1[] = [];
453
+ let replaced = false;
454
+
455
+ for (const line of raw.split("\n").filter(Boolean)) {
456
+ try {
457
+ const current = this.normalizePersistedMessage(JSON.parse(line));
458
+ if (!current) continue;
459
+ if (isSessionActionMessage(current) && current.id === message.id) {
460
+ messages.push(message);
461
+ replaced = true;
462
+ continue;
463
+ }
464
+ messages.push(current);
465
+ } catch {
466
+ // 关键点(中文):重写 action 时顺手跳过损坏行,避免坏 JSON 阻断状态更新。
467
+ }
468
+ }
469
+
470
+ if (!replaced) messages.push(message);
471
+ await this.rewriteMessagesUnsafe(messages);
472
+ }
473
+
382
474
  private async withWriteLock<T>(fn: () => Promise<T>): Promise<T> {
383
475
  await this.ensureLayout();
384
476
  const lockPath = this.getLockFilePath();
@@ -438,44 +530,6 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
438
530
  return system.filter((item) => item && typeof item === "object");
439
531
  }
440
532
 
441
- private normalizeOperation(
442
- operation: AgentSessionOperationRecord,
443
- ): AgentSessionOperationRecord {
444
- const operation_id = String(operation.operationId || "").trim() || generateId();
445
- const name = String(operation.name || "").trim() || "operation";
446
- const status = String(operation.status || "").trim();
447
- const normalized_status: AgentSessionOperationRecord["status"] =
448
- status === "started" ||
449
- status === "progress" ||
450
- status === "finished" ||
451
- status === "skipped" ||
452
- status === "failed"
453
- ? status
454
- : "progress";
455
- return {
456
- operationId: operation_id,
457
- name,
458
- status: normalized_status,
459
- ...(typeof operation.turnId === "string" && operation.turnId.trim()
460
- ? { turnId: operation.turnId.trim() }
461
- : {}),
462
- ...(typeof operation.label === "string" && operation.label.trim()
463
- ? { label: operation.label.trim() }
464
- : {}),
465
- ...(typeof operation.reason === "string" && operation.reason.trim()
466
- ? { reason: operation.reason.trim() }
467
- : {}),
468
- ...(typeof operation.progress === "number" && Number.isFinite(operation.progress)
469
- ? { progress: Math.max(0, Math.min(1, operation.progress)) }
470
- : {}),
471
- ...(operation.result !== undefined ? { result: operation.result } : {}),
472
- ...(typeof operation.error === "string" && operation.error.trim()
473
- ? { error: operation.error.trim() }
474
- : {}),
475
- ...(operation.visible === false ? { visible: false } : {}),
476
- };
477
- }
478
-
479
533
  async compact(input: SessionHistoryCompactInput): Promise<{
480
534
  compacted: boolean;
481
535
  reason?: string;
@@ -512,7 +566,7 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
512
566
  maxInputTokensApprox: input.maxInputTokensApprox,
513
567
  archiveOnCompact: input.archiveOnCompact,
514
568
  compactRatio: input.compactRatio,
515
- ...(input.onOperation ? { onOperation: input.onOperation } : {}),
569
+ ...(input.onAction ? { onAction: input.onAction } : {}),
516
570
  },
517
571
  );
518
572
  }
@@ -521,10 +575,14 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
521
575
  await this.withWriteLock(async () => {
522
576
  const normalized = this.normalizePersistedMessage(message);
523
577
  if (!normalized) return;
578
+ if (isSessionActionMessage(normalized)) {
579
+ await this.upsertActionMessageUnsafe(normalized);
580
+ return;
581
+ }
524
582
 
525
583
  // 关键点(中文):若上一次 assistant 在运行中中断,新的 user 到来前先把残留快照收口到正式历史,
526
584
  // 避免 `list()` 时旧 inflight 跑到新 user 后面,打乱时序。
527
- if (normalized.role === "user") {
585
+ if (isSessionModelMessage(normalized) && normalized.role === "user") {
528
586
  const current_inflight = await this.readInflightUnsafe();
529
587
  if (current_inflight) {
530
588
  await this.appendMessageUnsafe(current_inflight);
@@ -676,29 +734,26 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
676
734
  };
677
735
  }
678
736
 
679
- operation(input: {
680
- operation: AgentSessionOperationRecord;
681
- metadata: Omit<SessionMetadataV1, "v" | "ts" | "kind" | "source" | "operation"> &
737
+ action(input: {
738
+ action: AgentSessionActionRecord;
739
+ metadata: Pick<SessionMetadataV1, "sessionId"> &
682
740
  Partial<Pick<SessionMetadataV1, "ts">>;
683
741
  id?: string;
684
- }): SessionMessageV1 {
685
- const operation = this.normalizeOperation(input.operation);
686
- const label = String(
687
- operation.label || `${operation.name}: ${operation.status}`,
688
- ).trim();
742
+ }): SessionActionMessageV1 {
689
743
  const { ts, ...metadata } = input.metadata;
744
+ const message = toSessionActionMessage(
745
+ {
746
+ ...input.action,
747
+ ...(input.id ? { id: input.id } : {}),
748
+ },
749
+ metadata.sessionId || this.sessionId,
750
+ );
690
751
  return {
691
- id: input.id || `op:${this.sessionId}:${operation.operationId}:${generateId()}`,
692
- role: "operation",
752
+ ...message,
693
753
  metadata: {
694
- v: 1,
695
- ts: typeof ts === "number" ? ts : Date.now(),
696
- ...metadata,
697
- source: "operation",
698
- kind: "operation",
699
- operation,
754
+ ...message.metadata,
755
+ ts: typeof ts === "number" ? ts : message.metadata.ts,
700
756
  },
701
- parts: [{ type: "text", text: label }],
702
757
  };
703
758
  }
704
759
  }
@@ -9,18 +9,21 @@
9
9
 
10
10
  import type { UIMessage } from "ai";
11
11
  import type { JsonObject } from "@/types/common/Json.js";
12
- import type { AgentSessionOperationRecord } from "@/types/sdk/AgentSessionOperation.js";
12
+ import type {
13
+ AgentSessionActionRecord,
14
+ AgentSessionActionState,
15
+ } from "@/types/sdk/AgentSessionAction.js";
13
16
 
14
17
  /**
15
18
  * Session 消息:以 UIMessage[] 作为唯一事实源。
16
19
  *
17
20
  * 关键点(中文)
18
21
  * - 持久化存储在 `.downcity/agents/<encodedAgentId>/sessions/<encodedSessionId>/messages/messages.jsonl`
19
- * - 默认只存 `role=user|assistant|operation`
22
+ * - 默认只存模型消息,或 `type=action` 的 UI 状态消息。
20
23
  * - compact 会把更早消息压缩为一条 `assistant` 摘要消息
21
- * - operation 只用于 UI timeline,不进入 LLM 输入
24
+ * - action 只用于 UI timeline,不进入 LLM 输入
22
25
  */
23
- export type SessionMessageKind = "normal" | "summary" | "operation";
26
+ export type SessionMessageKind = "normal" | "summary";
24
27
 
25
28
  /**
26
29
  * Session 消息来源类型。
@@ -33,8 +36,7 @@ export type SessionMessageKind = "normal" | "summary" | "operation";
33
36
  export type SessionMessageSource =
34
37
  | "ingress"
35
38
  | "egress"
36
- | "compact"
37
- | "operation";
39
+ | "compact";
38
40
 
39
41
  /**
40
42
  * 入站消息细分类型。
@@ -74,14 +76,6 @@ export type SessionMetadataV1 = {
74
76
  archiveId?: string;
75
77
  /** compact 摘要所覆盖的原始消息范围。 */
76
78
  sourceRange?: SessionMessageSourceRangeV1;
77
- /**
78
- * operation message 对应的结构化操作记录。
79
- *
80
- * 说明(中文)
81
- * - 仅当 `kind=operation` 或 `source=operation` 时存在。
82
- * - 该字段供前端恢复时间线状态,不会进入 LLM 输入。
83
- */
84
- operation?: AgentSessionOperationRecord;
85
79
  /**
86
80
  * 扩展元信息。
87
81
  *
@@ -98,43 +92,87 @@ export type SessionMetadataV1 = {
98
92
  export type SessionModelMessageV1 = UIMessage<SessionMetadataV1>;
99
93
 
100
94
  /**
101
- * operation 角色的 Session 消息结构。
95
+ * action 消息元信息。
96
+ */
97
+ export type SessionActionMetadataV1 = {
98
+ /** 元信息 schema 版本号。 */
99
+ v: 1;
100
+ /** 当前 action 写入时的毫秒时间戳。 */
101
+ ts: number;
102
+ /** 当前 action 所属的 session ID。 */
103
+ sessionId: string;
104
+ /** 当前 action 关联的 turn 标识。 */
105
+ turnId?: string;
106
+ };
107
+
108
+ /**
109
+ * action 类型的 Session 消息结构。
102
110
  *
103
111
  * 关键点(中文)
104
- * - `operation` 不是 AI SDK 原生模型 role。
112
+ * - `action` 不是 AI SDK 原生模型消息。
105
113
  * - 它只存在于 JSONL history 与前端 timeline,进入 LLM 前必须过滤。
106
114
  */
107
- export type SessionOperationMessageV1 = Omit<SessionModelMessageV1, "role"> & {
108
- /** 消息角色固定为 `operation`。 */
109
- role: "operation";
115
+ export type SessionActionMessageV1 = {
116
+ /** item 类型固定为 `action`。 */
117
+ type: "action";
118
+ /** 同一个 action 生命周期内稳定复用的 ID。 */
119
+ id: string;
120
+ /** 当前 action 标题。 */
121
+ title: string;
122
+ /** 当前 action 描述。 */
123
+ description?: string;
124
+ /** 当前 action 状态。 */
125
+ state: AgentSessionActionState;
126
+ /** action 元信息。 */
127
+ metadata: SessionActionMetadataV1;
110
128
  };
111
129
 
112
130
  /**
113
131
  * Session 持久化消息结构。
114
132
  */
115
- export type SessionMessageV1 = SessionModelMessageV1 | SessionOperationMessageV1;
133
+ export type SessionMessageV1 = SessionModelMessageV1 | SessionActionMessageV1;
116
134
 
117
135
  /**
118
136
  * user 角色的 Session 消息结构。
119
137
  */
120
- export type SessionUserMessageV1 = SessionMessageV1 & {
138
+ export type SessionUserMessageV1 = SessionModelMessageV1 & {
121
139
  /** 消息角色固定为 `user`。 */
122
140
  role: "user";
123
141
  };
124
142
 
125
143
  /**
126
- * 判断一条消息是否为 operation message。
144
+ * 判断一条消息是否为 action message。
127
145
  */
128
- export function isSessionOperationMessage(
146
+ export function isSessionActionMessage(
129
147
  message: SessionMessageV1 | null | undefined,
130
- ): boolean {
148
+ ): message is SessionActionMessageV1 {
131
149
  if (!message || typeof message !== "object") return false;
132
- const metadata = (message.metadata || null) as SessionMetadataV1 | null;
133
- return (
134
- message.role === "operation" ||
135
- metadata?.kind === "operation" ||
136
- metadata?.source === "operation"
137
- );
150
+ return (message as { type?: unknown }).type === "action";
151
+ }
152
+
153
+ /**
154
+ * 从订阅事件构造 action message。
155
+ */
156
+ export function toSessionActionMessage(
157
+ action: AgentSessionActionRecord,
158
+ session_id: string,
159
+ ): SessionActionMessageV1 {
160
+ const title = String(action.title || "").trim() || "Action";
161
+ const description = String(action.description || "").trim();
162
+ const id = String(action.id || "").trim() || `action:${session_id}:${Date.now()}`;
163
+ return {
164
+ type: "action",
165
+ id,
166
+ title,
167
+ ...(description ? { description } : {}),
168
+ state: action.state,
169
+ metadata: {
170
+ v: 1,
171
+ ts: Date.now(),
172
+ sessionId: session_id,
173
+ ...(action.turnId ? { turnId: action.turnId } : {}),
174
+ },
175
+ };
138
176
  }
139
177
 
140
178
  /**
@@ -144,5 +182,5 @@ export function isSessionModelMessage(
144
182
  message: SessionMessageV1 | null | undefined,
145
183
  ): message is SessionModelMessageV1 {
146
184
  if (!message || typeof message !== "object") return false;
147
- return message.role !== "operation" && !isSessionOperationMessage(message);
185
+ return !isSessionActionMessage(message);
148
186
  }
package/src/index.ts CHANGED
@@ -56,12 +56,11 @@ export type {
56
56
  AgentSessionUnsubscribe,
57
57
  } from "./types/sdk/AgentSessionEvent.js";
58
58
  export type {
59
- AgentSessionOperationCallback,
60
- AgentSessionOperationEvent,
61
- AgentSessionOperationName,
62
- AgentSessionOperationRecord,
63
- AgentSessionOperationStatus,
64
- } from "./types/sdk/AgentSessionOperation.js";
59
+ AgentSessionActionCallback,
60
+ AgentSessionActionEvent,
61
+ AgentSessionActionRecord,
62
+ AgentSessionActionState,
63
+ } from "./types/sdk/AgentSessionAction.js";
65
64
  export type { AgentSessionPromptInput, SessionUserMessagePart } from "./types/sdk/AgentSessionPrompt.js";
66
65
  export type { AgentSessionStopResult } from "./types/sdk/AgentSessionStop.js";
67
66
  export type {
@@ -10,6 +10,7 @@
10
10
  import { generateText, type LanguageModel } from "ai";
11
11
  import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
12
12
  import type { SessionMessageV1 } from "@/executor/types/SessionMessages.js";
13
+ import { isSessionModelMessage } from "@/executor/types/SessionMessages.js";
13
14
  import {
14
15
  normalizeSessionTitle,
15
16
  readSessionMetadata,
@@ -61,6 +62,7 @@ function truncateTitle(input: string, maxChars: number): string {
61
62
  }
62
63
 
63
64
  function extractTextFromMessage(message: SessionMessageV1): string {
65
+ if (!isSessionModelMessage(message)) return "";
64
66
  if (!Array.isArray(message.parts)) return "";
65
67
  const texts: string[] = [];
66
68
  for (const part of message.parts) {
@@ -76,6 +78,7 @@ function extractTextFromMessage(message: SessionMessageV1): string {
76
78
 
77
79
  function resolveFirstUserText(messages: SessionMessageV1[]): string {
78
80
  for (const message of messages) {
81
+ if (!isSessionModelMessage(message)) continue;
79
82
  if (message.role !== "user") continue;
80
83
  const text = extractTextFromMessage(message);
81
84
  if (text) return text;
@@ -26,10 +26,15 @@ import type {
26
26
  AgentSessionTimelineEvent,
27
27
  } from "@/types/agent/AgentTypes.js";
28
28
  import type {
29
+ SessionActionMessageV1,
29
30
  SessionMessageV1,
30
31
  SessionMetadataV1,
32
+ SessionModelMessageV1,
33
+ } from "@/executor/types/SessionMessages.js";
34
+ import {
35
+ isSessionActionMessage,
36
+ isSessionModelMessage,
31
37
  } from "@/executor/types/SessionMessages.js";
32
- import { isSessionOperationMessage } from "@/executor/types/SessionMessages.js";
33
38
  import type { SessionHistoryMetaV1 } from "@/executor/types/SessionHistoryMeta.js";
34
39
  import { pickLastSuccessfulChatSendText } from "@/executor/messages/UserVisibleText.js";
35
40
  import { getSdkAgentSessionMessagesPath } from "@/session/storage/Paths.js";
@@ -172,7 +177,7 @@ function extractMessageText(parts: unknown): string {
172
177
  return texts.join("\n").trim();
173
178
  }
174
179
 
175
- function extractAssistantToolSummary(message: SessionMessageV1): string {
180
+ function extractAssistantToolSummary(message: SessionModelMessageV1): string {
176
181
  if (!Array.isArray(message.parts)) return "";
177
182
  const toolNames = new Set<string>();
178
183
  for (const part of message.parts as AnyUiPart[]) {
@@ -189,6 +194,12 @@ function extractAssistantToolSummary(message: SessionMessageV1): string {
189
194
  * 解析单条 session 消息的用户可见预览文本。
190
195
  */
191
196
  export function resolveSessionMessagePreview(message: SessionMessageV1): string {
197
+ if (isSessionActionMessage(message)) {
198
+ return message.description
199
+ ? `${message.title}\n${message.description}`
200
+ : message.title;
201
+ }
202
+ if (!isSessionModelMessage(message)) return "";
192
203
  const plainText = extractMessageText(message.parts);
193
204
  if (plainText) return plainText;
194
205
  if (message.role !== "assistant") return "";
@@ -226,7 +237,7 @@ function extractToolResultOutput(part: ToolPartCompatShape): unknown {
226
237
  }
227
238
 
228
239
  function toTimelineEvent(params: {
229
- message: SessionMessageV1;
240
+ message: SessionModelMessageV1;
230
241
  role: AgentSessionTimelineEvent["role"];
231
242
  text: string;
232
243
  sequence: number;
@@ -241,15 +252,21 @@ function toTimelineEvent(params: {
241
252
  ...(typeof metadata?.source === "string" ? { source: metadata.source } : {}),
242
253
  text: params.text,
243
254
  ...(params.toolName ? { toolName: params.toolName } : {}),
244
- ...(params.role === "operation" && metadata?.operation?.operationId
245
- ? { operationId: metadata.operation.operationId }
246
- : {}),
247
- ...(params.role === "operation" && metadata?.operation?.name
248
- ? { operationName: metadata.operation.name }
249
- : {}),
250
- ...(params.role === "operation" && metadata?.operation?.status
251
- ? { operationStatus: metadata.operation.status }
252
- : {}),
255
+ };
256
+ }
257
+
258
+ function toActionTimelineEvent(
259
+ message: SessionActionMessageV1,
260
+ ): AgentSessionTimelineEvent {
261
+ const metadata = message.metadata || null;
262
+ return {
263
+ id: `${String(message.id || "")}:0`,
264
+ role: "action",
265
+ ...(typeof metadata?.ts === "number" ? { ts: metadata.ts } : {}),
266
+ text: resolveSessionMessagePreview(message),
267
+ actionTitle: message.title,
268
+ ...(message.description ? { actionDescription: message.description } : {}),
269
+ actionState: message.state,
253
270
  };
254
271
  }
255
272
 
@@ -259,21 +276,11 @@ function toTimelineEvent(params: {
259
276
  export function toSessionTimelineEvents(
260
277
  message: SessionMessageV1,
261
278
  ): AgentSessionTimelineEvent[] {
262
- if (isSessionOperationMessage(message)) {
263
- const metadata = (message.metadata || null) as SessionMetadataV1 | null;
264
- if (metadata?.operation?.status !== "finished") return [];
265
- return [
266
- toTimelineEvent({
267
- message,
268
- role: "operation",
269
- text:
270
- String(metadata?.operation?.label || "").trim() ||
271
- resolveSessionMessagePreview(message),
272
- sequence: 0,
273
- }),
274
- ];
279
+ if (isSessionActionMessage(message)) {
280
+ return [toActionTimelineEvent(message)];
275
281
  }
276
282
 
283
+ if (!isSessionModelMessage(message)) return [];
277
284
  if (message.role !== "assistant") {
278
285
  return [
279
286
  toTimelineEvent({
@@ -368,10 +375,11 @@ export async function loadSessionMessagesFromPath(
368
375
  try {
369
376
  const parsed = JSON.parse(line) as SessionMessageV1;
370
377
  if (!parsed || typeof parsed !== "object") continue;
378
+ const candidate = parsed as { type?: unknown; role?: unknown };
371
379
  if (
372
- parsed.role !== "user" &&
373
- parsed.role !== "assistant" &&
374
- parsed.role !== "operation"
380
+ candidate.type !== "action" &&
381
+ candidate.role !== "user" &&
382
+ candidate.role !== "assistant"
375
383
  ) {
376
384
  continue;
377
385
  }
@@ -387,8 +395,7 @@ export async function loadSessionMessagesFromPath(
387
395
  try {
388
396
  const parsed = (await fs.readJson(inflight_path)) as SessionMessageV1;
389
397
  if (
390
- parsed &&
391
- typeof parsed === "object" &&
398
+ isSessionModelMessage(parsed) &&
392
399
  parsed.role === "assistant" &&
393
400
  Array.isArray(parsed.parts)
394
401
  ) {
@@ -417,14 +424,14 @@ export async function loadSessionArchiveMessagesFromPath(
417
424
  const items = Array.isArray(raw?.messages) ? raw.messages : [];
418
425
  return items.filter((item): item is SessionMessageV1 => {
419
426
  if (!item || typeof item !== "object") return false;
420
- const candidate = item as Partial<SessionMessageV1>;
427
+ const candidate = item as { type?: unknown; role?: unknown; parts?: unknown };
421
428
  return (
422
429
  (
430
+ candidate.type === "action" ||
423
431
  candidate.role === "user" ||
424
- candidate.role === "assistant" ||
425
- candidate.role === "operation"
432
+ candidate.role === "assistant"
426
433
  ) &&
427
- Array.isArray(candidate.parts)
434
+ (candidate.type === "action" || Array.isArray(candidate.parts))
428
435
  );
429
436
  });
430
437
  } catch {
@@ -433,6 +440,7 @@ export async function loadSessionArchiveMessagesFromPath(
433
440
  }
434
441
 
435
442
  function isCompactSummaryMessage(message: SessionMessageV1): boolean {
443
+ if (!isSessionModelMessage(message)) return false;
436
444
  const metadata = (message.metadata || null) as SessionMetadataV1 | null;
437
445
  return metadata?.source === "compact" || metadata?.kind === "summary";
438
446
  }