@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
@@ -12,6 +12,7 @@ import type { SessionPort } from "../../types/runtime/agent/AgentContext.js";
12
12
  import type { AgentSessionSystemBlock } from "../../types/agent/SessionTypes.js";
13
13
  import type { SessionComposerOptions } from "../../types/session/SessionComposerOptions.js";
14
14
  import type { Logger } from "../../utils/logger/Logger.js";
15
+ import type { AgentModel } from "../../model/CityModelAdapter.js";
15
16
  /**
16
17
  * Agent 可管理的本地 Session 实例。
17
18
  */
@@ -69,6 +70,8 @@ export interface SessionOptions {
69
70
  * 在执行前确保当前 session 已完成宿主侧默认配置。
70
71
  */
71
72
  ensureConfigured?: (session: AgentManagedSession) => Promise<void>;
73
+ /** 按稳定模型 ID 解析当前 Session 的运行时模型。 */
74
+ resolve_model?: (model_id: string) => Promise<AgentModel>;
72
75
  /**
73
76
  * 当前 session 的 Composer 覆盖项。
74
77
  */
@@ -1 +1 @@
1
- {"version":3,"file":"SessionOptions.d.ts","sourceRoot":"","sources":["../../../src/types/session/SessionOptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACxF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACvD;;OAEG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,cAAc,IAAI,WAAW,CAAC;IAE9B;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,0BAA0B,EAAE,MAAM,uBAAuB,EAAE,CAAC;IAE5D;;OAEG;IACH,4BAA4B,EAAE,MAAM,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAEvE;;OAEG;IACH,qBAAqB,EAAE,MAAM,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAEhE;;OAEG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;OAEG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAC;CACpC"}
1
+ {"version":3,"file":"SessionOptions.d.ts","sourceRoot":"","sources":["../../../src/types/session/SessionOptions.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AACzE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACxF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACvD;;OAEG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,cAAc,IAAI,WAAW,CAAC;IAE9B;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,0BAA0B,EAAE,MAAM,uBAAuB,EAAE,CAAC;IAE5D;;OAEG;IACH,4BAA4B,EAAE,MAAM,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAEvE;;OAEG;IACH,qBAAqB,EAAE,MAAM,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;IAEhE;;OAEG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE,oCAAoC;IACpC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAE1D;;OAEG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAC;CACpC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downcity/agent",
3
- "version": "1.1.216",
3
+ "version": "1.1.222",
4
4
  "type": "module",
5
5
  "description": "Downcity Agent 运行时 — 单 Agent 执行壳与本机 RPC 能力",
6
6
  "main": "./bin/index.js",
@@ -13,8 +13,8 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@ai-sdk/openai-compatible": "^2.0.48",
16
- "@downcity/shell": "^0.1.80",
17
- "@downcity/type": "0.1.113",
16
+ "@downcity/shell": "^0.1.85",
17
+ "@downcity/type": "0.1.118",
18
18
  "ai": "^6.0.193",
19
19
  "commander": "^15.0.0",
20
20
  "dotenv": "^17.4.2",
@@ -33,7 +33,7 @@ test("项目 .env 覆盖宿主环境且不修改 process.env", () => {
33
33
  assert.equal(env.PROJECT_ONLY, "value");
34
34
  assert.equal(process.env.DOWNCITY_ENV_TEST, original_value);
35
35
  } finally {
36
- fs.rmSync(project_root, { recursive: true, force: true });
36
+ fs.rmSync(project_root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
37
37
  }
38
38
  });
39
39
 
@@ -50,7 +50,7 @@ test("SDK 未传 config 时不读取 downcity.json", async () => {
50
50
  await agent.ready();
51
51
  await agent.dispose();
52
52
  } finally {
53
- fs.rmSync(project_root, { recursive: true, force: true });
53
+ fs.rmSync(project_root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
54
54
  }
55
55
  });
56
56
 
@@ -65,7 +65,7 @@ test("SDK 未注入 plugin_config 时明确拒绝持久化", async () => {
65
65
  await agent.ready();
66
66
  await agent.dispose();
67
67
  } finally {
68
- fs.rmSync(project_root, { recursive: true, force: true });
68
+ fs.rmSync(project_root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
69
69
  }
70
70
  });
71
71
 
@@ -96,6 +96,6 @@ test("项目初始化只创建 .env、Skills 和运行目录并返回真实结
96
96
  assert.equal(second.skippedFiles.includes(".agents/skills/"), true);
97
97
  assert.equal(second.skippedFiles.includes(".downcity/"), true);
98
98
  } finally {
99
- fs.rmSync(project_root, { recursive: true, force: true });
99
+ fs.rmSync(project_root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
100
100
  }
101
101
  });
@@ -65,3 +65,36 @@ test("RemoteSession reconnects the event pump after transport close", async () =
65
65
  });
66
66
  assert.equal((await second_turn.finished).text, "done");
67
67
  });
