@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,411 @@
1
+ /**
2
+ * @file 验证 CoreEngine 基于真实 usage 的触发水位与 part/tool transaction 深度折叠。
3
+ */
4
+
5
+ import assert from "node:assert/strict";
6
+ import test from "node:test";
7
+ import { MockLanguageModelV3 } from "ai/test";
8
+
9
+ import {
10
+ deep_compact_model_messages,
11
+ resolve_model_usage_ratio,
12
+ resolve_model_usage_tokens,
13
+ should_compact_after_usage,
14
+ } from "../bin/executor/core-engine/CoreEngineContextCompaction.js";
15
+ import { CoreEngineRunner } from "../bin/executor/core-engine/CoreEngineRunner.js";
16
+ import { LocalSessionContextComposer } from "../bin/executor/composer/context/LocalSessionContextComposer.js";
17
+
18
+ function create_stream_text_result(text, input_tokens, output_tokens) {
19
+ return {
20
+ stream: new ReadableStream({
21
+ start(controller) {
22
+ controller.enqueue({ type: "stream-start", warnings: [] });
23
+ controller.enqueue({ type: "text-start", id: "text-1" });
24
+ controller.enqueue({ type: "text-delta", id: "text-1", delta: text });
25
+ controller.enqueue({ type: "text-end", id: "text-1" });
26
+ controller.enqueue({
27
+ type: "finish",
28
+ finishReason: { unified: "stop", raw: "stop" },
29
+ usage: {
30
+ inputTokens: {
31
+ total: input_tokens,
32
+ noCache: input_tokens,
33
+ cacheRead: 0,
34
+ cacheWrite: 0,
35
+ },
36
+ outputTokens: {
37
+ total: output_tokens,
38
+ text: output_tokens,
39
+ reasoning: 0,
40
+ },
41
+ },
42
+ });
43
+ controller.close();
44
+ },
45
+ }),
46
+ };
47
+ }
48
+
49
+ function create_runner() {
50
+ const context_composer = new LocalSessionContextComposer({
51
+ sessionId: "compact-runner-session",
52
+ getTools: () => ({}),
53
+ });
54
+ return new CoreEngineRunner({
55
+ history_store: { sessionId: "compact-runner-session" },
56
+ context_composer,
57
+ logger: { log: async () => {} },
58
+ should_compact_on_error: () => false,
59
+ });
60
+ }
61
+
62
+ function create_context_error_runner() {
63
+ const context_composer = new LocalSessionContextComposer({
64
+ sessionId: "compact-runner-session",
65
+ getTools: () => ({}),
66
+ });
67
+ return new CoreEngineRunner({
68
+ history_store: { sessionId: "compact-runner-session" },
69
+ context_composer,
70
+ logger: { log: async () => {} },
71
+ should_compact_on_error: (error) =>
72
+ String(error || "").includes("context length"),
73
+ });
74
+ }
75
+
76
+ function create_run_input(model, messages, context_window = 100) {
77
+ return {
78
+ execute_input: {
79
+ query: "latest request",
80
+ system: [],
81
+ messages,
82
+ tools: {},
83
+ },
84
+ model,
85
+ run_context: {
86
+ sessionId: "compact-runner-session",
87
+ injectedUserMessages: [],
88
+ deferredPersistedUserMessages: [],
89
+ pendingAssistantFileParts: [],
90
+ },
91
+ resolve_step_inputs: async () => ({
92
+ model,
93
+ system: [],
94
+ tools: {},
95
+ context_window,
96
+ }),
97
+ };
98
+ }
99
+
100
+ test("usage 优先读取 totalTokens,并在缺失时回退 input + output", () => {
101
+ assert.equal(
102
+ resolve_model_usage_tokens({
103
+ totalTokens: 95,
104
+ inputTokens: 80,
105
+ outputTokens: 10,
106
+ }),
107
+ 95,
108
+ );
109
+ assert.equal(
110
+ resolve_model_usage_tokens({ inputTokens: 80, outputTokens: 15 }),
111
+ 95,
112
+ );
113
+ assert.equal(resolve_model_usage_tokens({}), null);
114
+ });
115
+
116
+ test("普通调用使用 95% 触发,compact 验收使用 50% 目标", () => {
117
+ assert.equal(resolve_model_usage_ratio({ totalTokens: 94 }, 100), 0.94);
118
+ assert.equal(should_compact_after_usage(0.94, false), false);
119
+ assert.equal(should_compact_after_usage(0.95, false), true);
120
+ assert.equal(should_compact_after_usage(0.5, true), false);
121
+ assert.equal(should_compact_after_usage(0.5001, true), true);
122
+ });
123
+
124
+ test("最终 step 达到 95% 时通过 run result 请求 writer 收口后持久化 compact", async () => {
125
+ const model = new MockLanguageModelV3({
126
+ modelId: "usage-trigger-model",
127
+ doStream: async () => create_stream_text_result("done", 90, 5),
128
+ });
129
+ const result = await create_runner().run(create_run_input(model, [{
130
+ id: "user-1",
131
+ role: "user",
132
+ metadata: {
133
+ v: 1,
134
+ ts: 1,
135
+ sessionId: "compact-runner-session",
136
+ source: "ingress",
137
+ kind: "normal",
138
+ },
139
+ parts: [{ type: "text", text: "latest request" }],
140
+ }]));
141
+ assert.equal(result.success, true);
142
+ assert.equal(result.compact_required, true);
143
+ });
144
+
145
+ test("新的持久化 Summary 只按 50% 水位验收一次", async () => {
146
+ const model = new MockLanguageModelV3({
147
+ modelId: "usage-validation-model",
148
+ doStream: async () => create_stream_text_result("done", 55, 5),
149
+ });
150
+ const runner = create_runner();
151
+ const messages = [{
152
+ id: "summary-1",
153
+ role: "assistant",
154
+ metadata: {
155
+ v: 1,
156
+ ts: 1,
157
+ sessionId: "compact-runner-session",
158
+ source: "compact",
159
+ kind: "summary",
160
+ },
161
+ parts: [{ type: "text", text: "previous checkpoint" }],
162
+ }, {
163
+ id: "user-1",
164
+ role: "user",
165
+ metadata: {
166
+ v: 1,
167
+ ts: 2,
168
+ sessionId: "compact-runner-session",
169
+ source: "ingress",
170
+ kind: "normal",
171
+ },
172
+ parts: [{ type: "text", text: "latest request" }],
173
+ }];
174
+ const first = await runner.run(create_run_input(model, messages));
175
+ const second = await runner.run(create_run_input(model, messages));
176
+ assert.equal(first.compact_required, true);
177
+ assert.equal(second.compact_required, undefined);
178
+ });
179
+
180
+ test("显式 compact 后在下一次 provider 调用前重载 canonical history", async () => {
181
+ const provider_prompts = [];
182
+ const compacted_records = [{
183
+ id: "summary-reloaded",
184
+ role: "assistant",
185
+ metadata: {
186
+ v: 1,
187
+ ts: 2,
188
+ sessionId: "compact-runner-session",
189
+ source: "compact",
190
+ kind: "summary",
191
+ },
192
+ parts: [{ type: "text", text: "compacted checkpoint" }],
193
+ }];
194
+ const context_composer = new LocalSessionContextComposer({
195
+ sessionId: "compact-runner-session",
196
+ getTools: () => ({}),
197
+ });
198
+ const runner = new CoreEngineRunner({
199
+ history_store: {
200
+ sessionId: "compact-runner-session",
201
+ list_records: async () => compacted_records,
202
+ },
203
+ context_composer,
204
+ logger: { log: async () => {} },
205
+ should_compact_on_error: () => false,
206
+ });
207
+ let reload_requested = true;
208
+ const model = new MockLanguageModelV3({
209
+ modelId: "history-reload-model",
210
+ doStream: async (options) => {
211
+ provider_prompts.push(JSON.stringify(options.prompt));
212
+ return create_stream_text_result("done", 20, 5);
213
+ },
214
+ });
215
+ const input = create_run_input(model, [{
216
+ id: "old-user",
217
+ role: "user",
218
+ metadata: {
219
+ v: 1,
220
+ ts: 1,
221
+ sessionId: "compact-runner-session",
222
+ source: "ingress",
223
+ kind: "normal",
224
+ },
225
+ parts: [{ type: "text", text: "history before compact" }],
226
+ }]);
227
+ input.run_context.consume_history_reload = () => {
228
+ const requested = reload_requested;
229
+ reload_requested = false;
230
+ return requested;
231
+ };
232
+
233
+ const result = await runner.run(input);
234
+
235
+ assert.equal(result.success, true);
236
+ assert.equal(provider_prompts.length, 1);
237
+ assert.match(provider_prompts[0], /compacted checkpoint/);
238
+ assert.doesNotMatch(provider_prompts[0], /history before compact/);
239
+ });
240
+
241
+ test("Provider context-length error 在当前 tool-loop 内 deep compact 后重试", async () => {
242
+ let provider_calls = 0;
243
+ const model = new MockLanguageModelV3({
244
+ modelId: "context-error-model",
245
+ doStream: async () => {
246
+ provider_calls += 1;
247
+ if (provider_calls === 1) throw new Error("context length exceeded");
248
+ return create_stream_text_result("recovered", 40, 5);
249
+ },
250
+ });
251
+ const result = await create_context_error_runner().run(
252
+ create_run_input(model, [{
253
+ id: "user-1",
254
+ role: "user",
255
+ metadata: {
256
+ v: 1,
257
+ ts: 1,
258
+ sessionId: "compact-runner-session",
259
+ source: "ingress",
260
+ kind: "normal",
261
+ },
262
+ parts: [{ type: "text", text: "latest request" }],
263
+ }]),
264
+ );
265
+ assert.equal(result.success, true);
266
+ assert.equal(result.compact_required, true);
267
+ assert.equal(provider_calls, 2);
268
+ });
269
+
270
+ test("deep compact 删除 reasoning,并完整保留最新并行 tool transaction", () => {
271
+ const large_output = "tool-output-".repeat(8_000);
272
+ const messages = [
273
+ { role: "user", content: "older request" },
274
+ {
275
+ role: "assistant",
276
+ content: [
277
+ { type: "reasoning", text: "old reasoning".repeat(2_000) },
278
+ {
279
+ type: "tool-call",
280
+ toolCallId: "old-call",
281
+ toolName: "old_tool",
282
+ input: { query: "old" },
283
+ },
284
+ ],
285
+ },
286
+ {
287
+ role: "tool",
288
+ content: [
289
+ {
290
+ type: "tool-result",
291
+ toolCallId: "old-call",
292
+ toolName: "old_tool",
293
+ output: { type: "text", value: large_output },
294
+ },
295
+ ],
296
+ },
297
+ { role: "user", content: "latest request" },
298
+ {
299
+ role: "assistant",
300
+ content: [
301
+ { type: "reasoning", text: "latest reasoning".repeat(2_000) },
302
+ { type: "text", text: "running tools" },
303
+ {
304
+ type: "tool-call",
305
+ toolCallId: "call-a",
306
+ toolName: "tool_a",
307
+ input: { value: "a" },
308
+ },
309
+ {
310
+ type: "tool-call",
311
+ toolCallId: "call-b",
312
+ toolName: "tool_b",
313
+ input: { value: "b" },
314
+ },
315
+ ],
316
+ },
317
+ {
318
+ role: "tool",
319
+ content: [
320
+ {
321
+ type: "tool-result",
322
+ toolCallId: "call-a",
323
+ toolName: "tool_a",
324
+ output: { type: "text", value: large_output },
325
+ },
326
+ {
327
+ type: "tool-result",
328
+ toolCallId: "call-b",
329
+ toolName: "tool_b",
330
+ output: { type: "json", value: { large_output } },
331
+ },
332
+ ],
333
+ },
334
+ ];
335
+
336
+ const compacted = deep_compact_model_messages(messages, 0);
337
+ const serialized = JSON.stringify(compacted);
338
+ assert.equal(serialized.includes("old reasoning"), false);
339
+ assert.equal(serialized.includes("latest reasoning"), false);
340
+ assert.equal(serialized.includes("latest request"), true);
341
+ assert.equal(serialized.includes("call-a"), true);
342
+ assert.equal(serialized.includes("call-b"), true);
343
+ assert.ok(serialized.length < JSON.stringify(messages).length / 2);
344
+
345
+ const tool_call_ids = new Set();
346
+ const tool_result_ids = new Set();
347
+ const active_tool_text = [];
348
+ for (const message of compacted) {
349
+ if (!Array.isArray(message.content)) continue;
350
+ for (const part of message.content) {
351
+ if (part.type === "tool-call") tool_call_ids.add(part.toolCallId);
352
+ if (part.type === "tool-result") tool_result_ids.add(part.toolCallId);
353
+ if (part.type === "tool-call" || part.type === "tool-result") {
354
+ active_tool_text.push(JSON.stringify(part));
355
+ }
356
+ }
357
+ }
358
+ assert.equal(active_tool_text.join("\n").includes("old-call"), false);
359
+ assert.deepEqual([...tool_call_ids].sort(), ["call-a", "call-b"]);
360
+ assert.deepEqual([...tool_result_ids].sort(), ["call-a", "call-b"]);
361
+ });
362
+
363
+ test("单条 assistant 含大量 parts 时也会在消息内部折叠", () => {
364
+ const messages = [{
365
+ role: "assistant",
366
+ content: Array.from({ length: 40 }, (_, index) => ({
367
+ type: "text",
368
+ text: `part-${String(index)}:${"x".repeat(4_000)}`,
369
+ })),
370
+ }];
371
+ const compacted = deep_compact_model_messages(messages, 0);
372
+ assert.equal(compacted.length, 1);
373
+ assert.ok(JSON.stringify(compacted).length < JSON.stringify(messages).length / 4);
374
+ });
375
+
376
+ test("tool approval request/response 与对应 call 一起保留且不产生孤儿", () => {
377
+ const compacted = deep_compact_model_messages([
378
+ { role: "user", content: "approve the operation" },
379
+ {
380
+ role: "assistant",
381
+ content: [
382
+ {
383
+ type: "tool-call",
384
+ toolCallId: "approval-call",
385
+ toolName: "dangerous_tool",
386
+ input: { path: "/tmp/example" },
387
+ },
388
+ {
389
+ type: "tool-approval-request",
390
+ toolCallId: "approval-call",
391
+ approvalId: "approval-1",
392
+ },
393
+ ],
394
+ },
395
+ {
396
+ role: "tool",
397
+ content: [
398
+ {
399
+ type: "tool-approval-response",
400
+ approvalId: "approval-1",
401
+ approved: true,
402
+ providerExecuted: true,
403
+ },
404
+ ],
405
+ },
406
+ ]);
407
+ const serialized = JSON.stringify(compacted);
408
+ assert.equal(serialized.includes('"toolCallId":"approval-call"'), true);
409
+ assert.equal(serialized.includes('"approvalId":"approval-1"'), true);
410
+ assert.equal(serialized.includes('"approved":true'), true);
411
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @file 验证 logger 实例不会跨 workspace 共享落盘目录。
3
+ *
4
+ * 关键点(中文)
5
+ * - 每次 getLogger 调用都应返回独立实例。
6
+ * - 两个实例并发写日志时,只能写入各自项目的 `.downcity/logs`。
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import fs from "node:fs/promises";
14
+
15
+ import { getLogger } from "../bin/index.js";
16
+
17
+ /**
18
+ * 读取项目日志目录内的全部 JSONL 文本。
19
+ */
20
+ async function read_project_logs(project_root) {
21
+ const logs_dir = path.join(project_root, ".downcity", "logs");
22
+ const files = await fs.readdir(logs_dir);
23
+ const jsonl_files = files.filter((file) => file.endsWith(".jsonl"));
24
+ const chunks = await Promise.all(
25
+ jsonl_files.map((file) => fs.readFile(path.join(logs_dir, file), "utf8")),
26
+ );
27
+ return chunks.join("\n");
28
+ }
29
+
30
+ test("getLogger keeps concurrent workspace logs isolated", async () => {
31
+ const root_a = await fs.mkdtemp(
32
+ path.join(os.tmpdir(), "downcity-logger-isolation-a-"),
33
+ );
34
+ const root_b = await fs.mkdtemp(
35
+ path.join(os.tmpdir(), "downcity-logger-isolation-b-"),
36
+ );
37
+ const logger_a = getLogger(root_a);
38
+ const logger_b = getLogger(root_b);
39
+
40
+ assert.notEqual(logger_a, logger_b);
41
+ await Promise.all([
42
+ logger_a.log("info", "workspace_a_only"),
43
+ logger_b.log("info", "workspace_b_only"),
44
+ ]);
45
+ await Promise.all([logger_a.saveAllLogs(), logger_b.saveAllLogs()]);
46
+
47
+ const [logs_a, logs_b] = await Promise.all([
48
+ read_project_logs(root_a),
49
+ read_project_logs(root_b),
50
+ ]);
51
+ assert.match(logs_a, /workspace_a_only/);
52
+ assert.doesNotMatch(logs_a, /workspace_b_only/);
53
+ assert.match(logs_b, /workspace_b_only/);
54
+ assert.doesNotMatch(logs_b, /workspace_a_only/);
55
+ });