@downcity/agent 1.1.232 → 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.
- package/bin/agent/local/Agent.d.ts +1 -1
- package/bin/agent/local/Agent.js +2 -2
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentSessions.d.ts +4 -4
- package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +8 -8
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +2 -0
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +4 -0
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +2 -0
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -0
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/executor/Executor.d.ts +20 -2
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +102 -18
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +6 -0
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -9
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -38
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
- package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts +9 -5
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +146 -35
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/store/history/SessionHistoryStore.d.ts +4 -10
- package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -3
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/types/SessionRun.d.ts +8 -0
- package/bin/executor/types/SessionRun.d.ts.map +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -1
- package/bin/index.js.map +1 -1
- package/bin/plugin/core/PluginLocalExecution.js +3 -3
- package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
- package/bin/plugin/core/PluginRegistry.d.ts +20 -2
- package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
- package/bin/plugin/core/PluginRegistry.js +100 -3
- package/bin/plugin/core/PluginRegistry.js.map +1 -1
- package/bin/rpc/Client.d.ts +4 -0
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +11 -0
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +7 -3
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +26 -19
- package/bin/session/Session.js.map +1 -1
- package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
- package/bin/session/SessionSystemBuilder.js +3 -1
- package/bin/session/SessionSystemBuilder.js.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.js +102 -60
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +11 -7
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +22 -24
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +10 -6
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +29 -20
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +13 -3
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +90 -7
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +8 -0
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +16 -8
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/plugin/PluginRuntime.d.ts +36 -4
- package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
- package/bin/types/plugin/PluginState.d.ts +10 -0
- package/bin/types/plugin/PluginState.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +10 -0
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +2 -2
- package/bin/types/runtime/agent/AgentContext.js +1 -1
- package/bin/types/session/SessionLocalState.d.ts +2 -2
- package/bin/types/session/SessionOptions.d.ts +5 -5
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/session/SessionQueueCommand.d.ts +68 -0
- package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
- package/bin/types/session/SessionQueueCommand.js +10 -0
- package/bin/types/session/SessionQueueCommand.js.map +1 -0
- package/bin/utils/logger/Logger.d.ts +7 -9
- package/bin/utils/logger/Logger.d.ts.map +1 -1
- package/bin/utils/logger/Logger.js +11 -9
- package/bin/utils/logger/Logger.js.map +1 -1
- package/package.json +3 -3
- package/scripts/core-engine-context-compaction.test.mjs +411 -0
- package/scripts/logger-isolation.test.mjs +55 -0
- package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
- package/scripts/plugin-tool-bridge.test.mjs +52 -0
- package/scripts/remote-session-reconnect.test.mjs +18 -0
- package/scripts/session-compaction-context-window.test.mjs +27 -26
- package/scripts/session-config-turn-boundary.test.mjs +80 -26
- package/scripts/session-prompt-runtime.test.mjs +23 -15
- package/scripts/session-recorder.test.mjs +15 -24
- package/src/agent/local/Agent.ts +2 -2
- package/src/agent/local/services/AgentSessions.ts +10 -10
- package/src/agent/remote/RemoteSession.ts +5 -0
- package/src/agent/remote/RemoteTransport.ts +2 -0
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -0
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
- package/src/executor/Executor.ts +111 -19
- package/src/executor/composer/compaction/SessionCompactionComposer.ts +7 -0
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -57
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
- package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
- package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
- package/src/executor/core-engine/CoreEngineRunner.ts +200 -48
- package/src/executor/store/history/SessionHistoryStore.ts +4 -12
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -3
- package/src/executor/types/SessionRun.ts +9 -0
- package/src/index.ts +1 -1
- package/src/plugin/core/PluginLocalExecution.ts +3 -3
- package/src/plugin/core/PluginRegistry.ts +116 -3
- package/src/rpc/Client.ts +12 -0
- package/src/session/Session.ts +28 -20
- package/src/session/SessionSystemBuilder.ts +4 -1
- package/src/session/recorder/SessionRecorderCompaction.ts +113 -65
- package/src/session/runtime/SessionPromptRuntime.ts +31 -46
- package/src/session/services/SessionStateService.ts +34 -25
- package/src/session/services/SessionTurnService.ts +97 -8
- package/src/types/agent/SessionActor.ts +9 -0
- package/src/types/executor/SessionRunContext.ts +17 -8
- package/src/types/plugin/PluginRuntime.ts +38 -4
- package/src/types/plugin/PluginState.ts +10 -0
- package/src/types/rpc/RpcProtocol.ts +11 -0
- package/src/types/runtime/agent/AgentContext.ts +2 -2
- package/src/types/session/SessionLocalState.ts +2 -2
- package/src/types/session/SessionOptions.ts +6 -6
- package/src/types/session/SessionQueueCommand.ts +78 -0
- package/src/utils/logger/Logger.ts +10 -10
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/types/session/SessionConfigMutation.d.ts +0 -68
- package/bin/types/session/SessionConfigMutation.d.ts.map +0 -1
- package/bin/types/session/SessionConfigMutation.js +0 -10
- package/bin/types/session/SessionConfigMutation.js.map +0 -1
- package/src/types/session/SessionConfigMutation.ts +0 -78
|
@@ -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
|
+
});
|