@downcity/agent 1.1.216 → 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 (175) hide show
  1. package/bin/agent/local/Agent.d.ts +2 -0
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +6 -0
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentSessions.d.ts +6 -0
  6. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  7. package/bin/agent/local/services/AgentSessions.js +8 -1
  8. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  9. package/bin/agent/remote/RemoteSession.d.ts +11 -7
  10. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  11. package/bin/agent/remote/RemoteSession.js +36 -11
  12. package/bin/agent/remote/RemoteSession.js.map +1 -1
  13. package/bin/agent/remote/RemoteTransport.d.ts +9 -3
  14. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  15. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +6 -2
  16. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  17. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +38 -11
  18. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  19. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +6 -2
  20. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  21. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -2
  22. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  23. package/bin/executor/types/SessionHistoryMeta.d.ts +2 -0
  24. package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
  25. package/bin/executor/types/SessionRecords.d.ts +2 -0
  26. package/bin/executor/types/SessionRecords.d.ts.map +1 -1
  27. package/bin/executor/types/SessionRecords.js.map +1 -1
  28. package/bin/index.d.ts +5 -2
  29. package/bin/index.d.ts.map +1 -1
  30. package/bin/index.js +1 -0
  31. package/bin/index.js.map +1 -1
  32. package/bin/rpc/Client.d.ts +18 -4
  33. package/bin/rpc/Client.d.ts.map +1 -1
  34. package/bin/rpc/Client.js +25 -3
  35. package/bin/rpc/Client.js.map +1 -1
  36. package/bin/session/Session.d.ts +13 -3
  37. package/bin/session/Session.d.ts.map +1 -1
  38. package/bin/session/Session.js +48 -18
  39. package/bin/session/Session.js.map +1 -1
  40. package/bin/session/browse/Browse.d.ts.map +1 -1
  41. package/bin/session/browse/Browse.js +3 -0
  42. package/bin/session/browse/Browse.js.map +1 -1
  43. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +45 -0
  44. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
  45. package/bin/session/recorder/JsonlSessionMessageStore.js +127 -0
  46. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -0
  47. package/bin/session/recorder/SessionMessageCodec.d.ts +20 -0
  48. package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -0
  49. package/bin/session/recorder/SessionMessageCodec.js +252 -0
  50. package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
  51. package/bin/session/recorder/SessionMessageReducer.d.ts +12 -0
  52. package/bin/session/recorder/SessionMessageReducer.d.ts.map +1 -0
  53. package/bin/session/recorder/SessionMessageReducer.js +109 -0
  54. package/bin/session/recorder/SessionMessageReducer.js.map +1 -0
  55. package/bin/session/recorder/SessionRecorder.d.ts +177 -0
  56. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
  57. package/bin/session/recorder/SessionRecorder.js +610 -0
  58. package/bin/session/recorder/SessionRecorder.js.map +1 -0
  59. package/bin/session/recorder/SessionRecorderCompaction.d.ts +18 -0
  60. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -0
  61. package/bin/session/recorder/SessionRecorderCompaction.js +141 -0
  62. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -0
  63. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +66 -0
  64. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -0
  65. package/bin/session/recorder/SessionRecorderHistoryStore.js +195 -0
  66. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -0
  67. package/bin/session/runtime/SessionMessageEventHub.d.ts +15 -0
  68. package/bin/session/runtime/SessionMessageEventHub.d.ts.map +1 -0
  69. package/bin/session/runtime/SessionMessageEventHub.js +28 -0
  70. package/bin/session/runtime/SessionMessageEventHub.js.map +1 -0
  71. package/bin/session/runtime/SessionPromptRuntime.d.ts +7 -13
  72. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  73. package/bin/session/runtime/SessionPromptRuntime.js +13 -13
  74. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  75. package/bin/session/services/SessionStateService.d.ts +15 -3
  76. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  77. package/bin/session/services/SessionStateService.js +165 -76
  78. package/bin/session/services/SessionStateService.js.map +1 -1
  79. package/bin/session/services/SessionTurnService.d.ts +4 -0
  80. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  81. package/bin/session/services/SessionTurnService.js +82 -52
  82. package/bin/session/services/SessionTurnService.js.map +1 -1
  83. package/bin/session/services/SessionViewService.d.ts +8 -3
  84. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  85. package/bin/session/services/SessionViewService.js +13 -5
  86. package/bin/session/services/SessionViewService.js.map +1 -1
  87. package/bin/session/storage/Metadata.d.ts +2 -0
  88. package/bin/session/storage/Metadata.d.ts.map +1 -1
  89. package/bin/session/storage/Metadata.js +10 -0
  90. package/bin/session/storage/Metadata.js.map +1 -1
  91. package/bin/session/storage/Persistence.d.ts +6 -0
  92. package/bin/session/storage/Persistence.d.ts.map +1 -1
  93. package/bin/session/storage/Persistence.js +12 -0
  94. package/bin/session/storage/Persistence.js.map +1 -1
  95. package/bin/session/storage/RuntimeSessionPort.d.ts +3 -2
  96. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  97. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  98. package/bin/types/agent/AgentOptions.d.ts +10 -0
  99. package/bin/types/agent/AgentOptions.d.ts.map +1 -1
  100. package/bin/types/agent/SessionActor.d.ts +12 -4
  101. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  102. package/bin/types/agent/SessionTypes.d.ts +13 -1
  103. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  104. package/bin/types/rpc/RpcProtocol.d.ts +32 -6
  105. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  106. package/bin/types/runtime/agent/AgentContext.d.ts +4 -3
  107. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  108. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  109. package/bin/types/sdk/AgentSessionEvent.d.ts +2 -1
  110. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
  111. package/bin/types/session/SessionMessage.d.ts +217 -0
  112. package/bin/types/session/SessionMessage.d.ts.map +1 -0
  113. package/bin/types/session/SessionMessage.js +8 -0
  114. package/bin/types/session/SessionMessage.js.map +1 -0
  115. package/bin/types/session/SessionMessageMutation.d.ts +92 -0
  116. package/bin/types/session/SessionMessageMutation.d.ts.map +1 -0
  117. package/bin/types/session/SessionMessageMutation.js +21 -0
  118. package/bin/types/session/SessionMessageMutation.js.map +1 -0
  119. package/bin/types/session/SessionOptions.d.ts +3 -0
  120. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  121. package/package.json +3 -3
  122. package/scripts/agent-env.test.mjs +4 -4
  123. package/scripts/remote-session-reconnect.test.mjs +33 -0
  124. package/scripts/session-action-message.test.mjs +148 -27
  125. package/scripts/session-list-title.test.mjs +6 -19
  126. package/scripts/session-prompt-runtime.test.mjs +4 -16
  127. package/scripts/session-recorder.test.mjs +245 -0
  128. package/scripts/session-title-event.test.mjs +10 -18
  129. package/src/agent/local/Agent.ts +6 -0
  130. package/src/agent/local/services/AgentSessions.ts +14 -1
  131. package/src/agent/remote/RemoteSession.ts +52 -16
  132. package/src/agent/remote/RemoteTransport.ts +20 -6
  133. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +67 -13
  134. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +27 -6
  135. package/src/executor/types/SessionHistoryMeta.ts +2 -0
  136. package/src/executor/types/SessionRecords.ts +2 -0
  137. package/src/index.ts +23 -5
  138. package/src/rpc/Client.ts +47 -8
  139. package/src/session/Session.ts +66 -31
  140. package/src/session/browse/Browse.ts +3 -0
  141. package/src/session/recorder/JsonlSessionMessageStore.ts +162 -0
  142. package/src/session/recorder/SessionMessageCodec.ts +279 -0
  143. package/src/session/recorder/SessionMessageReducer.ts +155 -0
  144. package/src/session/recorder/SessionRecorder.ts +812 -0
  145. package/src/session/recorder/SessionRecorderCompaction.ts +187 -0
  146. package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
  147. package/src/session/runtime/SessionMessageEventHub.ts +35 -0
  148. package/src/session/runtime/SessionPromptRuntime.ts +23 -24
  149. package/src/session/services/SessionStateService.ts +189 -84
  150. package/src/session/services/SessionTurnService.ts +88 -59
  151. package/src/session/services/SessionViewService.ts +28 -13
  152. package/src/session/storage/Metadata.ts +13 -0
  153. package/src/session/storage/Persistence.ts +18 -0
  154. package/src/session/storage/RuntimeSessionPort.ts +6 -4
  155. package/src/types/agent/AgentOptions.ts +12 -0
  156. package/src/types/agent/SessionActor.ts +28 -5
  157. package/src/types/agent/SessionTypes.ts +13 -1
  158. package/src/types/rpc/RpcProtocol.ts +34 -6
  159. package/src/types/runtime/agent/AgentContext.ts +8 -2
  160. package/src/types/sdk/AgentSessionEvent.ts +2 -0
  161. package/src/types/session/SessionMessage.ts +247 -0
  162. package/src/types/session/SessionMessageMutation.ts +121 -0
  163. package/src/types/session/SessionOptions.ts +4 -0
  164. package/tsconfig.tsbuildinfo +1 -1
  165. package/bin/session/SessionEventMapper.d.ts +0 -32
  166. package/bin/session/SessionEventMapper.d.ts.map +0 -1
  167. package/bin/session/SessionEventMapper.js +0 -131
  168. package/bin/session/SessionEventMapper.js.map +0 -1
  169. package/bin/types/sdk/InternalUiChunkEvent.d.ts +0 -100
  170. package/bin/types/sdk/InternalUiChunkEvent.d.ts.map +0 -1
  171. package/bin/types/sdk/InternalUiChunkEvent.js +0 -9
  172. package/bin/types/sdk/InternalUiChunkEvent.js.map +0 -1
  173. package/scripts/session-history-archive.test.mjs +0 -188
  174. package/src/session/SessionEventMapper.ts +0 -148
  175. package/src/types/sdk/InternalUiChunkEvent.ts +0 -108
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @file 验证 SessionRecorder 单一 Mutation 日志与实时/历史一致性。
3
+ */
4
+
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import fs from "node:fs/promises";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+
11
+ import { JsonlSessionMessageStore } from "../bin/session/recorder/JsonlSessionMessageStore.js";
12
+ import { SessionRecorder } from "../bin/session/recorder/SessionRecorder.js";
13
+ import { SessionRecorderHistoryStore } from "../bin/session/recorder/SessionRecorderHistoryStore.js";
14
+ import { MockLanguageModelV3 } from "ai/test";
15
+
16
+ async function create_recorder(session_id = "session-recorder-test") {
17
+ const root_path = await fs.mkdtemp(
18
+ path.join(os.tmpdir(), "downcity-session-recorder-"),
19
+ );
20
+ const file_path = path.join(root_path, "messages.jsonl");
21
+ const events = [];
22
+ const recorder = new SessionRecorder({
23
+ session_id,
24
+ store: new JsonlSessionMessageStore({ session_id, file_path }),
25
+ publish: (mutation) => events.push(mutation),
26
+ });
27
+ await recorder.initialize();
28
+ return { recorder, events, file_path };
29
+ }
30
+
31
+ test("每个 assistant delta 都先持久化为独立 Mutation 再发布", async () => {
32
+ const { recorder, events, file_path } = await create_recorder();
33
+ await recorder.append_user_message({
34
+ turn_id: "turn-1",
35
+ input_type: "prompt",
36
+ parts: [{
37
+ part_id: "user-text-1",
38
+ type: "text",
39
+ text: "你好",
40
+ state: "done",
41
+ }],
42
+ });
43
+ const writer = await recorder.open_assistant_message({
44
+ turn_id: "turn-1",
45
+ segment_index: 1,
46
+ });
47
+ await writer.apply_chunk({ type: "text-start", id: "text-1" });
48
+ await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "你" });
49
+ await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "好" });
50
+ await writer.apply_chunk({ type: "text-end", id: "text-1" });
51
+ await writer.complete();
52
+
53
+ const changes = await recorder.list_message_changes({
54
+ after_commit_sequence: 0,
55
+ });
56
+ const persisted_lines = (await fs.readFile(file_path, "utf8"))
57
+ .trim()
58
+ .split("\n");
59
+ assert.equal(persisted_lines.length, changes.items.length);
60
+ assert.deepEqual(
61
+ changes.items.map((item) => item.commit_sequence),
62
+ events.map((item) => item.commit_sequence),
63
+ );
64
+ assert.deepEqual(
65
+ changes.items.map((item) => `${item.variant}:${item.type}`),
66
+ [
67
+ "message:user",
68
+ "message:assistant",
69
+ "part:text",
70
+ "delta:text",
71
+ "delta:text",
72
+ "part:text",
73
+ "message:assistant",
74
+ ],
75
+ );
76
+ assert.deepEqual(
77
+ changes.items
78
+ .filter((item) => item.variant === "delta")
79
+ .map((item) => item.delta),
80
+ ["你", "好"],
81
+ );
82
+
83
+ const page = await recorder.list_messages();
84
+ assert.deepEqual(page.items.map((item) => item.type), ["user", "assistant"]);
85
+ const assistant = page.items[1];
86
+ assert.equal(assistant.status, "completed");
87
+ assert.equal(assistant.parts[0].text, "你好");
88
+ });
89
+
90
+ test("tool 与 action 在原 Message 和 part 上更新", async () => {
91
+ const { recorder } = await create_recorder("tool-action-test");
92
+ const writer = await recorder.open_assistant_message({
93
+ turn_id: "turn-1",
94
+ segment_index: 1,
95
+ });
96
+ await writer.apply_chunk({
97
+ type: "tool-input-start",
98
+ toolCallId: "call-1",
99
+ toolName: "search",
100
+ });
101
+ await writer.apply_chunk({
102
+ type: "tool-input-available",
103
+ toolCallId: "call-1",
104
+ toolName: "search",
105
+ input: { query: "downcity" },
106
+ });
107
+ await writer.apply_chunk({
108
+ type: "tool-output-available",
109
+ toolCallId: "call-1",
110
+ output: { count: 1 },
111
+ });
112
+ await writer.complete();
113
+
114
+ const action = await recorder.open_action_message({
115
+ message_id: "action-1",
116
+ action_type: "compact",
117
+ title: "Compacting",
118
+ });
119
+ const running = recorder.get_message("action-1");
120
+ await action.complete({ title: "Compacted" });
121
+ const completed = recorder.get_message("action-1");
122
+
123
+ const assistant = (await recorder.list_messages()).items[0];
124
+ assert.equal(assistant.parts.length, 1);
125
+ assert.equal(assistant.parts[0].state, "completed");
126
+ assert.deepEqual(assistant.parts[0].output, { count: 1 });
127
+ assert.equal(running.sequence, completed.sequence);
128
+ assert.equal(completed.revision, running.revision + 1);
129
+ assert.equal(completed.status, "completed");
130
+ });
131
+
132
+ test("snapshot、changes 分页和重启恢复保持单调顺序", async () => {
133
+ const { recorder, file_path } = await create_recorder("recovery-test");
134
+ await recorder.append_user_message({
135
+ turn_id: "turn-1",
136
+ input_type: "prompt",
137
+ parts: [{ part_id: "u1", type: "text", text: "one", state: "done" }],
138
+ });
139
+ await recorder.open_assistant_message({
140
+ turn_id: "turn-1",
141
+ segment_index: 1,
142
+ });
143
+ await recorder.open_action_message({
144
+ message_id: "running-action",
145
+ action_type: "fork",
146
+ title: "Forking",
147
+ });
148
+
149
+ const recovered_events = [];
150
+ const recovered = new SessionRecorder({
151
+ session_id: "recovery-test",
152
+ store: new JsonlSessionMessageStore({
153
+ session_id: "recovery-test",
154
+ file_path,
155
+ }),
156
+ publish: (mutation) => recovered_events.push(mutation),
157
+ });
158
+ await recovered.initialize();
159
+
160
+ const first_page = await recovered.list_messages({ limit: 2 });
161
+ const second_page = await recovered.list_messages({
162
+ limit: 2,
163
+ cursor: first_page.next_cursor,
164
+ });
165
+ assert.equal(first_page.items.length, 2);
166
+ assert.equal(second_page.items.length, 1);
167
+ assert.ok(first_page.items[1].sequence < second_page.items[0].sequence);
168
+
169
+ const messages = [...first_page.items, ...second_page.items];
170
+ assert.equal(messages[1].status, "stopped");
171
+ assert.equal(messages[2].status, "failed");
172
+ assert.deepEqual(
173
+ recovered_events.map((item) => `${item.variant}:${item.type}`),
174
+ ["message:assistant", "message:action"],
175
+ );
176
+
177
+ const first_changes = await recovered.list_message_changes({
178
+ after_commit_sequence: 0,
179
+ limit: 2,
180
+ });
181
+ const next_changes = await recovered.list_message_changes({
182
+ after_commit_sequence: first_changes.next_commit_sequence,
183
+ limit: 100,
184
+ });
185
+ assert.equal(first_changes.has_more, true);
186
+ assert.ok(
187
+ first_changes.items.at(-1).commit_sequence <
188
+ next_changes.items[0].commit_sequence,
189
+ );
190
+ });
191
+
192
+ test("compact 追加 internal summary 且不重写 Mutation 日志", async () => {
193
+ const { recorder, file_path } = await create_recorder("compact-test");
194
+ for (let index = 1; index <= 6; index += 1) {
195
+ await recorder.append_user_message({
196
+ turn_id: `turn-${String(index)}`,
197
+ input_type: "prompt",
198
+ parts: [{
199
+ part_id: `user-${String(index)}`,
200
+ type: "text",
201
+ text: `message ${String(index)}`,
202
+ state: "done",
203
+ }],
204
+ });
205
+ }
206
+ const before = await fs.readFile(file_path, "utf8");
207
+ const history_store = new SessionRecorderHistoryStore({
208
+ session_id: "compact-test",
209
+ recorder,
210
+ });
211
+ const model = new MockLanguageModelV3({
212
+ modelId: "compact-model",
213
+ doGenerate: async () => ({
214
+ content: [{ type: "text", text: "Compact summary" }],
215
+ finishReason: { unified: "stop", raw: "stop" },
216
+ usage: {
217
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
218
+ outputTokens: { total: 0, text: 0, reasoning: 0 },
219
+ },
220
+ warnings: [],
221
+ }),
222
+ });
223
+ const result = await history_store.compact({
224
+ model,
225
+ system: [],
226
+ keepLastMessages: 2,
227
+ maxInputTokensApprox: 1,
228
+ archiveOnCompact: false,
229
+ compactRatio: 0.5,
230
+ });
231
+ assert.equal(result.compacted, true);
232
+
233
+ const after = await fs.readFile(file_path, "utf8");
234
+ assert.equal(after.startsWith(before), true);
235
+ const all = await recorder.list_messages({
236
+ limit: 100,
237
+ include_internal: true,
238
+ });
239
+ const summary = all.items.find(
240
+ (message) => message.type === "assistant" && message.kind === "summary",
241
+ );
242
+ assert.equal(summary.visibility, "internal");
243
+ assert.equal(summary.summary_through_message_id, all.items[2].message_id);
244
+ assert.equal((await history_store.list_records()).length, 4);
245
+ });
@@ -110,11 +110,13 @@ test("Session keeps title empty when no model is available", async () => {
110
110
  text: "Use shell tools to inspect the current workspace",
111
111
  });