68
+
69
+ test("RemoteSession switches models by stable id without local model instances", async () => {
70
+ const updates = [];
71
+ const transport = {
72
+ async set(_session_id, input) {
73
+ updates.push(input);
74
+ return {
75
+ agentId: "agent_test",
76
+ sessionId: "session_test",
77
+ messageCount: 0,
78
+ modelId: input.modelId,
79
+ modelLabel: input.modelId,
80
+ };
81
+ },
82
+ };
83
+ const session = new RemoteSession(transport, {
84
+ agentId: "agent_test",
85
+ sessionId: "session_test",
86
+ messageCount: 0,
87
+ modelId: "old-model",
88
+ modelLabel: "old-model",
89
+ });
90
+
91
+ await session.set({ modelId: "next-model" });
92
+
93
+ assert.deepEqual(updates, [{ modelId: "next-model" }]);
94
+ assert.equal(session.config.modelId, "next-model");
95
+ assert.equal(session.config.modelLabel, "next-model");
96
+ await assert.rejects(
97
+ session.set({ model: { modelId: "local-model", provider: "test" } }),
98
+ /does not accept a local model instance/,
99
+ );
100
+ });
@@ -113,12 +113,15 @@ test("session.set only persists and publishes model-switching actions when model
113
113
  });
114
114
  const events = [];
115
115
  const unsubscribe = session.subscribe((event) => {
116
- if (event.type === "action") events.push(event);
116
+ if (
117
+ event.variant === "message" && event.type === "action"
118
+ ) events.push(event.message);
117
119
  });
118
120
 
119
121
  await session.set({
122
+ modelId: "test-model",
120
123
  model: {
121
- modelId: "test-model",
124
+ modelId: "test-model-label-v1",
122
125
  provider: "test",
123
126
  },
124
127
  });
