@downcity/agent 1.1.207 → 1.1.210

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 (292) hide show
  1. package/bin/agent/local/Agent.d.ts +3 -3
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +0 -3
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/AgentInstructions.d.ts +2 -1
  6. package/bin/agent/local/AgentInstructions.d.ts.map +1 -1
  7. package/bin/agent/local/AgentInstructions.js +10 -10
  8. package/bin/agent/local/AgentInstructions.js.map +1 -1
  9. package/bin/agent/local/services/AgentAssemblyService.d.ts +3 -8
  10. package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
  11. package/bin/agent/local/services/AgentAssemblyService.js +0 -1
  12. package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
  13. package/bin/agent/local/services/AgentSessions.d.ts +5 -7
  14. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  15. package/bin/agent/local/services/AgentSessions.js +2 -3
  16. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  17. package/bin/agent/remote/RemoteAgent.d.ts +3 -1
  18. package/bin/agent/remote/RemoteAgent.d.ts.map +1 -1
  19. package/bin/agent/remote/RemoteAgent.js.map +1 -1
  20. package/bin/agent/remote/RemoteSession.d.ts +10 -1
  21. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  22. package/bin/agent/remote/RemoteSession.js +18 -0
  23. package/bin/agent/remote/RemoteSession.js.map +1 -1
  24. package/bin/agent/remote/RemoteTransport.d.ts +5 -1
  25. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  26. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +4 -1
  27. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  28. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +7 -0
  29. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  30. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +4 -1
  31. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  32. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +1 -0
  33. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  34. package/bin/executor/Executor.d.ts +0 -16
  35. package/bin/executor/Executor.d.ts.map +1 -1
  36. package/bin/executor/Executor.js +0 -16
  37. package/bin/executor/Executor.js.map +1 -1
  38. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
  39. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  40. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +1 -5
  41. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  42. package/bin/executor/composer/history/SessionHistoryWriter.d.ts +0 -10
  43. package/bin/executor/composer/history/SessionHistoryWriter.d.ts.map +1 -1
  44. package/bin/executor/composer/history/SessionHistoryWriter.js +31 -61
  45. package/bin/executor/composer/history/SessionHistoryWriter.js.map +1 -1
  46. package/bin/executor/composer/system/default/StaticPromptCatalog.d.ts +4 -0
  47. package/bin/executor/composer/system/default/StaticPromptCatalog.d.ts.map +1 -1
  48. package/bin/executor/composer/system/default/StaticPromptCatalog.js +4 -0
  49. package/bin/executor/composer/system/default/StaticPromptCatalog.js.map +1 -1
  50. package/bin/executor/composer/system/default/SystemDomain.d.ts +1 -1
  51. package/bin/executor/composer/system/default/SystemDomain.d.ts.map +1 -1
  52. package/bin/executor/composer/system/default/SystemDomain.js +2 -7
  53. package/bin/executor/composer/system/default/SystemDomain.js.map +1 -1
  54. package/bin/executor/composer/system/default/SystemPromptAssets.d.ts +0 -4
  55. package/bin/executor/composer/system/default/SystemPromptAssets.d.ts.map +1 -1
  56. package/bin/executor/composer/system/default/SystemPromptAssets.js +0 -5
  57. package/bin/executor/composer/system/default/SystemPromptAssets.js.map +1 -1
  58. package/bin/executor/composer/system/default/assets/core.prompt.d.ts +1 -1
  59. package/bin/executor/composer/system/default/assets/core.prompt.d.ts.map +1 -1
  60. package/bin/executor/composer/system/default/assets/core.prompt.js +1 -1
  61. package/bin/executor/composer/system/default/assets/core.prompt.js.map +1 -1
  62. package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -1
  63. package/bin/executor/messages/AssistantFileResource.js +58 -15
  64. package/bin/executor/messages/AssistantFileResource.js.map +1 -1
  65. package/bin/executor/services/ExecutorInflightService.d.ts +0 -5
  66. package/bin/executor/services/ExecutorInflightService.d.ts.map +1 -1
  67. package/bin/executor/services/ExecutorInflightService.js +0 -6
  68. package/bin/executor/services/ExecutorInflightService.js.map +1 -1
  69. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +4 -0
  70. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  71. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +115 -3
  72. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  73. package/bin/executor/types/SessionHistoryMeta.d.ts +6 -0
  74. package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
  75. package/bin/index.d.ts +38 -6
  76. package/bin/index.d.ts.map +1 -1
  77. package/bin/index.js +14 -1
  78. package/bin/index.js.map +1 -1
  79. package/bin/plugin/core/ActionScheduleExecutor.js +1 -1
  80. package/bin/plugin/core/ActionScheduleExecutor.js.map +1 -1
  81. package/bin/plugin/core/BasePlugin.d.ts +7 -9
  82. package/bin/plugin/core/BasePlugin.d.ts.map +1 -1
  83. package/bin/plugin/core/BasePlugin.js +1 -14
  84. package/bin/plugin/core/BasePlugin.js.map +1 -1
  85. package/bin/plugin/core/HookRegistry.d.ts +1 -1
  86. package/bin/plugin/core/HookRegistry.d.ts.map +1 -1
  87. package/bin/plugin/core/PluginActionFactory.d.ts +1 -5
  88. package/bin/plugin/core/PluginActionFactory.d.ts.map +1 -1
  89. package/bin/plugin/core/PluginActionFactory.js +0 -3
  90. package/bin/plugin/core/PluginActionFactory.js.map +1 -1
  91. package/bin/plugin/core/PluginActionRunner.d.ts +6 -4
  92. package/bin/plugin/core/PluginActionRunner.d.ts.map +1 -1
  93. package/bin/plugin/core/PluginActionRunner.js +2 -2
  94. package/bin/plugin/core/PluginActionRunner.js.map +1 -1
  95. package/bin/plugin/core/PluginCatalog.d.ts +2 -1
  96. package/bin/plugin/core/PluginCatalog.d.ts.map +1 -1
  97. package/bin/plugin/core/PluginCatalog.js.map +1 -1
  98. package/bin/plugin/core/PluginCommand.d.ts +1 -1
  99. package/bin/plugin/core/PluginCommand.d.ts.map +1 -1
  100. package/bin/plugin/core/PluginCommand.js.map +1 -1
  101. package/bin/plugin/core/PluginHttpRoutes.d.ts +1 -1
  102. package/bin/plugin/core/PluginHttpRoutes.d.ts.map +1 -1
  103. package/bin/plugin/core/PluginLocalExecution.d.ts +4 -1
  104. package/bin/plugin/core/PluginLocalExecution.d.ts.map +1 -1
  105. package/bin/plugin/core/PluginLocalExecution.js +0 -1
  106. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  107. package/bin/plugin/core/PluginRegistry.d.ts +4 -2
  108. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  109. package/bin/plugin/core/PluginRegistry.js +7 -6
  110. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  111. package/bin/plugin/core/PluginStateController.d.ts +7 -27
  112. package/bin/plugin/core/PluginStateController.d.ts.map +1 -1
  113. package/bin/plugin/core/PluginStateController.js +5 -39
  114. package/bin/plugin/core/PluginStateController.js.map +1 -1
  115. package/bin/plugin/types/PluginApi.d.ts +3 -79
  116. package/bin/plugin/types/PluginApi.d.ts.map +1 -1
  117. package/bin/rpc/Client.d.ts +11 -6
  118. package/bin/rpc/Client.d.ts.map +1 -1
  119. package/bin/rpc/Client.js +2 -0
  120. package/bin/rpc/Client.js.map +1 -1
  121. package/bin/session/Session.d.ts +2 -5
  122. package/bin/session/Session.d.ts.map +1 -1
  123. package/bin/session/Session.js +3 -16
  124. package/bin/session/Session.js.map +1 -1
  125. package/bin/session/SessionSystemBuilder.d.ts +1 -1
  126. package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
  127. package/bin/session/browse/Browse.d.ts +4 -6
  128. package/bin/session/browse/Browse.d.ts.map +1 -1
  129. package/bin/session/browse/Browse.js +71 -71
  130. package/bin/session/browse/Browse.js.map +1 -1
  131. package/bin/session/preview/SessionMessagePreview.d.ts +13 -0
  132. package/bin/session/preview/SessionMessagePreview.d.ts.map +1 -0
  133. package/bin/session/preview/SessionMessagePreview.js +61 -0
  134. package/bin/session/preview/SessionMessagePreview.js.map +1 -0
  135. package/bin/session/services/SessionStateService.d.ts +1 -1
  136. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  137. package/bin/session/services/SessionStateService.js +2 -2
  138. package/bin/session/services/SessionStateService.js.map +1 -1
  139. package/bin/session/services/SessionViewService.d.ts +2 -1
  140. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  141. package/bin/session/services/SessionViewService.js +4 -1
  142. package/bin/session/services/SessionViewService.js.map +1 -1
  143. package/bin/session/storage/Metadata.d.ts.map +1 -1
  144. package/bin/session/storage/Metadata.js +23 -0
  145. package/bin/session/storage/Metadata.js.map +1 -1
  146. package/bin/session/storage/Persistence.d.ts +1 -1
  147. package/bin/session/storage/Persistence.d.ts.map +1 -1
  148. package/bin/session/storage/RuntimeSessionPort.d.ts +0 -12
  149. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  150. package/bin/session/storage/RuntimeSessionPort.js +0 -8
  151. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  152. package/bin/types/plugin/PluginAction.d.ts +0 -10
  153. package/bin/types/plugin/PluginAction.d.ts.map +1 -1
  154. package/bin/types/plugin/PluginCommand.d.ts +2 -3
  155. package/bin/types/plugin/PluginCommand.d.ts.map +1 -1
  156. package/bin/types/plugin/PluginControl.d.ts +74 -0
  157. package/bin/types/plugin/PluginControl.d.ts.map +1 -0
  158. package/bin/types/plugin/PluginControl.js +9 -0
  159. package/bin/types/plugin/PluginControl.js.map +1 -0
  160. package/bin/types/plugin/PluginDefinition.d.ts +1 -1
  161. package/bin/types/plugin/PluginDefinition.d.ts.map +1 -1
  162. package/bin/types/plugin/PluginRuntime.d.ts +1 -3
  163. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  164. package/bin/types/plugin/PluginState.d.ts +3 -16
  165. package/bin/types/plugin/PluginState.d.ts.map +1 -1
  166. package/bin/types/rpc/RpcProtocol.d.ts +3 -3
  167. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  168. package/bin/types/runtime/agent/AgentContext.d.ts +1 -9
  169. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  170. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  171. package/bin/types/session/SessionLocalState.d.ts +1 -1
  172. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  173. package/package.json +4 -7
  174. package/scripts/agent-ready.test.mjs +38 -0
  175. package/scripts/assistant-file-resource.test.mjs +39 -0
  176. package/scripts/city-model-tool-loop.test.mjs +18 -30
  177. package/scripts/image-plugin-job.test.mjs +13 -13
  178. package/scripts/plugin-tool-bridge.test.mjs +2 -3
  179. package/scripts/remote-session-reconnect.test.mjs +67 -0
  180. package/scripts/session-history-writer-error.test.mjs +57 -0
  181. package/scripts/session-list-title.test.mjs +114 -23
  182. package/scripts/session-system-blocks.test.mjs +73 -0
  183. package/src/agent/local/Agent.ts +4 -10
  184. package/src/agent/local/AgentInstructions.ts +11 -11
  185. package/src/agent/local/services/AgentAssemblyService.ts +3 -9
  186. package/src/agent/local/services/AgentSessions.ts +8 -15
  187. package/src/agent/remote/RemoteAgent.ts +5 -4
  188. package/src/agent/remote/RemoteSession.ts +21 -2
  189. package/src/agent/remote/RemoteTransport.ts +6 -2
  190. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +13 -3
  191. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +6 -2
  192. package/src/executor/Executor.ts +0 -23
  193. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +2 -8
  194. package/src/executor/composer/history/SessionHistoryWriter.ts +33 -64
  195. package/src/executor/composer/system/default/StaticPromptCatalog.ts +4 -0
  196. package/src/executor/composer/system/default/SystemDomain.ts +1 -9
  197. package/src/executor/composer/system/default/SystemPromptAssets.ts +0 -6
  198. package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
  199. package/src/executor/composer/system/default/assets/core.prompt.ts.txt +37 -15
  200. package/src/executor/messages/AssistantFileResource.ts +61 -17
  201. package/src/executor/services/ExecutorInflightService.ts +0 -10
  202. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +130 -4
  203. package/src/executor/types/SessionHistoryMeta.ts +6 -0
  204. package/src/index.ts +100 -29
  205. package/src/plugin/core/ActionScheduleExecutor.ts +1 -1
  206. package/src/plugin/core/BasePlugin.ts +8 -22
  207. package/src/plugin/core/HookRegistry.ts +1 -1
  208. package/src/plugin/core/PluginActionFactory.ts +1 -8
  209. package/src/plugin/core/PluginActionRunner.ts +8 -11
  210. package/src/plugin/core/PluginCatalog.ts +2 -5
  211. package/src/plugin/core/PluginCommand.ts +2 -1
  212. package/src/plugin/core/PluginHttpRoutes.ts +1 -1
  213. package/src/plugin/core/PluginLocalExecution.ts +4 -7
  214. package/src/plugin/core/PluginRegistry.ts +10 -10
  215. package/src/plugin/core/PluginStateController.ts +12 -60
  216. package/src/plugin/types/PluginApi.ts +6 -81
  217. package/src/rpc/Client.ts +22 -16
  218. package/src/session/Session.ts +5 -21
  219. package/src/session/SessionSystemBuilder.ts +1 -1
  220. package/src/session/browse/Browse.ts +103 -84
  221. package/src/session/preview/SessionMessagePreview.ts +75 -0
  222. package/src/session/services/SessionStateService.ts +3 -4
  223. package/src/session/services/SessionViewService.ts +6 -6
  224. package/src/session/storage/Metadata.ts +26 -0
  225. package/src/session/storage/Persistence.ts +1 -1
  226. package/src/session/storage/RuntimeSessionPort.ts +0 -20
  227. package/src/types/plugin/PluginAction.ts +0 -10
  228. package/src/types/plugin/PluginCommand.ts +2 -3
  229. package/src/types/plugin/PluginControl.ts +80 -0
  230. package/src/types/plugin/PluginDefinition.ts +1 -1
  231. package/src/types/plugin/PluginRuntime.ts +1 -3
  232. package/src/types/plugin/PluginState.ts +3 -20
  233. package/src/types/rpc/RpcProtocol.ts +4 -4
  234. package/src/types/runtime/agent/AgentContext.ts +1 -9
  235. package/src/types/session/SessionLocalState.ts +1 -1
  236. package/tsconfig.tsbuildinfo +1 -1
  237. package/bin/config/DowncitySchema.d.ts +0 -20
  238. package/bin/config/DowncitySchema.d.ts.map +0 -1
  239. package/bin/config/DowncitySchema.js +0 -160
  240. package/bin/config/DowncitySchema.js.map +0 -1
  241. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +0 -7
  242. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts.map +0 -1
  243. package/bin/executor/composer/system/default/assets/plugin.prompt.js +0 -8
  244. package/bin/executor/composer/system/default/assets/plugin.prompt.js.map +0 -1
  245. package/bin/plugin/core/Activation.d.ts +0 -18
  246. package/bin/plugin/core/Activation.d.ts.map +0 -1
  247. package/bin/plugin/core/Activation.js +0 -21
  248. package/bin/plugin/core/Activation.js.map +0 -1
  249. package/bin/plugin/core/Manager.d.ts +0 -7
  250. package/bin/plugin/core/Manager.d.ts.map +0 -1
  251. package/bin/plugin/core/Manager.js +0 -6
  252. package/bin/plugin/core/Manager.js.map +0 -1
  253. package/bin/plugin/types/Plugin.d.ts +0 -15
  254. package/bin/plugin/types/Plugin.d.ts.map +0 -1
  255. package/bin/plugin/types/Plugin.js +0 -9
  256. package/bin/plugin/types/Plugin.js.map +0 -1
  257. package/bin/plugin/types/PluginLifecycle.d.ts +0 -34
  258. package/bin/plugin/types/PluginLifecycle.d.ts.map +0 -1
  259. package/bin/plugin/types/PluginLifecycle.js +0 -9
  260. package/bin/plugin/types/PluginLifecycle.js.map +0 -1
  261. package/bin/plugin/types/PluginState.d.ts +0 -5
  262. package/bin/plugin/types/PluginState.d.ts.map +0 -1
  263. package/bin/plugin/types/PluginState.js +0 -5
  264. package/bin/plugin/types/PluginState.js.map +0 -1
  265. package/bin/plugin/types/Plugins.d.ts +0 -61
  266. package/bin/plugin/types/Plugins.d.ts.map +0 -1
  267. package/bin/plugin/types/Plugins.js +0 -5
  268. package/bin/plugin/types/Plugins.js.map +0 -1
  269. package/bin/session/index.d.ts +0 -15
  270. package/bin/session/index.d.ts.map +0 -1
  271. package/bin/session/index.js +0 -15
  272. package/bin/session/index.js.map +0 -1
  273. package/bin/types/agent/AgentTypes.d.ts +0 -18
  274. package/bin/types/agent/AgentTypes.d.ts.map +0 -1
  275. package/bin/types/agent/AgentTypes.js +0 -10
  276. package/bin/types/agent/AgentTypes.js.map +0 -1
  277. package/bin/utils/logger/Format.d.ts +0 -11
  278. package/bin/utils/logger/Format.d.ts.map +0 -1
  279. package/bin/utils/logger/Format.js +0 -10
  280. package/bin/utils/logger/Format.js.map +0 -1
  281. package/src/config/DowncitySchema.ts +0 -175
  282. package/src/executor/composer/system/default/assets/plugin.prompt.ts +0 -9
  283. package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +0 -20
  284. package/src/plugin/core/Activation.ts +0 -26
  285. package/src/plugin/core/Manager.ts +0 -17
  286. package/src/plugin/types/Plugin.ts +0 -63
  287. package/src/plugin/types/PluginLifecycle.ts +0 -35
  288. package/src/plugin/types/PluginState.ts +0 -12
  289. package/src/plugin/types/Plugins.ts +0 -67
  290. package/src/session/index.ts +0 -57
  291. package/src/types/agent/AgentTypes.ts +0 -58
  292. package/src/utils/logger/Format.ts +0 -15