112
112
 
113
- const title_event = events.find((event) => event.type === "session-title");
114
- assert.equal(title_event, undefined);
115
-
116
- const records = await session.records();
117
- assert.equal(records.session.title, undefined);
113
+ assert.equal(
114
+ events.some(
115
+ (event) => event.variant === "message" && event.type === "user",
116
+ ),
117
+ true,
118
+ );
119
+ assert.equal((await session.get_info()).title, undefined);
118
120
  } finally {
119
121
  unsubscribe();
120
122
  await agent.dispose();
@@ -137,8 +139,7 @@ test("Session logs title generation failure without blocking the session", async
137
139
  text: "Diagnose why session title generation is flaky",
138
140
  });
139
141
 
140
- const records = await session.records();
141
- assert.equal(records.session.title, undefined);
142
+ assert.equal((await session.get_info()).title, undefined);
142
143
 
143
144
  await agent.getLogger().saveAllLogs();
144
145
  const log_lines = await read_log_lines(agent_path);
@@ -185,8 +186,7 @@ test("Session retries title generation after model becomes available", async ()
185
186
  text: "Investigate flaky session title generation in the SDK",
186
187
  });
187
188
 
188
- const history_before_model = await session.records();
189
- assert.equal(history_before_model.session.title, undefined);
189
+ assert.equal((await session.get_info()).title, undefined);
190
190
 
