@downcity/agent 1.1.228 → 1.1.236

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 (221) hide show
  1. package/bin/agent/local/Agent.d.ts +6 -1
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +30 -3
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentSessions.d.ts +23 -0
  6. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  7. package/bin/agent/local/services/AgentSessions.js +44 -0
  8. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  9. package/bin/agent/remote/RemoteSession.d.ts +2 -0
  10. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  11. package/bin/agent/remote/RemoteSession.js +4 -0
  12. package/bin/agent/remote/RemoteSession.js.map +1 -1
  13. package/bin/agent/remote/RemoteTransport.d.ts +2 -0
  14. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  15. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
  16. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  17. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -7
  18. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  19. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
  20. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  21. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
  22. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  23. package/bin/executor/Executor.d.ts +43 -0
  24. package/bin/executor/Executor.d.ts.map +1 -1
  25. package/bin/executor/Executor.js +127 -7
  26. package/bin/executor/Executor.js.map +1 -1
  27. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +10 -0
  28. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  29. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -10
  30. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  31. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -34
  32. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  33. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
  34. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  35. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
  36. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  37. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
  38. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
  39. package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
  40. package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
  41. package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
  42. package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
  43. package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
  44. package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
  45. package/bin/executor/core-engine/CoreEngineRunner.d.ts +15 -0
  46. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  47. package/bin/executor/core-engine/CoreEngineRunner.js +150 -35
  48. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  49. package/bin/executor/store/history/SessionHistoryStore.d.ts +7 -23
  50. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  51. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  52. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -4
  53. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  54. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  55. package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
  56. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  57. package/bin/executor/types/SessionRun.d.ts +8 -0
  58. package/bin/executor/types/SessionRun.d.ts.map +1 -1
  59. package/bin/index.d.ts +3 -2
  60. package/bin/index.d.ts.map +1 -1
  61. package/bin/index.js +2 -2
  62. package/bin/index.js.map +1 -1
  63. package/bin/model/CityModelAdapter.d.ts +6 -0
  64. package/bin/model/CityModelAdapter.d.ts.map +1 -1
  65. package/bin/model/CityModelAdapter.js +13 -0
  66. package/bin/model/CityModelAdapter.js.map +1 -1
  67. package/bin/plugin/core/PluginLocalExecution.js +3 -3
  68. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  69. package/bin/plugin/core/PluginRegistry.d.ts +41 -2
  70. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  71. package/bin/plugin/core/PluginRegistry.js +145 -6
  72. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  73. package/bin/rpc/Client.d.ts +4 -0
  74. package/bin/rpc/Client.d.ts.map +1 -1
  75. package/bin/rpc/Client.js +11 -0
  76. package/bin/rpc/Client.js.map +1 -1
  77. package/bin/session/Session.d.ts +15 -1
  78. package/bin/session/Session.d.ts.map +1 -1
  79. package/bin/session/Session.js +83 -11
  80. package/bin/session/Session.js.map +1 -1
  81. package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
  82. package/bin/session/SessionSystemBuilder.js +3 -1
  83. package/bin/session/SessionSystemBuilder.js.map +1 -1
  84. package/bin/session/browse/Browse.d.ts +0 -8
  85. package/bin/session/browse/Browse.d.ts.map +1 -1
  86. package/bin/session/browse/Browse.js +72 -57
  87. package/bin/session/browse/Browse.js.map +1 -1
  88. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
  89. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  90. package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
  91. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  92. package/bin/session/recorder/SessionRecorder.d.ts +20 -2
  93. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  94. package/bin/session/recorder/SessionRecorder.js +91 -46
  95. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  96. package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
  97. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  98. package/bin/session/recorder/SessionRecorderCompaction.js +118 -88
  99. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  100. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
  101. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  102. package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
  103. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  104. package/bin/session/runtime/SessionPromptRuntime.d.ts +21 -1
  105. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  106. package/bin/session/runtime/SessionPromptRuntime.js +65 -8
  107. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  108. package/bin/session/services/SessionStateService.d.ts +19 -1
  109. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  110. package/bin/session/services/SessionStateService.js +70 -40
  111. package/bin/session/services/SessionStateService.js.map +1 -1
  112. package/bin/session/services/SessionTurnService.d.ts +15 -0
  113. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  114. package/bin/session/services/SessionTurnService.js +104 -7
  115. package/bin/session/services/SessionTurnService.js.map +1 -1
  116. package/bin/session/services/SessionViewService.d.ts +1 -5
  117. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  118. package/bin/session/services/SessionViewService.js +4 -34
  119. package/bin/session/services/SessionViewService.js.map +1 -1
  120. package/bin/session/storage/Paths.d.ts +3 -15
  121. package/bin/session/storage/Paths.d.ts.map +1 -1
  122. package/bin/session/storage/Paths.js +5 -21
  123. package/bin/session/storage/Paths.js.map +1 -1
  124. package/bin/types/agent/SessionActor.d.ts +8 -0
  125. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  126. package/bin/types/agent/SessionTypes.d.ts +2 -24
  127. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  128. package/bin/types/executor/SessionRunContext.d.ts +29 -0
  129. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  130. package/bin/types/plugin/PluginRuntime.d.ts +59 -2
  131. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  132. package/bin/types/plugin/PluginState.d.ts +10 -0
  133. package/bin/types/plugin/PluginState.d.ts.map +1 -1
  134. package/bin/types/rpc/RpcProtocol.d.ts +10 -0
  135. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  136. package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
  137. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  138. package/bin/types/runtime/agent/AgentContext.js +29 -6
  139. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  140. package/bin/types/session/SessionLocalState.d.ts +8 -0
  141. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  142. package/bin/types/session/SessionMessage.d.ts +16 -12
  143. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  144. package/bin/types/session/SessionMutation.d.ts +1 -1
  145. package/bin/types/session/SessionMutation.js +1 -1
  146. package/bin/types/session/SessionOptions.d.ts +16 -0
  147. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  148. package/bin/types/session/SessionQueueCommand.d.ts +68 -0
  149. package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
  150. package/bin/types/session/SessionQueueCommand.js +10 -0
  151. package/bin/types/session/SessionQueueCommand.js.map +1 -0
  152. package/bin/types/session/SessionSegment.d.ts +57 -0
  153. package/bin/types/session/SessionSegment.d.ts.map +1 -0
  154. package/bin/types/session/SessionSegment.js +8 -0
  155. package/bin/types/session/SessionSegment.js.map +1 -0
  156. package/bin/utils/logger/Logger.d.ts +7 -9
  157. package/bin/utils/logger/Logger.d.ts.map +1 -1
  158. package/bin/utils/logger/Logger.js +11 -9
  159. package/bin/utils/logger/Logger.js.map +1 -1
  160. package/package.json +3 -3
  161. package/scripts/city-model-tool-loop.test.mjs +32 -0
  162. package/scripts/core-engine-context-compaction.test.mjs +411 -0
  163. package/scripts/logger-isolation.test.mjs +55 -0
  164. package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
  165. package/scripts/plugin-tool-bridge.test.mjs +52 -0
  166. package/scripts/remote-session-reconnect.test.mjs +18 -0
  167. package/scripts/session-action-message.test.mjs +59 -13
  168. package/scripts/session-compaction-context-window.test.mjs +96 -0
  169. package/scripts/session-config-turn-boundary.test.mjs +278 -0
  170. package/scripts/session-list-title.test.mjs +21 -3
  171. package/scripts/session-prompt-runtime.test.mjs +93 -0
  172. package/scripts/session-recorder.test.mjs +335 -15
  173. package/src/agent/local/Agent.ts +34 -3
  174. package/src/agent/local/services/AgentSessions.ts +61 -0
  175. package/src/agent/remote/RemoteSession.ts +5 -0
  176. package/src/agent/remote/RemoteTransport.ts +2 -0
  177. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -5
  178. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
  179. package/src/executor/Executor.ts +161 -7
  180. package/src/executor/composer/compaction/SessionCompactionComposer.ts +12 -0
  181. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -55
  182. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
  183. package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
  184. package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
  185. package/src/executor/core-engine/CoreEngineRunner.ts +211 -43
  186. package/src/executor/store/history/SessionHistoryStore.ts +7 -26
  187. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -4
  188. package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
  189. package/src/executor/types/SessionRun.ts +9 -0
  190. package/src/index.ts +9 -1
  191. package/src/model/CityModelAdapter.ts +15 -0
  192. package/src/plugin/core/PluginLocalExecution.ts +3 -3
  193. package/src/plugin/core/PluginRegistry.ts +187 -6
  194. package/src/rpc/Client.ts +12 -0
  195. package/src/session/Session.ts +97 -11
  196. package/src/session/SessionSystemBuilder.ts +4 -1
  197. package/src/session/browse/Browse.ts +73 -69
  198. package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
  199. package/src/session/recorder/SessionRecorder.ts +116 -49
  200. package/src/session/recorder/SessionRecorderCompaction.ts +131 -108
  201. package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
  202. package/src/session/runtime/SessionPromptRuntime.ts +86 -10
  203. package/src/session/services/SessionStateService.ts +89 -45
  204. package/src/session/services/SessionTurnService.ts +112 -7
  205. package/src/session/services/SessionViewService.ts +4 -50
  206. package/src/session/storage/Paths.ts +5 -38
  207. package/src/types/agent/SessionActor.ts +9 -0
  208. package/src/types/agent/SessionTypes.ts +2 -24
  209. package/src/types/executor/SessionRunContext.ts +34 -0
  210. package/src/types/plugin/PluginRuntime.ts +63 -2
  211. package/src/types/plugin/PluginState.ts +10 -0
  212. package/src/types/rpc/RpcProtocol.ts +11 -0
  213. package/src/types/runtime/agent/AgentContext.ts +32 -7
  214. package/src/types/session/SessionLocalState.ts +9 -0
  215. package/src/types/session/SessionMessage.ts +16 -12
  216. package/src/types/session/SessionMutation.ts +1 -1
  217. package/src/types/session/SessionOptions.ts +21 -0
  218. package/src/types/session/SessionQueueCommand.ts +78 -0
  219. package/src/types/session/SessionSegment.ts +62 -0
  220. package/src/utils/logger/Logger.ts +10 -10
  221. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @file 验证多个 Agent 经 session.prompt 执行 plugin_call 时保持 registry 隔离。