@@ -126,8 +129,9 @@ test("session.set only persists and publishes model-switching actions when model
126
129
  assert.deepEqual(events, []);
127
130
 
128
131
  await session.set({
132
+ modelId: "test-model",
129
133
  model: {
130
- modelId: "test-model",
134
+ modelId: "test-model-label-v2",
131
135
  provider: "test",
132
136
  },
133
137
  });
@@ -135,26 +139,32 @@ test("session.set only persists and publishes model-switching actions when model
135
139
  assert.deepEqual(events, []);
136
140
 
137
141
  await session.set({
142
+ modelId: "next-test-model",
138
143
  model: {
139
- modelId: "next-test-model",
144
+ modelId: "next-test-model-label",
140
145
  provider: "test",
141
146
  },
142
147
  });
143
148
  unsubscribe();
144
149
 
145
150
  assert.deepEqual(
146
- events.map((event) => `${event.title}:${event.state}`),
147
- ["Switching session model:running", "Session model switched:completed"],
151
+ events.map((event) => `${event.title}:${event.status}`),
152
+ [
153
+ "Switching session model from test-model-label-v2 to next-test-model-label:running",
154
+ "Session model switched from test-model-label-v2 to next-test-model-label:completed",
155
+ ],
148
156
  );
149
- assert.equal(new Set(events.map((event) => event.id)).size, 1);
157
+ assert.equal(new Set(events.map((event) => event.message_id)).size, 1);
150
158
 
151
- const records = await session.records({ view: "timeline" });
152
- const action_events = records.items.filter(
153
- (item) => item.role === "action",
159
+ const messages = await session.messages();
160
+ const action_events = messages.items.filter(
161
+ (item) => item.type === "action",
154
162
  );
155
163
  assert.deepEqual(
156
- action_events.map((event) => `${event.actionTitle}:${event.actionState}`),
157
- ["Session model switched:completed"],
164
+ action_events.map((event) => `${event.title}:${event.status}`),
165
+ [
166
+ "Session model switched from test-model-label-v2 to next-test-model-label:completed",
167
+ ],
158
168
  );
159
169
  } finally {
160
170
  await agent.dispose();
@@ -177,9 +187,9 @@ test("default model initialization does not persist model-switching actions", as
177
187
  const session = await agent.sessions.create({
178
188
  sessionId: "model_init_action_session",
179
189
  });
180
- const records = await session.records({ view: "timeline" });
181
- const action_events = records.items.filter(
182
- (item) => item.role === "action",
190
+ const messages = await session.messages();
191
+ const action_events = messages.items.filter(
192
+ (item) => item.type === "action",
183
193
  );
184
194
  assert.deepEqual(action_events, []);
185
195
  } finally {
@@ -187,6 +197,117 @@ test("default model initialization does not persist model-switching actions", as
187
197
  }
188
198
  });
189
199
 
200
+ test("session model id persists and restores through the host resolver", async () => {
201
+ const agent_path = await fs.mkdtemp(
202
+ path.join(os.tmpdir(), "downcity-agent-session-model-id-"),
203
+ );
204
+ const resolved_model_ids = [];
205
+ const resolve_model = async (model_id) => {
206
+ resolved_model_ids.push(model_id);
207
+ return {
208
+ modelId: model_id,
209
+ provider: "test",
210
+ };
211
+ };
212
+ const first_agent = new Agent({
213
+ id: "session_model_id_agent",
214
+ path: agent_path,
215
+ model: await resolve_model("default-model"),
216
+ model_id: "default-model",
217
+ resolve_model,
218
+ });
219
+ try {
220
+ const session = await first_agent.sessions.create({
221
+ sessionId: "persisted-model-session",
222
+ });
223
+ await session.set({ modelId: "session-model" });
224
+ const info = await session.get_info();
225
+ assert.equal(info.modelId, "session-model");
226
+ assert.equal(session.config.modelId, "session-model");
227
+ } finally {
228
+ await first_agent.dispose();
229
+ }
230
+
231
+ const restored_agent = new Agent({
232
+ id: "session_model_id_agent",
233
+ path: agent_path,
234
+ model: await resolve_model("other-default-model"),
235
+ model_id: "other-default-model",
236
+ resolve_model,
237
+ });
238
+ try {
239
+ const restored_session = await restored_agent.sessions.get(
240
+ "persisted-model-session",
241
+ );
242
+ assert.equal(restored_session.config.modelId, "session-model");
243
+ assert.equal(restored_session.config.model.modelId, "session-model");
244
+ assert.equal(resolved_model_ids.at(-1), "session-model");
245
+ } finally {
246
+ await restored_agent.dispose();
247
+ }
248
+ });
249
+
250
+ test("runtime restoration does not publish a model switch when model id is unchanged", async () => {
251
+ const agent_path = await fs.mkdtemp(
252
+ path.join(os.tmpdir(), "downcity-agent-session-model-runtime-restore-"),
253
+ );
254
+ const first_agent = new Agent({
255
+ id: "session_model_runtime_restore_agent",
256
+ path: agent_path,
257
+ model: {
258
+ modelId: "default-model",
259
+ provider: "test",
260
+ },
261
+ model_id: "default-model",
262
+ resolve_model: async (model_id) => ({
263
+ modelId: model_id,
264
+ provider: "test",
265
+ }),
266
+ });
267
+ try {
268
+ const session = await first_agent.sessions.create({
269
+ sessionId: "runtime-restored-model-session",
270
+ });
271
+ await session.set({ modelId: "session-model" });
272
+ } finally {
273
+ await first_agent.dispose();
274
+ }
275
+
276
+ let resolve_count = 0;
277
+ const restored_agent = new Agent({
278
+ id: "session_model_runtime_restore_agent",
279
+ path: agent_path,
280
+ model: {
281
+ modelId: "other-default-model",
282
+ provider: "test",
283
+ },
284
+ model_id: "other-default-model",
285
+ resolve_model: async (model_id) => {
286
+ resolve_count += 1;
287
+ return {
288
+ modelId: model_id,
289
+ provider: "test",
290
+ };
291
+ },
292
+ });
293
+ try {
294
+ const session_port = restored_agent
295
+ .getContext()
296
+ .session.get("runtime-restored-model-session");
297
+ const events = [];
298
+ const unsubscribe = session_port.subscribe((event) => {
299
+ if (event.type === "action") events.push(event);
300
+ });
301
+ await session_port.stop();
302
+ unsubscribe();
303
+
304
+ assert.equal(resolve_count, 1);
305
+ assert.deepEqual(events, []);
306
+ } finally {
307
+ await restored_agent.dispose();
308
+ }
309
+ });
310
+
190
311
  test("session.fork persists and publishes history-forking actions on source session", async () => {
191
312
  const agent_path = await fs.mkdtemp(
192
313
  path.join(os.tmpdir(), "downcity-agent-history-forking-"),
@@ -209,32 +330,32 @@ test("session.fork persists and publishes history-forking actions on source sess
209
330
 
210
331
  const events = [];
211
332
  const unsubscribe = session.subscribe((event) => {
212
- if (event.type === "action") {
213
- events.push(event);
214
- }
333
+ if (
334
+ event.variant === "message" && event.type === "action"
335
+ ) events.push(event.message);
215
336
  });
216
337
  const forked = await session.fork();
217
338
  unsubscribe();
218
339
 
219
340
  assert.deepEqual(
220
- events.map((event) => `${event.title}:${event.state}`),
341
+ events.map((event) => `${event.title}:${event.status}`),
221
342
  ["Forking session records:running", "Session records forked:completed"],
222
343
  );
223
- assert.equal(new Set(events.map((event) => event.id)).size, 1);
344
+ assert.equal(new Set(events.map((event) => event.message_id)).size, 1);
224
345
  assert.notEqual(forked.id, session.id);
225
346
 
226
- const source_records = await session.records({ view: "timeline" });
227
- const source_fork_actions = source_records.items.filter(
228
- (item) => item.role === "action" && item.actionTitle === "Session records forked",
347
+ const source_messages = await session.messages();
348
+ const source_fork_actions = source_messages.items.filter(
349
+ (item) => item.type === "action" && item.title === "Session records forked",
229
350
  );
230
351
  assert.deepEqual(
231
- source_fork_actions.map((event) => event.actionState),
352
+ source_fork_actions.map((event) => event.status),
232
353
  ["completed"],
233
354
  );
234
355
 
235
- const forked_records = await forked.records({ view: "timeline" });
236
- const forked_model_actions = forked_records.items.filter(
237
- (item) => item.role === "action" && item.actionTitle === "Session model switched",
356
+ const forked_messages = await forked.messages();
357
+ const forked_model_actions = forked_messages.items.filter(
358
+ (item) => item.type === "action" && item.title.startsWith("Session model switched from "),
238
359
  );
239
360
  assert.equal(forked_model_actions.length, 0);
240
361
  } finally {
@@ -141,7 +141,7 @@ test("list_sessions returns persisted title from active session metadata", async
141
141
  }
142
142
  });
143
143
 
144
- test("list_sessions rebuilds stale metadata after external history changes", async () => {
144
+ test("list_sessions reflects canonical Recorder history changes", async () => {
145
145
  const { agent, collection, session } = await create_agent_with_titled_session({
146
146
  tmp_prefix: "downcity-agent-session-summary-repair-",
147
147
  agent_id: "summary_repair_agent",
@@ -162,31 +162,18 @@ test("list_sessions rebuilds stale metadata after external history changes", asy
162
162
  const meta_path = path.join(messages_dir, "meta.json");
163
163
 
164
164
  try {
165
- await fs.appendFile(
166
- messages_path,
167
- `${JSON.stringify({
168
- id: "external_assistant",
169
- role: "assistant",
170
- metadata: {
171
- v: 1,
172
- ts: Date.now(),
173
- sessionId: session.id,
174
- source: "egress",
175
- kind: "normal",
176
- },
177
- parts: [{ type: "text", text: "Externally appended history" }],
178
- })}\n`,
179
- "utf8",
180
- );
165
+ await session.append_assistant_message({
166
+ text: "Recorder appended history",
167
+ });
181
168
 
182
169
  const page = await collection.list();
183
170
  const repaired_meta = JSON.parse(await fs.readFile(meta_path, "utf8"));
184
171
  const history_stat = await fs.stat(messages_path);
185
172
 
186
173
  assert.equal(page.items[0].messageCount, 2);
187
- assert.equal(page.items[0].previewText, "Externally appended history");
174
+ assert.equal(page.items[0].previewText, "Recorder appended history");
188
175
  assert.equal(repaired_meta.messageCount, 2);
189
- assert.equal(repaired_meta.previewText, "Externally appended history");
176
+ assert.equal(repaired_meta.previewText, "Recorder appended history");
190
177
  assert.equal(repaired_meta.historyBytes, history_stat.size);
191
178
  } finally {
192
179
  await agent.dispose();
@@ -76,7 +76,6 @@ async function isSettled(promise) {
76
76
  test("SessionPromptRuntime merges queued prompts at the next step boundary", async () => {
77
77
  const events = [];
78
78
  const persisted = [];
79
- const steer_actions = [];
80
79
  const merge_lifecycle = [];
81
80
  const executionFinished = createDeferred();
82
81
  let stepMerge = null;
@@ -92,10 +91,7 @@ test("SessionPromptRuntime merges queued prompts at the next step boundary", asy
92
91
  merge_lifecycle.push(`message:${input.query}`);
93
92
  return message;
94
93
  },
95
- emit_steer_action: async (input) => {
96
- steer_actions.push(input);
97
- merge_lifecycle.push(`action:${input.message.parts[0]?.text}`);
98
- },
94
+ appendErrorMessage: async () => {},
99
95
  executeTurn: async (input) => {
100
96
  stepMerge = input.onStepMerge;
101
97
  await executionFinished.promise;
@@ -126,17 +122,9 @@ test("SessionPromptRuntime merges queued prompts at the next step boundary", asy
126
122
  persisted.map((message) => message.parts[0]?.text),
127
123
  ["first", "second"],
128
124
  );
129
- assert.deepEqual(
130
- steer_actions.map((action) => ({
131
- turn_id: action.turn_id,
132
- text: action.message.parts[0]?.text,
133
- })),
134
- [{ turn_id: firstTurn.id, text: "second" }],
135
- );
136
125
  assert.deepEqual(merge_lifecycle, [
137
126
  "message:first",
138
127
  "message:second",
139
- "action:second",
140
128
  ]);
141
129
 
142
130
  executionFinished.resolve();
@@ -163,7 +151,7 @@ test("SessionPromptRuntime moves unmerged prompts into the next turn", async ()
163
151
  createAndPersistUserMessage: async (input) => {
164
152
  return createUserMessage(input.query, events.length + 1);
165
153
  },
166
- emit_steer_action: async () => {},
154
+ appendErrorMessage: async () => {},
167
155
  executeTurn: async (input) => {
168
156
  const deferred = createDeferred();
169
157
  finishQueue.push({
@@ -221,7 +209,7 @@ test("SessionPromptRuntime stops current turn and cancels unmerged queued prompt
221
209
  createAndPersistUserMessage: async (input) => {
222
210
  return createUserMessage(input.query, events.length + 1);
223
211
  },
224
- emit_steer_action: async () => {},
212
+ appendErrorMessage: async () => {},
225
213
  executeTurn: async (input) => {
226
214
  await new Promise((resolve) => {
227
215
  input.abortSignal.addEventListener("abort", resolve, { once: true });
@@ -278,7 +266,7 @@ test("SessionPromptRuntime does not synthesize assistant text when stopped befor
278
266
  createAndPersistUserMessage: async (input) => {
279
267
  return createUserMessage(input.query, 1);
280
268
  },
281
- emit_steer_action: async () => {},
269
+ appendErrorMessage: async () => {},
282
270
  executeTurn: async (input) => {
283
271
  await new Promise((resolve) => {
284
272
  input.abortSignal.addEventListener("abort", resolve, { once: true });