191
191
  await session.set({
192
192
  model: create_mock_title_model("排查 session 标题"),
@@ -195,15 +195,7 @@ test("Session retries title generation after model becomes available", async ()
195
195
  text: "Need another prompt to trigger the retry path",
196
196
  });
197
197
 
198
- const title_event = events.find((event) => event.type === "session-title");
199
- assert.deepEqual(title_event, {
200
- type: "session-title",
201
- sessionId: session.id,
202
- title: "排查 session 标题",
203
- });
204
-
205
- const records = await session.records();
206
- assert.equal(records.session.title, "排查 session 标题");
198
+ assert.equal((await session.get_info()).title, "排查 session 标题");
207
199
  } finally {
208
200
  unsubscribe();
209
201
  await agent.dispose();
@@ -47,6 +47,8 @@ export class Agent {
47
47
  private readonly config: DowncityConfig;
48
48
  private readonly env: Record<string, string>;
49
49
  private readonly defaultModel?: AgentModel;
50
+ private readonly default_model_id?: string;
51
+ private readonly resolve_model?: AgentOptions["resolve_model"];
50
52
  private readonly SessionClass: AgentOptions["Session"];
51
53
  private readonly backgroundService: AgentBackgroundService;
52
54
  private readonly shell?: AgentOptions["shell"];
@@ -55,6 +57,8 @@ export class Agent {
55
57
 
56
58
  constructor(options: AgentOptions) {
57
59
  this.defaultModel = options.model;
60
+ this.default_model_id = String(options.model_id || "").trim() || undefined;
61
+ this.resolve_model = options.resolve_model;
58
62
  this.SessionClass = options.Session;
59
63
  let sessions_ref: LocalAgentSessions | null = null;
60
64
  const assembly_service = new AgentAssemblyService({
@@ -261,6 +265,8 @@ export class Agent {
261
265
  await this.backgroundService.ready();
262
266
  },
263
267
  default_model: this.defaultModel,
268
+ default_model_id: this.default_model_id,
269
+ resolve_model: this.resolve_model,
264
270
  SessionClass: this.SessionClass,
265
271
  });
266
272
  }
@@ -92,6 +92,12 @@ type AgentSessionsOptions = {
92
92
  */
93
93
  default_model?: AgentModel;
94
94
 
95
+ /** 当前默认模型的稳定 ID。 */
96
+ default_model_id?: string;
97
+
98
+ /** 按稳定 ID 解析 Session 模型。 */
99
+ resolve_model?: (model_id: string) => Promise<AgentModel>;
100
+
95
101
  /**
96
102
  * 当前 agent 使用的本地 Session 类。
97
103
  */
@@ -110,6 +116,8 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
110
116
  private readonly get_instruction: AgentSessionsOptions["get_instruction"];
111
117
  private readonly ensure_agent_ready: AgentSessionsOptions["ensure_agent_ready"];
112
118
  private readonly default_model?: AgentModel;
119
+ private readonly default_model_id?: string;
120
+ private readonly resolve_model?: AgentSessionsOptions["resolve_model"];
113
121
  private readonly SessionClass: AgentSessionConstructor;
114
122
  private readonly sessions_by_id = new Map<string, AgentManagedSession>();
115
123
  private readonly configured_session_ids = new Set<string>();
@@ -123,6 +131,8 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
123
131
  this.get_instruction = options.get_instruction;
124
132
  this.ensure_agent_ready = options.ensure_agent_ready;
125
133
  this.default_model = options.default_model;
134
+ this.default_model_id = options.default_model_id;
135
+ this.resolve_model = options.resolve_model;
126
136
  this.SessionClass = options.SessionClass || Session;
127
137
  }
128
138
 
@@ -337,6 +347,7 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
337
347
  await this.ensure_agent_ready();
338
348
  await this.apply_session_defaults(session);
339
349
  },
350
+ ...(this.resolve_model ? { resolve_model: this.resolve_model } : {}),
340
351
  });