@@ -101,6 +101,45 @@ test("materializeAssistantFileParts downloads remote file URLs into resources",
101
101
  }
102
102
  });
103
103
 
104
+ test("materializeAssistantFileParts rejects oversized remote resources before download", async () => {
105
+ const project_root = await fs.mkdtemp(
106
+ path.join(os.tmpdir(), "downcity-agent-assistant-large-resource-"),
107
+ );
108
+ const server = http.createServer((_, response) => {
109
+ response.writeHead(200, {
110
+ "content-type": "application/octet-stream",
111
+ "content-length": String(26 * 1024 * 1024),
112
+ });
113
+ response.end("oversized");
114
+ });
115
+
116
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
117
+ try {
118
+ const address = server.address();
119
+ assert.equal(typeof address, "object");
120
+ assert.ok(address);
121
+ const remote_url = `http://127.0.0.1:${address.port}/large.bin`;
122
+ const parts = await materializeAssistantFileParts({
123
+ projectRoot: project_root,
124
+ parts: [
125
+ {
126
+ type: "file",
127
+ mediaType: "application/octet-stream",
128
+ url: remote_url,
129
+ },
130
+ ],
131
+ });
132
+
133
+ assert.equal(parts[0].url, remote_url);
134
+ const resources_path = path.join(project_root, ".downcity", "resources");
135
+ await assert.rejects(fs.access(resources_path));
136
+ } finally {
137
+ await new Promise((resolve, reject) =>
138
+ server.close((error) => (error ? reject(error) : resolve())),
139
+ );
140
+ }
141
+ });
142
+
104
143
  test("materializeAssistantFileParts resolves relative local file URLs from agent project root", async () => {
105
144
  const project_root = await fs.mkdtemp(
106
145
  path.join(os.tmpdir(), "downcity-agent-assistant-relative-resource-"),
@@ -2,7 +2,8 @@
2
2
  * @file 验证 CityModel 会优先走 OpenAI-compatible LanguageModel 并完成 tool loop。
3
3
  *
4
4
  * 关键点(中文)
5
- * - 这里直接走编译后的 Agent / City 产物,避免测试只覆盖源码级辅助函数。
5
+ * - 这里直接走编译后的 Agent 产物,避免测试只覆盖源码级辅助函数。
6
+ * - CityModel 使用 @downcity/type 的共享协议构造,避免反向依赖 City SDK 实现。
6
7
  * - 重点锁住 CityModel -> LanguageModel -> tool-call -> 本地执行 -> tool-result 回传链路。
7
8
  * - 新路径不应再调用 `/v1/ai/stream`,避免 UIMessage stream 反向适配丢失 finish 语义。
8
9
  */
@@ -15,7 +16,7 @@ import path from "node:path";
15
16
  import fs from "node:fs/promises";
16
17
  import { Agent } from "../bin/index.js";
17
18
  import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
18
- import { City } from "../../city/bin/index.js";
19
+ import { CITY_MODEL_INVOKER, CITY_MODEL_KIND } from "@downcity/type";
19
20
  import { tool } from "ai";
20
21
  import { z } from "zod";
21
22
 
@@ -45,7 +46,6 @@ async function read_json_body(req) {
45
46
  }
46
47
 
47
48
  test("CityModel uses direct LanguageModel path and sends tool result back", async () => {
48
- const requests = [];
49
49
  const agent_requests = [];
50
50
  let stream_requests = 0;
51
51
  let tool_executed = false;
@@ -53,24 +53,6 @@ test("CityModel uses direct LanguageModel path and sends tool result back", asyn
53
53
  const server = http.createServer(async (req, res) => {
54
54
  const url = new URL(String(req.url || "/"), "http://127.0.0.1");
55
55
 
56
- if (req.method === "GET" && url.pathname === "/v1/ai/models") {
57
- res.writeHead(200, { "content-type": "application/json" });
58
- res.end(JSON.stringify({
59
- items: [
60
- {
61
- id: "mock-model",
62
- name: "Mock Model",
63
- description: "mock",
64
- modalities: ["text", "stream"],
65
- tags: [],
66
- meta: {},
67
- env: {},
68
- },
69
- ],
70
- }));
71
- return;
72
- }
73
-
74
56
  if (req.method === "POST" && url.pathname === "/v1/ai/stream") {
75
57
  stream_requests += 1;
76
58
  res.writeHead(500, { "content-type": "application/json" });
@@ -80,7 +62,6 @@ test("CityModel uses direct LanguageModel path and sends tool result back", asyn
80
62
 
81
63
  if (req.method === "POST" && url.pathname === "/v1/ai/chat/completions") {
82
64
  const body = await read_json_body(req);
83
- requests.push(body);
84
65
 
85
66
  if (!Array.isArray(body.tools)) {
86
67
  write_openai_sse(res, [
@@ -229,15 +210,22 @@ test("CityModel uses direct LanguageModel path and sends tool result back", asyn
229
210
  try {
230
211
  const address = server.address();
231
212
  assert.ok(address && typeof address === "object");
232
- const city = new City({
233
- role: "user",
234
- federation_url: `http://127.0.0.1:${String(address.port)}`,
235
- city_id: "city_demo",
236
- user_token: "ub_test",
213
+ const model = Object.freeze({
214
+ id: "mock-model",
215
+ name: "Mock Model",
216
+ description: "mock",
217
+ modalities: ["text", "stream"],
218
+ tags: [],
219
+ meta: {},
220
+ kind: CITY_MODEL_KIND,
221
+ [CITY_MODEL_INVOKER]: {
222
+ connection: () => ({
223
+ base_url: `http://127.0.0.1:${String(address.port)}/v1/ai`,
224
+ api_key: "ub_test",
225
+ model_id: "mock-model",
226
+ }),
227
+ },
237
228
  });
238
- const catalog = await city.ai.listModels();
239
- const model = catalog.get("mock-model");
240
- assert.ok(model);
241
229
 
242
230
  const agent_path = await fs.mkdtemp(
243
231
  path.join(os.tmpdir(), "downcity-agent-city-model-tool-loop-"),
@@ -38,6 +38,7 @@ function create_context(rootPath = process.cwd()) {
38
38
  function create_registry(plugin, rootPath = process.cwd()) {
39
39
  return createAgentPluginRegistry({
40
40
  plugins: [plugin],
41
+ plugin_instances: new Map(),
41
42
  get_context: () => create_context(rootPath),
42
43
  });
43
44
  }
@@ -67,7 +68,7 @@ test("ImagePlugin image_create returns image job", async () => {
67
68
 
68
69
  const result = await plugin.actions.image_create.execute({
69
70
  context: create_context(),
70
- payload: { prompt: "draw" },
71
+ input: { prompt: "draw" },
71
72
  pluginName: "image",
72
73
  actionName: "image_create",
73
74
  });
@@ -104,13 +105,12 @@ test("ImagePlugin models lists image-capable models", async () => {
104
105
 
105
106
  const result = await plugin.actions.models.execute({
106
107
  context: create_context(),
107
- payload: {},
108
+ input: {},
108
109
  pluginName: "image",
109
110
  actionName: "models",
110
111
  });
111
112
 
112
113
  assert.equal(result.success, true);
113
- assert.equal(result.data.default_model_id, "image_1");
114
114
  assert.deepEqual(result.data.items.map((item) => item.id), ["image_1"]);
115
115
  assert.equal(result.data.items[0].meta.provider, "test");
116
116
  });
@@ -153,7 +153,7 @@ test("ImagePlugin image_result reads pending state once", async () => {
153
153
 
154
154
  const result = await plugin.actions.image_result.execute({
155
155
  context: create_context(),
156
- payload: { job_id: "img_1" },
156
+ input: { job_id: "img_1" },
157
157
  pluginName: "image",
158
158
  actionName: "image_result",
159
159
  });
@@ -179,7 +179,7 @@ test("ImagePlugin image_result returns final message when succeeded", async () =
179
179
 
180
180
  const result = await plugin.actions.image_result.execute({
181
181
  context: create_context(),
182
- payload: { job_id: "img_1" },
182
+ input: { job_id: "img_1" },
183
183
  pluginName: "image",
184
184
  actionName: "image_result",
185
185
  });
@@ -202,7 +202,7 @@ test("ImagePlugin image_result reports failed terminal job", async () => {
202
202
 
203
203
  const result = await plugin.actions.image_result.execute({
204
204
  context: create_context(),
205
- payload: { job_id: "img_1" },
205
+ input: { job_id: "img_1" },
206
206
  pluginName: "image",
207
207
  actionName: "image_result",
208
208
  });
@@ -244,7 +244,7 @@ test("ImagePlugin image_create converts local content image paths", async () =>
244
244
 
245
245
  const result = await plugin.actions.image_create.execute({
246
246
  context: create_context(tempDir),
247
- payload: {
247
+ input: {
248
248
  content: [
249
249
  { type: "text", text: "change background" },
250
250
  { type: "image", url: "./input.png" },
@@ -277,7 +277,7 @@ test("ImagePlugin image_create uses content instead of prompt when both exist",
277
277
 
278
278
  const result = await plugin.actions.image_create.execute({
279
279
  context: create_context(),
280
- payload: {
280
+ input: {
281
281
  prompt: "ignore this prompt",
282
282
  content: [{ type: "text", text: "use this content" }],
283
283
  },
@@ -302,7 +302,7 @@ test("ImagePlugin image_create keeps remote content image URLs", async () => {
302
302
 
303
303
  const result = await plugin.actions.image_create.execute({
304
304
  context: create_context(),
305
- payload: {
305
+ input: {
306
306
  content: [
307
307
  { type: "text", text: "use this style" },
308
308
  { type: "image", url: "https://example.com/input.webp", media_type: "image/webp" },
@@ -325,7 +325,7 @@ test("ImagePlugin image_create rejects legacy messages and data URLs", async ()
325
325
 
326
326
  const messages_result = await plugin.actions.image_create.execute({
327
327
  context: create_context(),
328
- payload: {
328
+ input: {
329
329
  messages: [
330
330
  {
331
331
  role: "user",
@@ -342,7 +342,7 @@ test("ImagePlugin image_create rejects legacy messages and data URLs", async ()
342
342
 
343
343
  const data_url_result = await plugin.actions.image_create.execute({
344
344
  context: create_context(),
345
- payload: {
345
+ input: {
346
346
  content: [
347
347
  { type: "text", text: "edit this" },
348
348
  { type: "image", url: "data:image/png;base64,cG5n" },
@@ -386,7 +386,7 @@ test("ImagePlugin image_result polls until terminal when until_done=true", async
386
386
 
387
387
  const result = await plugin.actions.image_result.execute({
388
388
  context: create_context(),
389
- payload: {
389
+ input: {
390
390
  job_id: "img_wait",
391
391
  until_done: true,
392
392
  max_wait_ms: 500,
@@ -414,7 +414,7 @@ test("ImagePlugin image_result returns last status when max_wait_ms elapses", as
414
414
 
415
415
  const result = await plugin.actions.image_result.execute({
416
416
  context: create_context(),
417
- payload: {
417
+ input: {
418
418
  job_id: "img_timeout",
419
419
  until_done: true,
420
420
  max_wait_ms: 30,
@@ -132,7 +132,6 @@ test("invokePluginReadTool returns plugin action metadata", async () => {
132
132
  payload: { prompt: "draw" },
133
133
  },
134
134
  ],
135
- allow_when_disabled: false,
136
135
  has_command: false,
137
136
  has_api: false,
138
137
  },
@@ -303,7 +302,7 @@ test("PluginRegistry keeps plugin ready after action business failure", async ()
303
302
  payload: { name: "missing" },
304
303
  });
305
304
  assert.equal(failed.success, false);
306
- assert.equal(registry.status("skill").state, "ready");
305
+ assert.equal(registry.status("skill").status, "ready");
307
306
 
308
307
  const retry = await registry.runAction({
309
308
  plugin: "skill",
@@ -312,5 +311,5 @@ test("PluginRegistry keeps plugin ready after action business failure", async ()
312
311
  });
313
312
  assert.equal(retry.success, true);
314
313
  assert.equal(retry.data.loaded, true);
315
- assert.equal(registry.status("skill").state, "ready");
314
+ assert.equal(registry.status("skill").status, "ready");
316
315
  });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file 验证 RemoteSession 在事件连接断开后能够重新建立订阅。
3
+ *
4
+ * 关键点(中文)
5
+ * - 断流必须结束当前 pending turn,避免 finished 永久等待。
6
+ * - 下一次 prompt 必须重新调用 transport.subscribe。
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { RemoteSession } from "../bin/agent/remote/RemoteSession.js";
13
+
14
+ test("RemoteSession reconnects the event pump after transport close", async () => {
15
+ const subscriptions = [];
16
+ let prompt_count = 0;
17
+ const transport = {
18
+ async subscribe(input) {
19
+ subscriptions.push(input);
20
+ input.on_ready();
21
+ return { close: async () => {} };
22
+ },
23
+ async prompt() {
24
+ prompt_count += 1;
25
+ return { id: `turn_${prompt_count}` };
26
+ },
27
+ async get_info() {
28
+ throw new Error("unused");
29
+ },
30
+ async stop() {
31
+ return { stopped: false, cancelledQueuedPrompts: 0, reason: "idle" };
32
+ },
33
+ async records() {
34
+ throw new Error("unused");
35
+ },
36
+ async system() {
37
+ throw new Error("unused");
38
+ },
39
+ async fork() {
40
+ throw new Error("unused");
41
+ },
42
+ };
43
+ const session = new RemoteSession(transport, {
44
+ agentId: "agent_test",
45
+ sessionId: "session_test",
46
+ messageCount: 0,
47
+ });
48
+
49
+ const first_turn = await session.prompt({ query: "first" });
50
+ subscriptions[0].on_close(new Error("stream dropped"));
51
+ assert.deepEqual(await first_turn.finished, {
52
+ turnId: "turn_1",
53
+ text: "",
54
+ success: false,
55
+ error: "stream dropped",
56
+ });
57
+
58
+ const second_turn = await session.prompt({ query: "second" });
59
+ assert.equal(subscriptions.length, 2);
60
+ subscriptions[1].on_event({
61
+ type: "turn-finish",
62
+ turnId: second_turn.id,
63
+ text: "done",
64
+ success: true,
65
+ });
66
+ assert.equal((await second_turn.finished).text, "done");
67
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @file 验证 SessionHistoryWriter 不会掩盖事实源写入失败。
3
+ *
4
+ * 关键点(中文)
5
+ * - user 消息写入失败必须向调用方抛出。
6
+ * - assistant inflight 落盘失败也必须向调用方抛出。
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { SessionHistoryWriter } from "../bin/executor/composer/history/SessionHistoryWriter.js";
13
+
14
+ function create_failing_history_store() {
15
+ return {
16
+ userText() {
17
+ throw new Error("unused");
18
+ },
19
+ assistantText() {
20
+ throw new Error("unused");
21
+ },
22
+ async write_record() {
23
+ throw new Error("disk full");
24
+ },
25
+ async finalize_inflight() {
26
+ throw new Error("disk full");
27
+ },
28
+ };
29
+ }
30
+
31
+ test("append_user_message propagates history write failures", async () => {
32
+ const writer = new SessionHistoryWriter({
33
+ sessionId: "writer_error_session",
34
+ getHistoryStore: create_failing_history_store,
35
+ });
36
+
37
+ await assert.rejects(
38
+ writer.append_user_message({
39
+ message: { v: 1, id: "user_1", role: "user", parts: [] },
40
+ }),
41
+ /disk full/,
42
+ );
43
+ });
44
+
45
+ test("append_assistant_message propagates history write failures", async () => {
46
+ const writer = new SessionHistoryWriter({
47
+ sessionId: "writer_error_session",
48
+ getHistoryStore: create_failing_history_store,
49
+ });
50
+
51
+ await assert.rejects(
52
+ writer.append_assistant_message({
53
+ message: { v: 1, id: "assistant_1", role: "assistant", parts: [] },
54
+ }),
55
+ /disk full/,
56
+ );
57
+ });
@@ -19,28 +19,37 @@ import { Agent } from "../bin/index.js";
19
19
  function create_mock_title_model(title_text) {
20
20
  return new MockLanguageModelV3({
21
21
  modelId: "mock-session-list-title-model",
22
- doGenerate: async () => ({
23
- content: [
24
- {
25
- type: "text",
26
- text: title_text,
27
- },
28
- ],
29
- finishReason: "stop",
30
- usage: {
31
- inputTokens: {
32
- total: 0,
33
- noCache: 0,
34
- cacheRead: 0,
35
- cacheWrite: 0,
22
+ doStream: async () => ({
23
+ stream: new ReadableStream({
24
+ start(controller) {
25
+ controller.enqueue({ type: "stream-start", warnings: [] });
26
+ controller.enqueue({ type: "text-start", id: "title" });
27
+ controller.enqueue({
28
+ type: "text-delta",
29
+ id: "title",
30
+ delta: title_text,
31
+ });
32
+ controller.enqueue({ type: "text-end", id: "title" });
33
+ controller.enqueue({
34
+ type: "finish",
35
+ finishReason: { unified: "stop", raw: "stop" },
36
+ usage: {
37
+ inputTokens: {
38
+ total: 0,
39
+ noCache: 0,
40
+ cacheRead: 0,
41
+ cacheWrite: 0,
42
+ },
43
+ outputTokens: {
44
+ total: 0,
45
+ text: 0,
46
+ reasoning: 0,
47
+ },
48
+ },
49
+ });
50
+ controller.close();
36
51
  },
37
- outputTokens: {
38
- total: 0,
39
- text: 0,
40
- reasoning: 0,
41
- },
42
- },
43
- warnings: [],
52
+ }),
44
53
  }),
45
54
  });
46
55
  }
@@ -81,8 +90,38 @@ test("list_sessions returns persisted title from active session metadata", async
81
90
 
82
91
  try {
83
92
  const page = await collection.list();
93
+ const meta = JSON.parse(
94
+ await fs.readFile(
95
+ path.join(
96
+ agent.path,
97
+ ".downcity",
98
+ "agents",
99
+ encodeURIComponent(agent.id),
100
+ "sessions",
101
+ encodeURIComponent(session.id),
102
+ "messages",
103
+ "meta.json",
104
+ ),
105
+ "utf8",
106
+ ),
107
+ );
84
108
 
85
109
  assert.equal(page.total, 1);
110
+ assert.equal(meta.messageCount, 1);
111
+ assert.equal(meta.previewText, "Need the session list to show the generated title");
112
+ const history_stat = await fs.stat(
113
+ path.join(
114
+ agent.path,
115
+ ".downcity",
116
+ "agents",
117
+ encodeURIComponent(agent.id),
118
+ "sessions",
119
+ encodeURIComponent(session.id),
120
+ "messages",
121
+ "messages.jsonl",
122
+ ),
123
+ );
124
+ assert.equal(meta.historyBytes, history_stat.size);
86
125
  assert.deepEqual(
87
126
  page.items.map((item) => ({
88
127
  sessionId: item.sessionId,
@@ -93,7 +132,7 @@ test("list_sessions returns persisted title from active session metadata", async
93
132
  {
94
133
  sessionId: session.id,
95
134
  title: "列表标题",
96
- messageCount: 2,
135
+ messageCount: 1,
97
136
  },
98
137
  ],
99
138
  );
@@ -102,6 +141,58 @@ test("list_sessions returns persisted title from active session metadata", async
102
141
  }
103
142
  });
104
143
 
144
+ test("list_sessions rebuilds stale metadata after external history changes", async () => {
145
+ const { agent, collection, session } = await create_agent_with_titled_session({
146
+ tmp_prefix: "downcity-agent-session-summary-repair-",
147
+ agent_id: "summary_repair_agent",
148
+ session_id: "summary_repair_session",
149
+ title: "摘要修复",
150
+ first_user_text: "Initial history",
151
+ });
152
+ const messages_dir = path.join(
153
+ agent.path,
154
+ ".downcity",
155
+ "agents",
156
+ encodeURIComponent(agent.id),
157
+ "sessions",
158
+ encodeURIComponent(session.id),
159
+ "messages",
160
+ );
161
+ const messages_path = path.join(messages_dir, "messages.jsonl");
162
+ const meta_path = path.join(messages_dir, "meta.json");
163
+
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
+ );
181
+
182
+ const page = await collection.list();
183
+ const repaired_meta = JSON.parse(await fs.readFile(meta_path, "utf8"));
184
+ const history_stat = await fs.stat(messages_path);
185
+
186
+ assert.equal(page.items[0].messageCount, 2);
187
+ assert.equal(page.items[0].previewText, "Externally appended history");
188
+ assert.equal(repaired_meta.messageCount, 2);
189
+ assert.equal(repaired_meta.previewText, "Externally appended history");
190
+ assert.equal(repaired_meta.historyBytes, history_stat.size);
191
+ } finally {
192
+ await agent.dispose();
193
+ }
194
+ });
195
+
105
196
  test("archive_sessions returns title from archived session metadata", async () => {
106
197
  const { agent, collection, session } = await create_agent_with_titled_session({
107
198
  tmp_prefix: "downcity-agent-archive-list-title-",
@@ -131,7 +222,7 @@ test("archive_sessions returns title from archived session metadata", async () =
131
222
  {
132
223
  sessionId: session.id,
133
224
  title: "归档标题",
134
- messageCount: 2,
225
+ messageCount: 1,
135
226
  },
136
227
  ],
137
228
  );
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @file 验证 SDK session system blocks 的稳定分层顺序。
3
+ *
4
+ * 关键点(中文)
5
+ * - 测试编译后的 bin 输出,避免测试文件依赖 TS 源码加载器。
6
+ * - 自定义 instruction 不能替代 Downcity core;core 必须包含 Shell 与 plugin 总规则。
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { createInstructionSystemBlocks } from "../bin/agent/local/AgentInstructions.js";
13
+ import { buildSessionSystemBlocks } from "../bin/session/SessionSystemBuilder.js";
14
+
15
+ test("instruction blocks keep Downcity core after custom instruction", () => {
16
+ const blocks = createInstructionSystemBlocks(
17
+ ["你是这个项目的工程 agent。"],
18
+ "/tmp/downcity-project",
19
+ );
20
+
21
+ assert.deepEqual(
22
+ blocks.map((block) => `${block.source}:${block.name}`),
23
+ ["instruction:agent", "core:default"],
24
+ );
25
+ assert.match(blocks[1].content, /# Downcity Agent/);
26
+ assert.match(blocks[1].content, /# Harness Design/);
27
+ assert.match(blocks[1].content, /human-owned workspace/);
28
+ assert.match(blocks[1].content, /project structure is the control surface/);
29
+ assert.match(blocks[1].content, /# Shell Commands/);
30
+ assert.match(blocks[1].content, /# Plugin System/);
31
+ assert.doesNotMatch(blocks[1].content, /\/tmp\/downcity-project/);
32
+ assert.doesNotMatch(blocks[1].content, /current year/i);
33
+ assert.doesNotMatch(blocks[1].content, /# Project Runtime/);
34
+ assert.doesNotMatch(blocks[1].content, /\.downcity\/agents/);
35
+ assert.doesNotMatch(blocks[1].content, /\.downcity\/memory/);
36
+ assert.doesNotMatch(blocks[1].content, /\.downcity\/public/);
37
+ });
38
+
39
+ test("session system blocks are ordered as instruction, core, plugin, session", async () => {
40
+ const blocks = await buildSessionSystemBlocks({
41
+ agentId: "agent-test",
42
+ projectRoot: "/tmp/downcity-project",
43
+ sessionId: "session-test",
44
+ createdAt: Date.UTC(2026, 6, 9, 8, 0, 0),
45
+ timezone: "Asia/Shanghai",
46
+ getInstructionSystemBlocks: () =>
47
+ createInstructionSystemBlocks(
48
+ ["使用中文回复。"],
49
+ "/tmp/downcity-project",
50
+ ),
51
+ getManagedPluginSystemBlocks: async () => [],
52
+ getPluginSystemBlocks: async () => [
53
+ {
54
+ source: "plugin",
55
+ name: "task",
56
+ content: "# Task Plugin\n\n任务插件说明。",
57
+ },
58
+ ],
59
+ });
60
+
61
+ assert.deepEqual(
62
+ blocks.map((block) => `${block.source}:${block.name}`),
63
+ [
64
+ "instruction:agent",
65
+ "core:default",
66
+ "plugin:task",
67
+ "session:context",
68
+ ],
69
+ );
70
+ assert.equal(blocks[0].content, "使用中文回复。");
71
+ assert.match(blocks[1].content, /plugin_call/);
72
+ assert.match(blocks[3].content, /session-test/);
73
+ });