3
+ *
4
+ * 关键点(中文)
5
+ * - 先创建 Agent A,再创建 Agent B,锁住历史全局 runtime 的覆盖顺序。
6
+ * - 两个 Session 并发进入模型 tool loop,验证异步作用域不会造成 registry 串线。
7
+ * - 第二次模型请求必须收到各自 plugin action 返回的 owner。
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import http from "node:http";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import fs from "node:fs/promises";
16
+
17
+ import { Agent } from "../bin/index.js";
18
+ import {
19
+ createAction,
20
+ createPlugin,
21
+ } from "../bin/plugin/core/PluginActionFactory.js";
22
+ import { CITY_MODEL_INVOKER, CITY_MODEL_KIND } from "@downcity/type";
23
+
24
+ /**
25
+ * 写入一组 OpenAI-compatible SSE chunk。
26
+ */
27
+ function write_openai_sse(response, chunks) {
28
+ response.writeHead(200, {
29
+ "content-type": "text/event-stream; charset=utf-8",
30
+ "cache-control": "no-cache",
31
+ connection: "keep-alive",
32
+ });
33
+ for (const chunk of chunks) {
34
+ const payload = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
35
+ response.write(`data: ${payload}\n\n`);
36
+ }
37
+ response.end();
38
+ }
39
+
40
+ /**
41
+ * 读取 HTTP JSON 请求体。
42
+ */
43
+ async function read_json_body(request) {
44
+ const raw = await new Promise((resolve, reject) => {
45
+ let data = "";
46
+ request.on("data", (chunk) => {
47
+ data += chunk;
48
+ });
49
+ request.on("end", () => resolve(data));
50
+ request.on("error", reject);
51
+ });
52
+ return JSON.parse(String(raw || "{}"));
53
+ }
54
+
55
+ /**
56
+ * 返回普通文本模型流。
57
+ */
58
+ function write_text_response(response, input) {
59
+ write_openai_sse(response, [
60
+ {
61
+ id: input.id,
62
+ object: "chat.completion.chunk",
63
+ created: 1,
64
+ model: input.model,
65
+ choices: [
66
+ {
67
+ index: 0,
68
+ delta: { role: "assistant", content: input.text },
69
+ finish_reason: null,
70
+ },
71
+ ],
72
+ },
73
+ {
74
+ id: input.id,
75
+ object: "chat.completion.chunk",
76
+ created: 1,
77
+ model: input.model,
78
+ choices: [
79
+ {
80
+ index: 0,
81
+ delta: {},
82
+ finish_reason: "stop",
83
+ },
84
+ ],
85
+ },
86
+ "[DONE]",
87
+ ]);
88
+ }
89
+
90
+ /**
91
+ * 返回一次 plugin_call tool call。
92
+ */
93
+ function write_plugin_call_response(response, model) {
94
+ write_openai_sse(response, [
95
+ {
96
+ id: `chatcmpl_${model}_tool`,
97
+ object: "chat.completion.chunk",
98
+ created: 1,
99
+ model,
100
+ choices: [
101
+ {
102
+ index: 0,
103
+ delta: { role: "assistant" },
104
+ finish_reason: null,
105
+ },
106
+ ],
107
+ },
108
+ {
109
+ id: `chatcmpl_${model}_tool`,
110
+ object: "chat.completion.chunk",
111
+ created: 1,
112
+ model,
113
+ choices: [
114
+ {
115
+ index: 0,
116
+ delta: {
117
+ tool_calls: [
118
+ {
119
+ index: 0,
120
+ id: `call_${model}`,
121
+ type: "function",
122
+ function: {
123
+ name: "plugin_call",
124
+ arguments: JSON.stringify({
125
+ plugin: "skill",
126
+ action: "lookup",
127
+ payload: {},
128
+ }),
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ finish_reason: null,
134
+ },
135
+ ],
136
+ },
137
+ {
138
+ id: `chatcmpl_${model}_tool`,
139
+ object: "chat.completion.chunk",
140
+ created: 1,
141
+ model,
142
+ choices: [
143
+ {
144
+ index: 0,
145
+ delta: {},
146
+ finish_reason: "tool_calls",
147
+ },
148
+ ],
149
+ },
150
+ "[DONE]",
151
+ ]);
152
+ }
153
+
154
+ /**
155
+ * 创建绑定测试 HTTP 服务的 CityModel。
156
+ */
157
+ function create_test_model(base_url, model_id) {
158
+ return Object.freeze({
159
+ id: model_id,
160
+ name: model_id,
161
+ description: "Multi-agent plugin isolation model",
162
+ modalities: ["text", "stream"],
163
+ tags: [],
164
+ meta: {},
165
+ kind: CITY_MODEL_KIND,
166
+ [CITY_MODEL_INVOKER]: {
167
+ connection: () => ({
168
+ base_url,
169
+ api_key: "test_key",
170
+ model_id,
171
+ }),
172
+ },
173
+ });
174
+ }
175
+
176
+ /**
177
+ * 创建返回固定 owner 的 skill plugin。
178
+ */
179
+ function create_owner_plugin(owner, executed_owners) {
180
+ return createPlugin({
181
+ name: "skill",
182
+ title: `Skill ${owner}`,
183
+ description: "Return the owning Agent id",
184
+ actions: {
185
+ lookup: createAction({
186
+ description: "Return registry owner",
187
+ execute: async () => {
188
+ executed_owners.push(owner);
189
+ return {
190
+ success: true,
191
+ data: { owner },
192
+ message: owner,
193
+ };
194
+ },
195
+ }),
196
+ },
197
+ });
198
+ }
199
+
200
+ test("multiple session prompts use only their owning Agent plugin registry", async () => {
201
+ const model_requests = new Map();
202
+ const executed_owners = [];
203
+ const server = http.createServer(async (request, response) => {
204
+ const url = new URL(String(request.url || "/"), "http://127.0.0.1");
205
+ if (
206
+ request.method !== "POST" ||
207
+ url.pathname !== "/v1/ai/chat/completions"
208
+ ) {
209
+ response.writeHead(404);
210
+ response.end("not found");
211
+ return;
212
+ }
213
+
214
+ const body = await read_json_body(request);
215
+ const model = String(body.model || "");
216
+ if (!Array.isArray(body.tools)) {
217
+ write_text_response(response, {
218
+ id: `chatcmpl_${model}_title`,
219
+ model,
220
+ text: `Title ${model}`,
221
+ });
222
+ return;
223
+ }
224
+
225
+ const requests = model_requests.get(model) || [];
226
+ requests.push(body);
227
+ model_requests.set(model, requests);
228
+ if (requests.length === 1) {
229
+ write_plugin_call_response(response, model);
230
+ return;
231
+ }
232
+ write_text_response(response, {
233
+ id: `chatcmpl_${model}_done`,
234
+ model,
235
+ text: "done",
236
+ });
237
+ });
238
+
239
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
240
+ const address = server.address();
241
+ assert.ok(address && typeof address === "object");
242
+ const base_url = `http://127.0.0.1:${String(address.port)}/v1/ai`;
243
+ const root_a = await fs.mkdtemp(
244
+ path.join(os.tmpdir(), "downcity-plugin-isolation-a-"),
245
+ );
246
+ const root_b = await fs.mkdtemp(
247
+ path.join(os.tmpdir(), "downcity-plugin-isolation-b-"),
248
+ );
249
+ const agent_a = new Agent({
250
+ id: "agent_a",
251
+ path: root_a,
252
+ plugins: [create_owner_plugin("agent_a", executed_owners)],
253
+ model: create_test_model(base_url, "model_a"),
254
+ });
255
+ const agent_b = new Agent({
256
+ id: "agent_b",
257
+ path: root_b,
258
+ plugins: [create_owner_plugin("agent_b", executed_owners)],
259
+ model: create_test_model(base_url, "model_b"),
260
+ });
261
+
262
+ try {
263
+ const session_a = await agent_a.sessions.create({ sessionId: "session_a" });
264
+ const session_b = await agent_b.sessions.create({ sessionId: "session_b" });
265
+ const [turn_a, turn_b] = await Promise.all([
266
+ session_a.prompt({ query: "Call your skill plugin" }),
267
+ session_b.prompt({ query: "Call your skill plugin" }),
268
+ ]);
269
+ const [result_a, result_b] = await Promise.all([
270
+ turn_a.finished,
271
+ turn_b.finished,
272
+ ]);
273
+
274
+ assert.equal(result_a.success, true);
275
+ assert.equal(result_b.success, true);
276
+ assert.deepEqual([...executed_owners].sort(), ["agent_a", "agent_b"]);
277
+ for (const [model, owner] of [
278
+ ["model_a", "agent_a"],
279
+ ["model_b", "agent_b"],
280
+ ]) {
281
+ const requests = model_requests.get(model) || [];
282
+ assert.equal(requests.length, 2);
283
+ assert.match(JSON.stringify(requests[1].messages), new RegExp(owner));
284
+ }
285
+ } finally {
286
+ await Promise.all([agent_a.dispose(), agent_b.dispose()]);
287
+ await new Promise((resolve) => server.close(() => resolve()));
288
+ }
289
+ });
@@ -313,3 +313,55 @@ test("PluginRegistry keeps plugin ready after action business failure", async ()
313
313
  assert.equal(retry.data.loaded, true);
314
314
  assert.equal(registry.status("skill").status, "ready");
315
315
  });
316
+
317
+ test("PluginRegistry delays lifecycle stop until the active execution lease is released", async () => {
318
+ let lifecycle_active = false;
319
+ let stop_count = 0;
320
+ const plugin = createPlugin({
321
+ name: "leased-plugin",
322
+ title: "Leased Plugin",
323
+ description: "Keeps runtime resources alive for an active Session step",
324
+ lifecycle: {
325
+ start: async () => {
326
+ lifecycle_active = true;
327
+ },
328
+ stop: async () => {
329
+ lifecycle_active = false;
330
+ stop_count += 1;
331
+ },
332
+ },
333
+ actions: {
334
+ status: createAction({
335
+ description: "Read lifecycle state",
336
+ execute: async () => ({
337
+ success: lifecycle_active,
338
+ data: { lifecycle_active },
339
+ }),
340
+ }),
341
+ },
342
+ });
343
+ const registry = createAgentPluginRegistry({
344
+ plugins: [plugin],
345
+ plugin_instances: new Map(),
346
+ get_context: () => ({ rootPath: process.cwd() }),
347
+ });
348
+ await registry.startAll();
349
+ const lease = registry.execution_view().acquire();
350
+
351
+ assert.equal(await registry.unregister("leased-plugin"), true);
352
+ assert.equal(registry.has("leased-plugin"), false);
353
+ assert.equal(lifecycle_active, true);
354
+ assert.equal(stop_count, 0);
355
+
356
+ const result = await lease.runAction({
357
+ plugin: "leased-plugin",
358
+ action: "status",
359
+ });
360
+ assert.equal(result.success, true);
361
+ assert.equal(result.data.lifecycle_active, true);
362
+
363
+ await lease.release();
364
+ await lease.release();
365
+ assert.equal(lifecycle_active, false);
366
+ assert.equal(stop_count, 1);
367
+ });
@@ -102,3 +102,21 @@ test("RemoteSession switches models by stable id without local model instances",
102
102
  /does not accept a local model instance/,
103
103
  );
104
104
  });
105
+
106
+ test("RemoteSession queues compact through its transport", async () => {
107
+ const compacted_session_ids = [];
108
+ const transport = {
109
+ async compact(session_id) {
110
+ compacted_session_ids.push(session_id);
111
+ },
112
+ };
113
+ const session = new RemoteSession(transport, {
114
+ agentId: "agent_test",
115
+ sessionId: "session_test",
116
+ messageCount: 0,
117
+ });
118
+
119
+ await session.compact();
120
+
121
+ assert.deepEqual(compacted_session_ids, ["session_test"]);
122
+ });
@@ -17,6 +17,48 @@ import { JsonlSessionHistoryStore } from "../bin/executor/store/history/jsonl/Js
17
17
  import { JsonlSessionHistoryComposer } from "../bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js";
18
18
  import { toSessionTimelineEvents } from "../bin/session/browse/Browse.js";
19
19
  import { Agent } from "../bin/index.js";
20
+ import { MockLanguageModelV3 } from "ai/test";
21
+
22
+ function create_stream_text_result(text) {
23
+ return {
24
+ stream: new ReadableStream({
25
+ start(controller) {
26
+ controller.enqueue({ type: "stream-start", warnings: [] });
27
+ controller.enqueue({ type: "text-start", id: "text_1" });
28
+ controller.enqueue({ type: "text-delta", id: "text_1", delta: text });
29
+ controller.enqueue({ type: "text-end", id: "text_1" });
30
+ controller.enqueue({
31
+ type: "finish",
32
+ finishReason: { unified: "stop", raw: "stop" },
33
+ usage: {
34
+ inputTokens: {
35
+ total: 0,
36
+ noCache: 0,
37
+ cacheRead: 0,
38
+ cacheWrite: 0,
39
+ },
40
+ outputTokens: {
41
+ total: 0,
42
+ text: 0,
43
+ reasoning: 0,
44
+ },
45
+ },
46
+ });
47
+ controller.close();
48
+ },
49
+ }),
50
+ };
51
+ }
52
+
53
+ function create_model(model_id, calls) {
54
+ return new MockLanguageModelV3({
55
+ modelId: model_id,
56
+ doStream: async () => {
57
+ calls.push(model_id);
58
+ return create_stream_text_result("done");
59
+ },
60
+ });
61
+ }
20
62
 
21
63
  test("action record is upserted but filtered from LLM history", async () => {
22
64
  const root_path = await fs.mkdtemp(
@@ -107,6 +149,7 @@ test("session.set only persists and publishes model-switching actions when model
107
149
  id: "model_switching_agent",
108
150
  path: agent_path,
109
151
  });
152
+ const model_calls = [];
110
153
  try {
111
154
  const session = await agent.sessions.create({
112
155
  sessionId: "model_switching_session",
@@ -120,37 +163,40 @@ test("session.set only persists and publishes model-switching actions when model
120
163
 
121
164
  await session.set({
122
165
  modelId: "test-model",
123
- model: {
124
- modelId: "test-model-label-v1",
125
- provider: "test",
126
- },
166
+ model: create_model("test-model-label-v1", model_calls),
127
167
  });
128
168
 
129
169
  assert.deepEqual(events, []);
130
170
 
131
171
  await session.set({
132
172
  modelId: "test-model",
133
- model: {
134
- modelId: "test-model-label-v2",
135
- provider: "test",
136
- },
173
+ model: create_model("test-model-label-v2", model_calls),
137
174
  });
138
175
 
139
176
  assert.deepEqual(events, []);
140
177
 
141
178
  await session.set({
142
179
  modelId: "next-test-model",
143
- model: {
144
- modelId: "next-test-model-label",
145
- provider: "test",
146
- },
180
+ model: create_model("next-test-model-label", model_calls),
147
181
  });
182
+
183
+ assert.deepEqual(events, []);
184
+ assert.deepEqual(model_calls, []);
185
+
186
+ const turn = await session.prompt({ query: "apply queued config" });
187
+ const result = await turn.finished;
148
188
  unsubscribe();
149
189
 
190
+ assert.equal(result.success, true);
191
+ assert.deepEqual(model_calls, [
192
+ "next-test-model-label",
193
+ "next-test-model-label",
194
+ ]);
195
+
150
196
  assert.deepEqual(
151
197
  events.map((event) => `${event.title}:${event.status}`),
152
198
  [
153
- "Switching session model from test-model-label-v2 to next-test-model-label:running",
199
+ "Session model switched from test-model-label-v2 to next-test-model-label:running",
154
200
  "Session model switched from test-model-label-v2 to next-test-model-label:completed",
155
201
  ],
156
202
  );
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @file 验证 CityModel 上下文窗口元数据与 compact 的明确触发语义。
3
+ *
4
+ * 关键点(中文)
5
+ * - CityModel 目录元数据需要在归一化前读取。
6
+ * - compact 不再根据字符/token 预估自动触发。
7
+ * - 只有明确 force 或超限恢复才会调用 history store。
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import test from "node:test";
12
+
13
+ import {
14
+ JsonlSessionCompactionComposer,
15
+ read_agent_model_context_window,
16
+ } from "../bin/index.js";
17
+ import {
18
+ CITY_MODEL_INVOKER,
19
+ CITY_MODEL_KIND,
20
+ } from "@downcity/type";
21
+
22
+ /** 创建仅用于读取公开目录元数据的 CityModel。 */
23
+ function create_city_model(context_window) {
24
+ return {
25
+ id: "context-model",
26
+ name: "Context Model",
27
+ description: "Context window test model",
28
+ context_window,
29
+ modalities: ["text"],
30
+ tags: [],
31
+ meta: {},
32
+ kind: CITY_MODEL_KIND,
33
+ [CITY_MODEL_INVOKER]: {
34
+ connection: () => ({
35
+ base_url: "https://example.com/v1/ai",
36
+ model_id: "context-model",
37
+ }),
38
+ },
39
+ };
40
+ }
41
+
42
+ /** 执行一次策略并返回传给 history store 的 compact 参数。 */
43
+ async function capture_compact_input(options = {}) {
44
+ let compact_input;
45
+ const composer = new JsonlSessionCompactionComposer(options.composer_options);
46
+ await composer.run({
47
+ historyStore: {
48
+ compact: async (input) => {
49
+ compact_input = input;
50
+ return { compacted: false, reason: "captured" };
51
+ },
52
+ },
53
+ model: {},
54
+ system: [],
55
+ retryCount: options.retry_count ?? 0,
56
+ force: true,
57
+ ...(options.context_window !== undefined
58
+ ? { context_window: options.context_window }
59
+ : {}),
60
+ });
61
+ assert.ok(compact_input);
62
+ return compact_input;
63
+ }
64
+
65
+ test("CityModel exposes its configured context window to Agent", () => {
66
+ assert.equal(
67
+ read_agent_model_context_window(create_city_model(256000)),
68
+ 256000,
69
+ );
70
+ })
71
+
72
+ test("compact input carries only an explicit force request", async () => {
73
+ const compact_input = await capture_compact_input({ context_window: 256000 });
74
+ assert.equal(compact_input.force, true);
75
+ assert.equal("maxInputTokensApprox" in compact_input, false);
76
+ assert.equal("compactRatio" in compact_input, false);
77
+ assert.equal("keepLastMessages" in compact_input, false);
78
+ });
79
+
80
+ test("normal composer run does not call history store without force", async () => {
81
+ let called = false;
82
+ const composer = new JsonlSessionCompactionComposer();
83
+ const result = await composer.run({
84
+ historyStore: {
85
+ compact: async () => {
86
+ called = true;
87
+ return { compacted: true };
88
+ },
89
+ },
90
+ model: {},
91
+ system: [],
92
+ retryCount: 0,
93
+ });
94
+ assert.deepEqual(result, { compacted: false, reason: "not_requested" });
95
+ assert.equal(called, false);
96
+ });