341
352
  this.sessions_by_id.set(resolved_session_id, created);
342
353
  return created;
@@ -346,9 +357,11 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
346
357
  session: AgentManagedSession,
347
358
  ): Promise<void> {
348
359
  if (this.configured_session_ids.has(session.id)) return;
349
- if (this.default_model) {
360
+ const persisted_model_id = String(session.config.modelId || "").trim();
361
+ if (!persisted_model_id && this.default_model) {
350
362
  await session.set({
351
363
  model: this.default_model,
364
+ ...(this.default_model_id ? { modelId: this.default_model_id } : {}),
352
365
  });
353
366
  }
354
367
  this.configured_session_ids.add(session.id);
@@ -9,8 +9,6 @@
9
9
  import type {
10
10
  AgentSessionConfigSnapshot,
11
11
  AgentSessionForkInput,
12
- AgentSessionRecordsInput,
13
- AgentSessionRecordsPage,
14
12
  AgentSessionInfo,
15
13
  AgentSessionSetInput,
16
14
  AgentSessionSystemSnapshot,
@@ -28,6 +26,17 @@ import type {
28
26
  AgentSessionTurnHandle,
29
27
  AgentSessionTurnResult,
30
28
  } from "@/types/sdk/AgentSessionTurn.js";
29
+ import type {
30
+ ListSessionMessagesInput,
31
+ SessionMessagePage,
32
+ } from "@/types/session/SessionMessage.js";
33
+ import {
34
+ is_session_message_mutation,
35
+ type ListSessionMessageChangesInput,
36
+ type SessionMessageMutationPage,
37
+ type SessionMessageMutationSubscriber,
38
+ type SessionMessageMutationUnsubscribe,
39
+ } from "@/types/session/SessionMessageMutation.js";
31
40
  import { SessionEventHub } from "@/session/runtime/SessionEventHub.js";
32
41
  import type {
33
42
  RemoteSessionTransport,
@@ -71,17 +80,25 @@ export class RemoteSession implements RemoteAgentSession {
71
80
  this.transport = transport;
72
81
  this.id = info.sessionId;
73
82
  this.agentId = info.agentId;
74
- // 远程 session 不暴露服务端模型实例,config 返回空快照。
75
- this.config = {} as AgentSessionConfigSnapshot;
83
+ // 远程 session 不暴露服务端模型实例,只保留可序列化配置。
84
+ this.config = {
85
+ ...(info.modelId ? { modelId: info.modelId } : {}),
86
+ ...(info.modelLabel ? { modelLabel: info.modelLabel } : {}),
87
+ };
76
88
  }
77
89
 
78
- /**
79
- * 远程 session 当前不支持直接注入本地模型实例。
80
- */
81
- async set(_input: AgentSessionSetInput): Promise<void> {
82
- throw new Error(
83
- "Remote session.set({ model }) is not supported in v1. Configure the model on the server-side local Agent session instead.",
84
- );
90
+ /** 按稳定模型 ID 更新远程 Session 模型。 */
91
+ async set(input: AgentSessionSetInput): Promise<void> {
92
+ if (input.model) {
93
+ throw new Error("Remote session.set does not accept a local model instance.");
94
+ }
95
+ const model_id = String(input.modelId || "").trim();
96
+ if (!model_id) {
97
+ throw new Error("Remote session.set requires modelId.");
98
+ }
99
+ const info = await this.transport.set(this.id, { modelId: model_id });
100
+ this.config.modelId = info.modelId;
101
+ this.config.modelLabel = info.modelLabel;
85
102
  }
86
103
 
87
104
  /**
@@ -116,7 +133,9 @@ export class RemoteSession implements RemoteAgentSession {
116
133
  /**
117
134
  * 订阅当前远程 session 的 future 事件。
118
135
  */
119
- subscribe(subscriber: AgentSessionSubscriber): AgentSessionUnsubscribe {
136
+ subscribe(
137
+ subscriber: SessionMessageMutationSubscriber,
138
+ ): SessionMessageMutationUnsubscribe {
120
139
  this.event_subscriber_count += 1;
121
140
  void this.ensure_event_pump().catch((error) => {
122
141
  this.event_hub.publish({
@@ -124,7 +143,9 @@ export class RemoteSession implements RemoteAgentSession {
124
143
  message: error instanceof Error ? error.message : String(error),
125
144
  });
126
145
  });
127
- const unsubscribe = this.event_hub.subscribe(subscriber);
146
+ const unsubscribe = this.event_hub.subscribe((event) => {
147
+ if (is_session_message_mutation(event)) subscriber(event);
148
+ });
128
149
  return () => {
129
150
  unsubscribe();
130
151
  this.event_subscriber_count = Math.max(0, this.event_subscriber_count - 1);
@@ -132,11 +153,23 @@ export class RemoteSession implements RemoteAgentSession {
132
153
  };
133
154
  }
134
155
 
156
+ /** 订阅 transport 使用的 Message 与 lifecycle 事件。 */
157
+ subscribe_transport(subscriber: AgentSessionSubscriber): AgentSessionUnsubscribe {
158
+ return this.event_hub.subscribe(subscriber);
159
+ }
160
+
135
161
  /**
136
162
  * 读取远程消息历史。
137
163
  */
138
- async records(input?: AgentSessionRecordsInput): Promise<AgentSessionRecordsPage> {
139
- return await this.transport.records(this.id, input);
164
+ async messages(input?: ListSessionMessagesInput): Promise<SessionMessagePage> {
165
+ return await this.transport.messages(this.id, input);
166
+ }
167
+
168
+ /** 读取远程增量 Message Mutation。 */
169
+ async message_changes(
170
+ input: ListSessionMessageChangesInput,
171
+ ): Promise<SessionMessageMutationPage> {
172
+ return await this.transport.message_changes(this.id, input);
140
173
  }
141
174
 
142
175
  /**
@@ -189,7 +222,7 @@ export class RemoteSession implements RemoteAgentSession {
189
222
  }
190
223
 
191
224
  private handle_event(event: AgentSessionEvent): void {
192
- if (event.type === "error") {
225
+ if (!is_session_message_mutation(event) && event.type === "error") {
193
226
  this.fail_pending_turns(event.message);
194
227
  }
195
228
  const turn_id = extract_turn_id(event);
@@ -306,6 +339,9 @@ function create_turn_handle(
306
339
  }
307
340
 
308
341
  function extract_turn_id(event: AgentSessionEvent): string | null {
342
+ if (is_session_message_mutation(event)) {
343
+ return event.turn_id || null;
344
+ }
309
345
  switch (event.type) {
310
346
  case "turn-start":
311
347
  case "text-delta":
@@ -15,11 +15,10 @@ import type {
15
15
  AgentArchiveSessionsResult,
16
16
  AgentCleanArchiveResult,
17
17
  AgentSessionForkInput,
18
- AgentSessionRecordsInput,
19
- AgentSessionRecordsPage,
20
18
  AgentSessionInfo,
21
19
  AgentSessionSummaryPage,
22
20
  AgentSessionSystemSnapshot,
21
+ AgentSessionSetInput,
23
22
  } from "@/types/agent/SessionTypes.js";
24
23
  import type {
25
24
  RemoteAgentPluginActionInput,
@@ -28,6 +27,14 @@ import type {
28
27
  import type { AgentSessionEvent } from "@/types/sdk/AgentSessionEvent.js";
29
28
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
30
29
  import type { AgentSessionStopResult } from "@/types/sdk/AgentSessionStop.js";
30
+ import type {
31
+ ListSessionMessagesInput,
32
+ SessionMessagePage,
33
+ } from "@/types/session/SessionMessage.js";
34
+ import type {
35
+ ListSessionMessageChangesInput,
36
+ SessionMessageMutationPage,
37
+ } from "@/types/session/SessionMessageMutation.js";
31
38
  import type {
32
39
  ShellApprovalMode,
33
40
  ShellApprovalDecisionResult,
@@ -51,6 +58,8 @@ export type TransportSubscription = {
51
58
  export type RemoteSessionTransport = {
52
59
  /** 读取 session 信息。 */
53
60
  get_info(session_id: string): Promise<AgentSessionInfo>;
61
+ /** 按稳定模型 ID 更新远程 session 配置。 */
62
+ set(session_id: string, input: AgentSessionSetInput): Promise<AgentSessionInfo>;
54
63
  /** 发送 prompt。 */
55
64
  prompt(
56
65
  session_id: string,
@@ -66,11 +75,16 @@ export type RemoteSessionTransport = {
66
75
  /** 底层事件连接结束后的通知;主动关闭不触发。 */
67
76
  on_close: (error?: unknown) => void;
68
77
  }): Promise<TransportSubscription>;
69
- /** 读取 history。 */
70
- records(
78
+ /** 读取 Session Message。 */
79
+ messages(
80
+ session_id: string,
81
+ input?: ListSessionMessagesInput,
82
+ ): Promise<SessionMessagePage>;
83
+ /** 读取增量 Message Mutation。 */
84
+ message_changes(
71
85
  session_id: string,
72
- input?: AgentSessionRecordsInput,
73
- ): Promise<AgentSessionRecordsPage>;
86
+ input: ListSessionMessageChangesInput,
87
+ ): Promise<SessionMessageMutationPage>;
74
88
  /** 读取 system snapshot。 */
75
89
  system(session_id: string): Promise<AgentSessionSystemSnapshot>;
76
90
  /** 分叉 session。 */
@@ -15,12 +15,19 @@ import type {
15
15
  AgentArchiveSessionsResult,
16
16
  AgentCleanArchiveResult,
17
17
  AgentSessionForkInput,
18
- AgentSessionRecordsInput,
19
- AgentSessionRecordsPage,
20
18
  AgentSessionInfo,
21
19
  AgentSessionSummaryPage,
22
20
  AgentSessionSystemSnapshot,
21
+ AgentSessionSetInput,
23
22
  } from "@/types/agent/SessionTypes.js";
23
+ import type {
24
+ ListSessionMessagesInput,
25
+ SessionMessagePage,
26
+ } from "@/types/session/SessionMessage.js";
27
+ import type {
28
+ ListSessionMessageChangesInput,
29
+ SessionMessageMutationPage,
30
+ } from "@/types/session/SessionMessageMutation.js";
24
31
  import type {
25
32
  RemoteAgentPluginActionInput,
26
33
  RemoteAgentPluginActionResult,
@@ -100,6 +107,27 @@ export class HttpRemoteAgentTransport implements RemoteAgentTransport {
100
107
  return payload.session;
101
108
  }
102
109
 
110
+ async set(
111
+ session_id: string,
112
+ input: AgentSessionSetInput,
113
+ ): Promise<AgentSessionInfo> {
114
+ const payload = await read_http_json<{
115
+ success?: boolean;
116
+ error?: string;
117
+ session?: AgentSessionInfo;
118
+ }>(`${this.base_url}/api/sdk/sessions/${encodeURIComponent(session_id)}/model`, {
119
+ method: "PUT",
120
+ headers: this.headers({
121
+ "Content-Type": "application/json",
122
+ }),
123
+ body: JSON.stringify({ modelId: input.modelId }),
124
+ });
125
+ if (!payload.success || !payload.session?.sessionId) {
126
+ throw new Error(String(payload.error || "Remote session model update failed"));
127
+ }
128
+ return payload.session;
129
+ }
130
+
103
131
  async prompt(
104
132
  session_id: string,
105
133
  input: AgentSessionPromptInput,
@@ -191,32 +219,58 @@ export class HttpRemoteAgentTransport implements RemoteAgentTransport {
191
219
  };
192
220
  }
193
221
 
194
- async records(
222
+ async messages(
195
223
  session_id: string,
196
- input?: AgentSessionRecordsInput,
197
- ): Promise<AgentSessionRecordsPage> {
224
+ input?: ListSessionMessagesInput,
225
+ ): Promise<SessionMessagePage> {
198
226
  const query = new URLSearchParams();
199
227
  if (input?.limit !== undefined) query.set("limit", String(input.limit));
200
228
  if (input?.cursor) query.set("cursor", input.cursor);
201
- if (input?.archive_id) query.set("archive_id", input.archive_id);
202
- if (input?.order) query.set("order", input.order);
203
- if (input?.view) query.set("view", input.view);
229
+ if (input?.before_sequence !== undefined) {
230
+ query.set("before_sequence", String(input.before_sequence));
231
+ }
232
+ if (input?.include_internal) query.set("include_internal", "true");
233
+ if (input?.through_sequence !== undefined) {
234
+ query.set("through_sequence", String(input.through_sequence));
235
+ }
204
236
  const payload = await read_http_json<{
205
237
  success?: boolean;
206
238
  error?: string;
207
- records?: AgentSessionRecordsPage;
239
+ messages?: SessionMessagePage;
208
240
  }>(
209
- `${this.base_url}/api/sdk/sessions/${encodeURIComponent(session_id)}/records${
241
+ `${this.base_url}/api/sdk/sessions/${encodeURIComponent(session_id)}/messages${
210
242
  query.size > 0 ? `?${query.toString()}` : ""
211
243
  }`,
212
244
  {
213
245
  headers: this.headers(),
214
246
  },
215
247
  );
216
- if (!payload.success || !payload.records || !Array.isArray(payload.records.items)) {
217
- throw new Error(String(payload.error || "Remote session records failed"));
248
+ if (!payload.success || !payload.messages || !Array.isArray(payload.messages.items)) {
249
+ throw new Error(String(payload.error || "Remote session messages failed"));
250
+ }
251
+ return payload.messages;
252
+ }
253
+
254
+ async message_changes(
255
+ session_id: string,
256
+ input: ListSessionMessageChangesInput,
257
+ ): Promise<SessionMessageMutationPage> {
258
+ const query = new URLSearchParams({
259
+ after_commit_sequence: String(input.after_commit_sequence),
260
+ });
261
+ if (input.limit !== undefined) query.set("limit", String(input.limit));
262
+ const payload = await read_http_json<{
263
+ success?: boolean;
264
+ error?: string;
265
+ changes?: SessionMessageMutationPage;
266
+ }>(
267
+ `${this.base_url}/api/sdk/sessions/${encodeURIComponent(session_id)}/message-changes?${query.toString()}`,
268
+ { headers: this.headers() },
269
+ );
270
+ if (!payload.success || !payload.changes) {
271
+ throw new Error(String(payload.error || "Remote session message changes failed"));
218
272
  }
219
- return payload.records;
273
+ return payload.changes;
220
274
  }
221
275
 
222
276
  async system(session_id: string): Promise<AgentSessionSystemSnapshot> {