@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
@@ -0,0 +1,187 @@
1
+ /**
2
+ * SessionRecorder canonical 上下文压缩。
3
+ *
4
+ * 压缩只追加一条 internal summary Message,不删除、不归档、不重写 Mutation 日志。
5
+ */
6
+
7
+ import { generateText } from "ai";
8
+ import { generateId } from "@/utils/Id.js";
9
+ import type { SessionHistoryCompactInput } from "@/executor/store/history/SessionHistoryStore.js";
10
+ import type { SessionActionRecordV1 } from "@/executor/types/SessionRecords.js";
11
+ import {
12
+ build_initial_session_compaction_prompt,
13
+ build_update_session_compaction_prompt,
14
+ SESSION_COMPACTION_SYSTEM_PROMPT,
15
+ } from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
16
+ import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
17
+ import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
18
+ import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
19
+
20
+ /** 必要时把较早上下文追加为 canonical internal summary。 */
21
+ export async function compact_session_recorder_messages(input: {
22
+ /** 当前 Session Recorder。 */
23
+ recorder: SessionRecorder;
24
+ /** 压缩策略和模型。 */
25
+ compact_input: SessionHistoryCompactInput;
26
+ }): Promise<{ compacted: boolean; reason?: string }> {
27
+ const page = await input.recorder.list_messages({
28
+ limit: 500,
29
+ include_internal: true,
30
+ });
31
+ const context_messages = project_current_context(page.items);
32
+ if (context_messages.length < 4) {
33
+ return { compacted: false, reason: "small_messages" };
34
+ }
35
+ const system_text = input.compact_input.system
36
+ .map((message) => String(message.content || ""))
37
+ .join("\n\n");
38
+ const estimated_tokens = Math.ceil(
39
+ `${system_text}\n${JSON.stringify(context_messages)}`.length / 3,
40
+ );
41
+ if (estimated_tokens <= input.compact_input.maxInputTokensApprox) {
42
+ return { compacted: false, reason: "under_budget" };
43
+ }
44
+
45
+ const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
46
+ const compact_count = Math.max(
47
+ 2,
48
+ Math.min(context_messages.length - 1, Math.floor(context_messages.length * ratio)),
49
+ );
50
+ const older = context_messages.slice(0, compact_count);
51
+ const boundary = [...older]
52
+ .reverse()
53
+ .find((message) => message.type !== "assistant" || message.kind !== "summary");
54
+ if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
55
+
56
+ const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
57
+ await publish_action(input.compact_input.onAction, {
58
+ id: action_id,
59
+ session_id: input.recorder.session_id,
60
+ title: "Compacting session messages",
61
+ state: "running",
62
+ });
63
+
64
+ const previous_summary = older.find(
65
+ (message): message is SessionAssistantMessage =>
66
+ message.type === "assistant" && message.kind === "summary",
67
+ );
68
+ const conversation_text = older
69
+ .filter((message) => message !== previous_summary)
70
+ .map((message) => message_to_compaction_text(message))
71
+ .filter(Boolean)
72
+ .join("\n")
73
+ .slice(-24_000);
74
+ const previous_summary_text = previous_summary
75
+ ? assistant_text(previous_summary)
76
+ : "";
77
+ const prompt = previous_summary_text
78
+ ? build_update_session_compaction_prompt({
79
+ previous_summary: previous_summary_text,
80
+ new_conversation_text: conversation_text || "(none)",
81
+ })
82
+ : build_initial_session_compaction_prompt({
83
+ conversation_text: conversation_text || "(none)",
84
+ });
85
+
86
+ let summary = "";
87
+ try {
88
+ const result = await generateText({
89
+ model: input.compact_input.model,
90
+ system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
91
+ prompt,
92
+ });
93
+ summary = String(result.text || "").trim();
94
+ } catch {
95
+ summary = "Earlier conversation was compacted because the context window was exceeded.";
96
+ }
97
+
98
+ await input.recorder.append_completed_assistant_message({
99
+ turn_id: `compact:${input.recorder.session_id}:${generateId()}`,
100
+ kind: "summary",
101
+ visibility: "internal",
102
+ summary_through_message_id: boundary.message_id,
103
+ parts: [{
104
+ part_id: `summary-text:${generateId()}`,
105
+ type: "text",
106
+ text: summary,
107
+ state: "done",
108
+ }],
109
+ });
110
+ await publish_action(input.compact_input.onAction, {
111
+ id: action_id,
112
+ session_id: input.recorder.session_id,
113
+ title: "Session messages compacted",
114
+ description: `Compacted through Message ${boundary.message_id}.`,
115
+ state: "completed",
116
+ });
117
+ return { compacted: true };
118
+ }
119
+
120
+ function project_current_context(messages: SessionMessage[]): SessionMessage[] {
121
+ const latest_summary = [...messages].reverse().find(
122
+ (message): message is SessionAssistantMessage =>
123
+ message.type === "assistant" &&
124
+ message.kind === "summary" &&
125
+ message.visibility === "internal",
126
+ );
127
+ if (!latest_summary?.summary_through_message_id) {
128
+ return messages.filter(
129
+ (message) => message.type === "user" || message.type === "assistant",
130
+ );
131
+ }
132
+ const boundary_index = messages.findIndex(
133
+ (message) => message.message_id === latest_summary.summary_through_message_id,
134
+ );
135
+ return [
136
+ latest_summary,
137
+ ...messages.slice(boundary_index + 1).filter(
138
+ (message) =>
139
+ message !== latest_summary &&
140
+ (message.type === "user" ||
141
+ message.type === "assistant" && message.kind !== "summary"),
142
+ ),
143
+ ];
144
+ }
145
+
146
+ function message_to_compaction_text(message: SessionMessage): string {
147
+ const projected = to_executor_ui_message(message);
148
+ if (!projected) return "";
149
+ const text = projected.parts
150
+ .flatMap((part) => part.type === "text" ? [String(part.text || "")] : [])
151
+ .join("\n")
152
+ .trim();
153
+ return text ? `${message.type}: ${text}` : "";
154
+ }
155
+
156
+ function assistant_text(message: SessionAssistantMessage): string {
157
+ return message.parts
158
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
159
+ .join("\n")
160
+ .trim();
161
+ }
162
+
163
+ async function publish_action(
164
+ callback: SessionHistoryCompactInput["onAction"],
165
+ input: {
166
+ id: string;
167
+ session_id: string;
168
+ title: string;
169
+ description?: string;
170
+ state: "running" | "completed";
171
+ },
172
+ ): Promise<void> {
173
+ if (!callback) return;
174
+ const action: SessionActionRecordV1 = {
175
+ type: "action",
176
+ id: input.id,
177
+ title: input.title,
178
+ ...(input.description ? { description: input.description } : {}),
179
+ state: input.state,
180
+ metadata: {
181
+ v: 1,
182
+ ts: Date.now(),
183
+ sessionId: input.session_id,
184
+ },
185
+ };
186
+ await callback(action);
187
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
3
+ *
4
+ * 关键点(中文)
5
+ * - Recorder messages.jsonl 是唯一持久化事实源。
6
+ * - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
7
+ * - inflight 写入已经由 AssistantMessageWriter 取代,因此对应接口只提供投影兼容。
8
+ */
9
+
10
+ import { nanoid } from "nanoid";
11
+ import type {
12
+ SessionActionRecordV1,
13
+ SessionMetadataV1,
14
+ SessionRecordV1,
15
+ } from "@/executor/types/SessionRecords.js";
16
+ import { is_session_action_record } from "@/executor/types/SessionRecords.js";
17
+ import type {
18
+ SessionHistoryCompactInput,
19
+ SessionHistoryStore,
20
+ } from "@/executor/store/history/SessionHistoryStore.js";
21
+ import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
22
+ import {
23
+ from_ui_assistant_parts,
24
+ from_ui_user_parts,
25
+ to_executor_ui_message,
26
+ } from "@/session/recorder/SessionMessageCodec.js";
27
+ import { compact_session_recorder_messages } from "@/session/recorder/SessionRecorderCompaction.js";
28
+
29
+ /** Recorder History Store 构造参数。 */
30
+ export interface SessionRecorderHistoryStoreOptions {
31
+ /** 当前 Session 标识。 */
32
+ session_id: string;
33
+ /** 当前 Session Recorder。 */
34
+ recorder: SessionRecorder;
35
+ }
36
+
37
+ /** 只把 canonical SessionMessage 投影给 Executor 的 Store。 */
38
+ export class SessionRecorderHistoryStore implements SessionHistoryStore {
39
+ readonly sessionId: string;
40
+
41
+ private readonly recorder: SessionRecorder;
42
+
43
+ constructor(options: SessionRecorderHistoryStoreOptions) {
44
+ this.sessionId = String(options.session_id || "").trim();
45
+ this.recorder = options.recorder;
46
+ if (!this.sessionId) {
47
+ throw new Error("SessionRecorderHistoryStore requires session_id");
48
+ }
49
+ }
50
+
51
+ /** 把外部 user/assistant/action 写入请求收口到 Recorder。 */
52
+ async write_record(message: SessionRecordV1): Promise<void> {
53
+ if (is_session_action_record(message)) {
54
+ const action = message as SessionActionRecordV1;
55
+ const existing = this.recorder.get_message(action.id);
56
+ if (existing?.type === "action") {
57
+ if (action.state !== "running") {
58
+ await this.recorder.update_action_message(
59
+ action.id,
60
+ action.state,
61
+ {
62
+ title: action.title,
63
+ description: action.description,
64
+ },
65
+ );
66
+ }
67
+ return;
68
+ }
69
+ const writer = await this.recorder.open_action_message({
70
+ message_id: action.id,
71
+ turn_id: action.metadata.turnId,
72
+ action_type: infer_action_type(action.id),
73
+ title: action.title,
74
+ description: action.description,
75
+ });
76
+ if (action.state === "completed") await writer.complete();
77
+ if (action.state === "failed") await writer.fail(action.description || action.title);
78
+ return;
79
+ }
80
+
81
+ if (message.role === "user") {
82
+ const metadata = message.metadata || {
83
+ v: 1 as const,
84
+ ts: Date.now(),
85
+ sessionId: this.sessionId,
86
+ };
87
+ await this.recorder.append_user_message({
88
+ turn_id: String(metadata.turnId || `external:${nanoid(10)}`),
89
+ input_type:
90
+ metadata.extra?.inputType === "steer" ? "steer" : "prompt",
91
+ parts: from_ui_user_parts(message.parts),
92
+ });
93
+ return;
94
+ }
95
+
96
+ const metadata = message.metadata || {
97
+ v: 1 as const,
98
+ ts: Date.now(),
99
+ sessionId: this.sessionId,
100
+ };
101
+ await this.recorder.append_completed_assistant_message({
102
+ turn_id: metadata.turnId,
103
+ parts: from_ui_assistant_parts(message.parts),
104
+ visibility:
105
+ metadata.extra?.visibility === "internal"
106
+ ? "internal"
107
+ : "visible",
108
+ kind: metadata.kind === "summary" ? "summary" : "normal",
109
+ });
110
+ }
111
+
112
+ /** Recorder 已经逐 delta 保存 assistant,不再维护第二份 inflight。 */
113
+ async read_inflight(): Promise<SessionRecordV1 | null> {
114
+ const page = await this.recorder.list_messages({
115
+ limit: 500,
116
+ include_internal: true,
117
+ });
118
+ const assistant = [...page.items]
119
+ .reverse()
120
+ .find(
121
+ (message) =>
122
+ message.type === "assistant" && message.status === "streaming",
123
+ );
124
+ return assistant ? to_executor_ui_message(assistant) : null;
125
+ }
126
+
127
+ /** 旧 inflight 写入口已废弃;流式内容由 Recorder writer 负责。 */
128
+ async write_inflight(_message: SessionRecordV1): Promise<void> {}
129
+
130
+ /** 旧 inflight 收口入口已废弃;Assistant writer 负责 complete。 */
131
+ async finalize_inflight(_message?: SessionRecordV1 | null): Promise<void> {}
132
+
133
+ /** 返回 Context Composer 使用的 UIMessage 投影。 */
134
+ async list_records(): Promise<SessionRecordV1[]> {
135
+ const page = await this.recorder.list_messages({
136
+ limit: 500,
137
+ include_internal: true,
138
+ });
139
+ const messages = project_context_messages(page.items);
140
+ return messages.flatMap((message) => {
141
+ const projected = to_executor_ui_message(message);
142
+ return projected ? [projected] : [];
143
+ });
144
+ }
145
+
146
+ /** 返回 Context Message 区间。 */
147
+ async slice_records(start: number, end: number): Promise<SessionRecordV1[]> {
148
+ return (await this.list_records()).slice(start, end);
149
+ }
150
+
151
+ /** 返回 Context Message 数量。 */
152
+ async record_count(): Promise<number> {
153
+ return (await this.list_records()).length;
154
+ }
155
+
156
+ /** Recorder metadata 由 SessionStateService 管理。 */
157
+ async meta(): Promise<Record<string, unknown>> {
158
+ return {};
159
+ }
160
+
161
+ /** 通过 Recorder 追加 internal summary,不重写 Mutation 历史。 */
162
+ async compact(input: SessionHistoryCompactInput): Promise<{
163
+ compacted: boolean;
164
+ reason?: string;
165
+ }> {
166
+ return await compact_session_recorder_messages({
167
+ recorder: this.recorder,
168
+ compact_input: input,
169
+ });
170
+ }
171
+
172
+ /** 构造临时 user UIMessage。 */
173
+ userText(input: {
174
+ text: string;
175
+ metadata: Omit<SessionMetadataV1, "v" | "ts"> &
176
+ Partial<Pick<SessionMetadataV1, "ts">>;
177
+ id?: string;
178
+ }): SessionRecordV1 {
179
+ const { ts, ...metadata } = input.metadata;
180
+ return {
181
+ id: input.id || `u:${this.sessionId}:${nanoid(10)}`,
182
+ role: "user",
183
+ metadata: {
184
+ v: 1,
185
+ ts: typeof ts === "number" ? ts : Date.now(),
186
+ ...metadata,
187
+ source: "ingress",
188
+ kind: "normal",
189
+ },
190
+ parts: [{ type: "text", text: String(input.text || "") }],
191
+ };
192
+ }
193
+
194
+ /** 构造临时 assistant UIMessage。 */
195
+ assistantText(input: {
196
+ text: string;
197
+ metadata: Omit<SessionMetadataV1, "v" | "ts"> &
198
+ Partial<Pick<SessionMetadataV1, "ts">>;
199
+ id?: string;
200
+ kind?: "normal" | "summary";
201
+ source?: "egress" | "compact";
202
+ }): SessionRecordV1 {
203
+ const { ts, ...metadata } = input.metadata;
204
+ return {
205
+ id: input.id || `a:${this.sessionId}:${nanoid(10)}`,
206
+ role: "assistant",
207
+ metadata: {
208
+ v: 1,
209
+ ts: typeof ts === "number" ? ts : Date.now(),
210
+ ...metadata,
211
+ source: input.source || "egress",
212
+ kind: input.kind || "normal",
213
+ },
214
+ parts: [{ type: "text", text: String(input.text || "") }],
215
+ };
216
+ }
217
+
218
+ /** 返回 action 原结构,真正持久化仍经过 write_record。 */
219
+ action(input: {
220
+ action: SessionActionRecordV1;
221
+ metadata: Pick<SessionMetadataV1, "sessionId"> &
222
+ Partial<Pick<SessionMetadataV1, "ts">>;
223
+ id?: string;
224
+ }): SessionActionRecordV1 {
225
+ return {
226
+ ...input.action,
227
+ id: input.id || input.action.id,
228
+ metadata: {
229
+ ...input.action.metadata,
230
+ sessionId: input.metadata.sessionId,
231
+ ts:
232
+ typeof input.metadata.ts === "number"
233
+ ? input.metadata.ts
234
+ : input.action.metadata.ts,
235
+ },
236
+ };
237
+ }
238
+ }
239
+
240
+ function project_context_messages(messages: import("@/types/session/SessionMessage.js").SessionMessage[]) {
241
+ const latest_summary = [...messages]
242
+ .reverse()
243
+ .find(
244
+ (message) =>
245
+ message.type === "assistant" &&
246
+ message.kind === "summary" &&
247
+ message.visibility === "internal",
248
+ );
249
+ if (
250
+ !latest_summary ||
251
+ latest_summary.type !== "assistant" ||
252
+ !latest_summary.summary_through_message_id
253
+ ) {
254
+ return messages.filter(
255
+ (message) => message.type === "user" || message.type === "assistant",
256
+ );
257
+ }
258
+ const boundary_index = messages.findIndex(
259
+ (message) => message.message_id === latest_summary.summary_through_message_id,
260
+ );
261
+ return [
262
+ latest_summary,
263
+ ...messages
264
+ .slice(boundary_index + 1)
265
+ .filter(
266
+ (message) =>
267
+ message !== latest_summary &&
268
+ message.type === "user" ||
269
+ (message.type === "assistant" && message.kind !== "summary"),
270
+ ),
271
+ ];
272
+ }
273
+
274
+ function infer_action_type(message_id: string): string {
275
+ return String(message_id || "").split(":")[0] || "action";
276
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * SessionMessage mutation 事件总线。
3
+ *
4
+ * EventHub 只广播 Recorder 已成功提交的 mutation,不创建或转换事件。
5
+ */
6
+
7
+ import type {
8
+ SessionMessageMutation,
9
+ SessionMessageMutationSubscriber,
10
+ SessionMessageMutationUnsubscribe,
11
+ } from "@/types/session/SessionMessageMutation.js";
12
+
13
+ /** SessionMessage mutation 的进程内广播器。 */
14
+ export class SessionMessageEventHub {
15
+ private readonly subscribers = new Set<SessionMessageMutationSubscriber>();
16
+
17
+ /** 订阅未来成功提交的 mutation。 */
18
+ subscribe(subscriber: SessionMessageMutationSubscriber): SessionMessageMutationUnsubscribe {
19
+ this.subscribers.add(subscriber);
20
+ return () => {
21
+ this.subscribers.delete(subscriber);
22
+ };
23
+ }
24
+
25
+ /** 广播一条已持久化 mutation。 */
26
+ publish(mutation: SessionMessageMutation): void {
27
+ for (const subscriber of this.subscribers) {
28
+ try {
29
+ subscriber(mutation);
30
+ } catch {
31
+ // 单个订阅者失败不能影响提交结果和其他订阅者。
32
+ }
33
+ }
34
+ }
35
+ }
@@ -96,21 +96,16 @@ export interface SessionPromptRuntimeOptions {
96
96
  */
97
97
  createAndPersistUserMessage: (
98
98
  input: AgentSessionPromptInput,
99
+ turn_id: string,
100
+ input_type: "prompt" | "steer",
99
101
  ) => Promise<SessionUserMessageV1>;
100
102
 
101
- /**
102
- * steer message 成功持久化后发送对应 action。
103
- */
104
- emit_steer_action: (input: {
105
- /**
106
- * steer message 所属的当前 turn 标识。
107
- */
103
+ /** 持久化一条用户可见的 turn 错误 Message。 */
104
+ appendErrorMessage: (input: {
105
+ /** 错误所属 turn。 */
108
106
  turn_id: string;
109
-
110
- /**
111
- * 已成功持久化的 steer message。
112
- */
113
- message: SessionUserMessageV1;
107
+ /** 用户可见错误文本。 */
108
+ message: string;
114
109
  }) => Promise<void>;
115
110
 
116
111
  /**
@@ -141,7 +136,7 @@ export class SessionPromptRuntime {
141
136
  private readonly sessionId: string;
142
137
  private readonly publish: SessionPromptRuntimeOptions["publish"];
143
138
  private readonly createAndPersistUserMessage: SessionPromptRuntimeOptions["createAndPersistUserMessage"];
144
- private readonly emit_steer_action: SessionPromptRuntimeOptions["emit_steer_action"];
139
+ private readonly appendErrorMessage: SessionPromptRuntimeOptions["appendErrorMessage"];
145
140
  private readonly executeTurn: SessionPromptRuntimeOptions["executeTurn"];
146
141
  private readonly stopTurn: SessionPromptRuntimeOptions["stopTurn"];
147
142
  private readonly queue: QueuedPrompt[] = [];
@@ -152,7 +147,7 @@ export class SessionPromptRuntime {
152
147
  this.sessionId = String(options.sessionId || "").trim();
153
148
  this.publish = options.publish;
154
149
  this.createAndPersistUserMessage = options.createAndPersistUserMessage;
155
- this.emit_steer_action = options.emit_steer_action;
150
+ this.appendErrorMessage = options.appendErrorMessage;
156
151
  this.executeTurn = options.executeTurn;
157
152
  this.stopTurn = options.stopTurn;
158
153
  if (!this.sessionId) {
@@ -237,7 +232,7 @@ export class SessionPromptRuntime {
237
232
  current.deferredHandle.resolve(createTurnHandle(activeTurn));
238
233
 
239
234
  try {
240
- await this.createAndPersistUserMessage(current.input);
235
+ await this.createAndPersistUserMessage(current.input, turnId, "prompt");
241
236
  const result = await this.executeTurn({
242
237
  turnId,
243
238
  promptInput: current.input,
@@ -283,6 +278,14 @@ export class SessionPromptRuntime {
283
278
  };
284
279
  activeTurn.result = finalResult;
285
280
  if (message !== TURN_STOPPED_MESSAGE) {
281
+ try {
282
+ await this.appendErrorMessage({
283
+ turn_id: turnId,
284
+ message,
285
+ });
286
+ } catch {
287
+ // Error Message 写入失败不能阻止 turn handle 收口。
288
+ }
286
289
  this.publish({
287
290
  type: "error",
288
291
  message,
@@ -348,17 +351,13 @@ export class SessionPromptRuntime {
348
351
  for (let index = 0; index < drained.length; index += 1) {
349
352
  const item = drained[index];
350
353
  try {
351
- const message = await this.createAndPersistUserMessage(item.input);
354
+ const message = await this.createAndPersistUserMessage(
355
+ item.input,
356
+ activeTurn.turnId,
357
+ "steer",
358
+ );
352
359
  item.deferredHandle.resolve(createTurnHandle(activeTurn));
353
360
  merged.push(message);
354
- try {
355
- await this.emit_steer_action({
356
- turn_id: activeTurn.turnId,
357
- message,
358
- });
359
- } catch {
360
- // action 发送失败不应阻断已经持久化的 steer message。
361
- }
362
361
  } catch {
363
362
  // 关键点(中文):若某条消息持久化失败,把未处理部分重新放回队列头部,避免静默丢失。
364
363
  const remaining = drained.slice(index);