@downcity/agent 1.1.226 → 1.1.232
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 +6 -1
- package/bin/agent/local/Agent.d.ts.map +1 -1
- package/bin/agent/local/Agent.js +30 -3
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentSessions.d.ts +23 -0
- package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +44 -0
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +0 -7
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/executor/Executor.d.ts +25 -0
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +36 -0
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +4 -0
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +15 -11
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts +11 -0
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +12 -8
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/store/history/SessionHistoryStore.d.ts +3 -13
- package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
- package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
- package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
- package/bin/index.d.ts +2 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -1
- package/bin/index.js.map +1 -1
- package/bin/model/CityModelAdapter.d.ts +6 -0
- package/bin/model/CityModelAdapter.d.ts.map +1 -1
- package/bin/model/CityModelAdapter.js +13 -0
- package/bin/model/CityModelAdapter.js.map +1 -1
- package/bin/plugin/core/PluginRegistry.d.ts +22 -1
- package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
- package/bin/plugin/core/PluginRegistry.js +46 -4
- package/bin/plugin/core/PluginRegistry.js.map +1 -1
- package/bin/session/Session.d.ts +11 -1
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +76 -11
- package/bin/session/Session.js.map +1 -1
- package/bin/session/browse/Browse.d.ts +0 -8
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +72 -57
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
- package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
- package/bin/session/recorder/SessionRecorder.d.ts +30 -2
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +120 -49
- package/bin/session/recorder/SessionRecorder.js.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.js +50 -62
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
- package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +17 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +67 -8
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +15 -1
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +60 -39
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +5 -0
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +17 -3
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts +1 -5
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +4 -34
- package/bin/session/services/SessionViewService.js.map +1 -1
- package/bin/session/storage/Paths.d.ts +3 -15
- package/bin/session/storage/Paths.d.ts.map +1 -1
- package/bin/session/storage/Paths.js +5 -21
- package/bin/session/storage/Paths.js.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +2 -24
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +21 -0
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/plugin/PluginRuntime.d.ts +25 -0
- package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
- package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.js +29 -6
- package/bin/types/runtime/agent/AgentContext.js.map +1 -1
- package/bin/types/session/SessionConfigMutation.d.ts +68 -0
- package/bin/types/session/SessionConfigMutation.d.ts.map +1 -0
- package/bin/types/session/SessionConfigMutation.js +10 -0
- package/bin/types/session/SessionConfigMutation.js.map +1 -0
- package/bin/types/session/SessionLocalState.d.ts +8 -0
- package/bin/types/session/SessionLocalState.d.ts.map +1 -1
- package/bin/types/session/SessionMessage.d.ts +16 -12
- package/bin/types/session/SessionMessage.d.ts.map +1 -1
- package/bin/types/session/SessionMutation.d.ts +1 -1
- package/bin/types/session/SessionMutation.js +1 -1
- package/bin/types/session/SessionOptions.d.ts +16 -0
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/session/SessionSegment.d.ts +57 -0
- package/bin/types/session/SessionSegment.d.ts.map +1 -0
- package/bin/types/session/SessionSegment.js +8 -0
- package/bin/types/session/SessionSegment.js.map +1 -0
- package/package.json +3 -3
- package/scripts/city-model-tool-loop.test.mjs +32 -0
- package/scripts/session-action-message.test.mjs +59 -13
- package/scripts/session-compaction-context-window.test.mjs +95 -0
- package/scripts/session-config-turn-boundary.test.mjs +224 -0
- package/scripts/session-list-title.test.mjs +21 -3
- package/scripts/session-prompt-runtime.test.mjs +85 -0
- package/scripts/session-recorder.test.mjs +379 -11
- package/src/agent/local/Agent.ts +34 -3
- package/src/agent/local/services/AgentSessions.ts +61 -0
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +0 -5
- package/src/executor/Executor.ts +62 -0
- package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -0
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +16 -14
- package/src/executor/core-engine/CoreEngineRunner.ts +24 -8
- package/src/executor/store/history/SessionHistoryStore.ts +3 -14
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -1
- package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
- package/src/index.ts +8 -0
- package/src/model/CityModelAdapter.ts +15 -0
- package/src/plugin/core/PluginRegistry.ts +72 -4
- package/src/session/Session.ts +89 -11
- package/src/session/browse/Browse.ts +73 -69
- package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
- package/src/session/recorder/SessionRecorder.ts +151 -52
- package/src/session/recorder/SessionRecorderCompaction.ts +55 -80
- package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
- package/src/session/runtime/SessionPromptRuntime.ts +100 -9
- package/src/session/services/SessionStateService.ts +79 -44
- package/src/session/services/SessionTurnService.ts +19 -3
- package/src/session/services/SessionViewService.ts +4 -50
- package/src/session/storage/Paths.ts +5 -38
- package/src/types/agent/SessionTypes.ts +2 -24
- package/src/types/executor/SessionRunContext.ts +25 -0
- package/src/types/plugin/PluginRuntime.ts +27 -0
- package/src/types/runtime/agent/AgentContext.ts +32 -7
- package/src/types/session/SessionConfigMutation.ts +78 -0
- package/src/types/session/SessionLocalState.ts +9 -0
- package/src/types/session/SessionMessage.ts +16 -12
- package/src/types/session/SessionMutation.ts +1 -1
- package/src/types/session/SessionOptions.ts +21 -0
- package/src/types/session/SessionSegment.ts +62 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -11,11 +11,12 @@ import path from "node:path";
|
|
|
11
11
|
import { JsonlSessionMessageStore } from "../bin/session/recorder/JsonlSessionMessageStore.js";
|
|
12
12
|
import { SessionRecorder } from "../bin/session/recorder/SessionRecorder.js";
|
|
13
13
|
import { SessionRecorderHistoryStore } from "../bin/session/recorder/SessionRecorderHistoryStore.js";
|
|
14
|
+
import { SessionViewService } from "../bin/session/services/SessionViewService.js";
|
|
14
15
|
import { MockLanguageModelV3 } from "ai/test";
|
|
15
16
|
|
|
16
17
|
async function create_recorder(session_id = "session-recorder-test") {
|
|
17
18
|
const root_path = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-session-recorder-"));
|
|
18
|
-
const file_path = path.join(root_path, "
|
|
19
|
+
const file_path = path.join(root_path, "active.jsonl");
|
|
19
20
|
const assistant_message_file_path = path.join(root_path, "assistant_message.json");
|
|
20
21
|
const events = [];
|
|
21
22
|
const recorder = new SessionRecorder({
|
|
@@ -32,7 +33,45 @@ async function read_jsonl(file_path) {
|
|
|
32
33
|
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
async function create_seeded_recorder(session_id, messages) {
|
|
37
|
+
const root_path = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-session-seeded-"));
|
|
38
|
+
const file_path = path.join(root_path, "active.jsonl");
|
|
39
|
+
await fs.writeFile(
|
|
40
|
+
file_path,
|
|
41
|
+
`${messages.map((message) => JSON.stringify(message)).join("\n")}\n`,
|
|
42
|
+
"utf8",
|
|
43
|
+
);
|
|
44
|
+
const recorder = new SessionRecorder({
|
|
45
|
+
session_id,
|
|
46
|
+
store: new JsonlSessionMessageStore({ session_id, file_path }),
|
|
47
|
+
publish: () => {},
|
|
48
|
+
});
|
|
49
|
+
await recorder.initialize();
|
|
50
|
+
return { recorder, file_path };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function create_seeded_user_message(session_id, sequence) {
|
|
54
|
+
return {
|
|
55
|
+
message_id: `user-${String(sequence)}`,
|
|
56
|
+
session_id,
|
|
57
|
+
turn_id: `turn-${String(sequence)}`,
|
|
58
|
+
sequence,
|
|
59
|
+
revision: 1,
|
|
60
|
+
visibility: "visible",
|
|
61
|
+
created_at: sequence,
|
|
62
|
+
updated_at: sequence,
|
|
63
|
+
type: "user",
|
|
64
|
+
input_type: "prompt",
|
|
65
|
+
parts: [{
|
|
66
|
+
part_id: `user-text-${String(sequence)}`,
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `message ${String(sequence)}`,
|
|
69
|
+
state: "done",
|
|
70
|
+
}],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("delta 只更新 Assistant 草稿,完成后才写入 active.jsonl", async () => {
|
|
36
75
|
const { recorder, events, file_path, assistant_message_file_path } = await create_recorder();
|
|
37
76
|
await recorder.append_user_message({
|
|
38
77
|
turn_id: "turn-1",
|
|
@@ -110,6 +149,45 @@ test("Tool Part 在文本中间创建后,输入、审批和输出都保持原
|
|
|
110
149
|
);
|
|
111
150
|
});
|
|
112
151
|
|
|
152
|
+
test("不同模型 step 重复使用 Text chunk ID 时仍保持真实 Part 顺序", async () => {
|
|
153
|
+
const { recorder, file_path } = await create_recorder("reused-text-id-order-test");
|
|
154
|
+
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
155
|
+
|
|
156
|
+
await writer.apply_chunk({ type: "text-start", id: "txt-0" });
|
|
157
|
+
await writer.apply_chunk({ type: "text-delta", id: "txt-0", delta: "first" });
|
|
158
|
+
await writer.apply_chunk({ type: "text-end", id: "txt-0" });
|
|
159
|
+
await writer.apply_chunk({ type: "tool-input-start", toolCallId: "call-1", toolName: "search" });
|
|
160
|
+
await writer.apply_chunk({
|
|
161
|
+
type: "tool-input-available",
|
|
162
|
+
toolCallId: "call-1",
|
|
163
|
+
toolName: "search",
|
|
164
|
+
input: { query: "first" },
|
|
165
|
+
});
|
|
166
|
+
await writer.apply_chunk({ type: "tool-output-available", toolCallId: "call-1", output: "one" });
|
|
167
|
+
|
|
168
|
+
await writer.apply_chunk({ type: "text-start", id: "txt-0" });
|
|
169
|
+
await writer.apply_chunk({ type: "text-delta", id: "txt-0", delta: "second" });
|
|
170
|
+
await writer.apply_chunk({ type: "text-end", id: "txt-0" });
|
|
171
|
+
await writer.apply_chunk({ type: "tool-input-start", toolCallId: "call-2", toolName: "search" });
|
|
172
|
+
await writer.apply_chunk({
|
|
173
|
+
type: "tool-input-available",
|
|
174
|
+
toolCallId: "call-2",
|
|
175
|
+
toolName: "search",
|
|
176
|
+
input: { query: "second" },
|
|
177
|
+
});
|
|
178
|
+
await writer.apply_chunk({ type: "tool-output-available", toolCallId: "call-2", output: "two" });
|
|
179
|
+
await writer.complete();
|
|
180
|
+
|
|
181
|
+
const assistant = (await read_jsonl(file_path))[0];
|
|
182
|
+
assert.deepEqual(assistant.parts.map((part) => part.type), ["text", "tool", "text", "tool"]);
|
|
183
|
+
assert.deepEqual(assistant.parts.map((part) => part.sequence), [1, 2, 3, 4]);
|
|
184
|
+
assert.deepEqual(
|
|
185
|
+
assistant.parts.filter((part) => part.type === "text").map((part) => part.text),
|
|
186
|
+
["first", "second"],
|
|
187
|
+
);
|
|
188
|
+
assert.notEqual(assistant.parts[0].part_id, assistant.parts[2].part_id);
|
|
189
|
+
});
|
|
190
|
+
|
|
113
191
|
test("空 Text Start 不会抢占后续 Tool 的真实顺序", async () => {
|
|
114
192
|
const { recorder, events, file_path } = await create_recorder("deferred-text-order-test");
|
|
115
193
|
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
@@ -177,7 +255,7 @@ test("重启时将 Assistant 草稿收口为 stopped,并将运行中 Action
|
|
|
177
255
|
});
|
|
178
256
|
await recovered.initialize();
|
|
179
257
|
|
|
180
|
-
const page = await recovered.list_messages(
|
|
258
|
+
const page = await recovered.list_messages();
|
|
181
259
|
assert.deepEqual(page.items.map((message) => message.type), ["user", "assistant", "action"]);
|
|
182
260
|
assert.equal(page.items[1].status, "stopped");
|
|
183
261
|
assert.equal(page.items[1].parts[0].text, "partial");
|
|
@@ -205,7 +283,7 @@ test("Action revision 追加完整快照,读取时只返回最新版本", asyn
|
|
|
205
283
|
assert.equal(page.items[0].title, "Compacted");
|
|
206
284
|
});
|
|
207
285
|
|
|
208
|
-
test("compact
|
|
286
|
+
test("compact 把 Active 前缀关闭为带累计 Summary 的 Segment", async () => {
|
|
209
287
|
const { recorder, file_path } = await create_recorder("compact-test");
|
|
210
288
|
for (let index = 1; index <= 6; index += 1) {
|
|
211
289
|
await recorder.append_user_message({
|
|
@@ -214,7 +292,6 @@ test("compact 追加 internal summary 且不重写已有 Message 快照", async
|
|
|
214
292
|
parts: [{ part_id: `user-${String(index)}`, type: "text", text: `message ${String(index)}`, state: "done" }],
|
|
215
293
|
});
|
|
216
294
|
}
|
|
217
|
-
const before = await fs.readFile(file_path, "utf8");
|
|
218
295
|
const history_store = new SessionRecorderHistoryStore({ session_id: "compact-test", recorder });
|
|
219
296
|
const model = new MockLanguageModelV3({
|
|
220
297
|
modelId: "compact-model",
|
|
@@ -233,14 +310,305 @@ test("compact 追加 internal summary 且不重写已有 Message 快照", async
|
|
|
233
310
|
system: [],
|
|
234
311
|
keepLastMessages: 2,
|
|
235
312
|
maxInputTokensApprox: 1,
|
|
236
|
-
archiveOnCompact: false,
|
|
237
313
|
compactRatio: 0.5,
|
|
238
314
|
});
|
|
239
315
|
assert.equal(result.compacted, true);
|
|
240
|
-
assert.equal((await fs.readFile(file_path, "utf8")).
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
assert.equal(
|
|
244
|
-
assert.equal(
|
|
316
|
+
assert.equal((await fs.readFile(file_path, "utf8")).includes("message 1"), false);
|
|
317
|
+
const active = await recorder.list_messages({ include_internal: true });
|
|
318
|
+
assert.deepEqual(active.items.map((message) => message.sequence), [4, 5, 6]);
|
|
319
|
+
assert.equal(active.source, "active");
|
|
320
|
+
assert.equal(active.has_more, true);
|
|
321
|
+
const segment = await recorder.list_messages({
|
|
322
|
+
before_sequence: active.start_sequence,
|
|
323
|
+
include_internal: true,
|
|
324
|
+
});
|
|
325
|
+
assert.deepEqual(segment.items.map((message) => message.sequence), [1, 2, 3]);
|
|
326
|
+
assert.equal(segment.source, "segment");
|
|
245
327
|
assert.equal((await history_store.list_records()).length, 4);
|
|
328
|
+
await assert.rejects(
|
|
329
|
+
recorder.list_messages({ before_sequence: 0 }),
|
|
330
|
+
/before_sequence must be a positive integer/,
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("重启后从最新 Segment Summary 与 Active 恢复模型上下文", async () => {
|
|
335
|
+
const session_id = "compact-restart-test";
|
|
336
|
+
const { recorder, file_path } = await create_recorder(session_id);
|
|
337
|
+
for (let index = 1; index <= 6; index += 1) {
|
|
338
|
+
await recorder.append_user_message({
|
|
339
|
+
turn_id: `turn-${String(index)}`,
|
|
340
|
+
input_type: "prompt",
|
|
341
|
+
parts: [{
|
|
342
|
+
part_id: `user-${String(index)}`,
|
|
343
|
+
type: "text",
|
|
344
|
+
text: `message ${String(index)}`,
|
|
345
|
+
state: "done",
|
|
346
|
+
}],
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
await recorder.compact_active({
|
|
350
|
+
through_sequence: 4,
|
|
351
|
+
summary: {
|
|
352
|
+
record_type: "summary",
|
|
353
|
+
session_id,
|
|
354
|
+
summary_id: "summary-through-4",
|
|
355
|
+
through_sequence: 4,
|
|
356
|
+
text: "summary through 4",
|
|
357
|
+
created_at: 7,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const restarted = new SessionRecorder({
|
|
362
|
+
session_id,
|
|
363
|
+
store: new JsonlSessionMessageStore({ session_id, file_path }),
|
|
364
|
+
publish: () => {},
|
|
365
|
+
});
|
|
366
|
+
await restarted.initialize();
|
|
367
|
+
const context_store = new SessionRecorderHistoryStore({ session_id, recorder: restarted });
|
|
368
|
+
const context = await context_store.list_records();
|
|
369
|
+
assert.deepEqual(
|
|
370
|
+
context.map((message) => message.parts[0]?.text),
|
|
371
|
+
["summary through 4", "message 5", "message 6"],
|
|
372
|
+
);
|
|
373
|
+
const segment = await restarted.list_messages({ before_sequence: 5 });
|
|
374
|
+
assert.deepEqual(segment.items.map((message) => message.sequence), [1, 2, 3, 4]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("Compact 两步提交中断后会清理 Active 与 Segment 的重叠前缀", async () => {
|
|
378
|
+
const session_id = "compact-overlap-recovery-test";
|
|
379
|
+
const { recorder, file_path } = await create_recorder(session_id);
|
|
380
|
+
for (let index = 1; index <= 6; index += 1) {
|
|
381
|
+
await recorder.append_user_message({
|
|
382
|
+
turn_id: `turn-${String(index)}`,
|
|
383
|
+
input_type: "prompt",
|
|
384
|
+
parts: [{
|
|
385
|
+
part_id: `user-${String(index)}`,
|
|
386
|
+
type: "text",
|
|
387
|
+
text: `message ${String(index)}`,
|
|
388
|
+
state: "done",
|
|
389
|
+
}],
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
await recorder.compact_active({
|
|
393
|
+
through_sequence: 4,
|
|
394
|
+
summary: {
|
|
395
|
+
record_type: "summary",
|
|
396
|
+
session_id,
|
|
397
|
+
summary_id: "summary-through-4",
|
|
398
|
+
through_sequence: 4,
|
|
399
|
+
text: "summary through 4",
|
|
400
|
+
created_at: 7,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const segment_path = path.join(
|
|
405
|
+
path.dirname(file_path),
|
|
406
|
+
"segments",
|
|
407
|
+
"000000000001-000000000004.jsonl",
|
|
408
|
+
);
|
|
409
|
+
const segment_rows = await read_jsonl(segment_path);
|
|
410
|
+
const active_rows = await read_jsonl(file_path);
|
|
411
|
+
await fs.writeFile(
|
|
412
|
+
file_path,
|
|
413
|
+
`${[...segment_rows.slice(0, -1), ...active_rows]
|
|
414
|
+
.map((message) => JSON.stringify(message))
|
|
415
|
+
.join("\n")}\n`,
|
|
416
|
+
"utf8",
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const restarted = new SessionRecorder({
|
|
420
|
+
session_id,
|
|
421
|
+
store: new JsonlSessionMessageStore({ session_id, file_path }),
|
|
422
|
+
publish: () => {},
|
|
423
|
+
});
|
|
424
|
+
await restarted.initialize();
|
|
425
|
+
const active = await restarted.list_messages();
|
|
426
|
+
assert.deepEqual(active.items.map((message) => message.sequence), [5, 6]);
|
|
427
|
+
assert.deepEqual(
|
|
428
|
+
(await read_jsonl(file_path)).map((message) => message.sequence),
|
|
429
|
+
[5, 6],
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("连续 Compact 生成按 sequence 连续的 Segment 与累计 Summary", async () => {
|
|
434
|
+
const session_id = "cumulative-compact-test";
|
|
435
|
+
const { recorder } = await create_recorder(session_id);
|
|
436
|
+
const prompts = [];
|
|
437
|
+
let generation_count = 0;
|
|
438
|
+
const model = new MockLanguageModelV3({
|
|
439
|
+
modelId: "cumulative-compact-model",
|
|
440
|
+
doGenerate: async (options) => {
|
|
441
|
+
prompts.push(JSON.stringify(options.prompt));
|
|
442
|
+
generation_count += 1;
|
|
443
|
+
return {
|
|
444
|
+
content: [{ type: "text", text: `Summary ${String(generation_count)}` }],
|
|
445
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
446
|
+
usage: {
|
|
447
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
448
|
+
outputTokens: { total: 0, text: 0, reasoning: 0 },
|
|
449
|
+
},
|
|
450
|
+
warnings: [],
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
const history_store = new SessionRecorderHistoryStore({ session_id, recorder });
|
|
455
|
+
const append_messages = async (start, end) => {
|
|
456
|
+
for (let index = start; index <= end; index += 1) {
|
|
457
|
+
await recorder.append_user_message({
|
|
458
|
+
turn_id: `turn-${String(index)}`,
|
|
459
|
+
input_type: "prompt",
|
|
460
|
+
parts: [{
|
|
461
|
+
part_id: `user-${String(index)}`,
|
|
462
|
+
type: "text",
|
|
463
|
+
text: `message ${String(index)}`,
|
|
464
|
+
state: "done",
|
|
465
|
+
}],
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
const compact = async () => await history_store.compact({
|
|
470
|
+
model,
|
|
471
|
+
system: [],
|
|
472
|
+
keepLastMessages: 2,
|
|
473
|
+
maxInputTokensApprox: 1,
|
|
474
|
+
compactRatio: 0.5,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await append_messages(1, 8);
|
|
478
|
+
assert.equal((await compact()).compacted, true);
|
|
479
|
+
await append_messages(9, 12);
|
|
480
|
+
assert.equal((await compact()).compacted, true);
|
|
481
|
+
|
|
482
|
+
assert.equal(prompts.length, 2);
|
|
483
|
+
assert.equal(prompts[1].includes("<previous-summary>"), true);
|
|
484
|
+
assert.equal(prompts[1].includes("Summary 1"), true);
|
|
485
|
+
const active = await recorder.list_messages();
|
|
486
|
+
assert.deepEqual(active.items.map((message) => message.sequence), [9, 10, 11, 12]);
|
|
487
|
+
const latest_segment = await recorder.list_messages({ before_sequence: 9 });
|
|
488
|
+
assert.deepEqual(latest_segment.items.map((message) => message.sequence), [5, 6, 7, 8]);
|
|
489
|
+
assert.equal(latest_segment.has_more, true);
|
|
490
|
+
const earliest_segment = await recorder.list_messages({
|
|
491
|
+
before_sequence: latest_segment.start_sequence,
|
|
492
|
+
});
|
|
493
|
+
assert.deepEqual(earliest_segment.items.map((message) => message.sequence), [1, 2, 3, 4]);
|
|
494
|
+
const context = await history_store.list_records();
|
|
495
|
+
assert.equal(context[0].parts[0]?.text, "Summary 2");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("Summary 生成失败时保留完整 Active 且不创建 Segment", async () => {
|
|
499
|
+
const session_id = "compact-summary-failure-test";
|
|
500
|
+
const { recorder, file_path } = await create_recorder(session_id);
|
|
501
|
+
for (let index = 1; index <= 6; index += 1) {
|
|
502
|
+
await recorder.append_user_message({
|
|
503
|
+
turn_id: `turn-${String(index)}`,
|
|
504
|
+
input_type: "prompt",
|
|
505
|
+
parts: [{
|
|
506
|
+
part_id: `user-${String(index)}`,
|
|
507
|
+
type: "text",
|
|
508
|
+
text: `message ${String(index)}`,
|
|
509
|
+
state: "done",
|
|
510
|
+
}],
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const model = new MockLanguageModelV3({
|
|
514
|
+
modelId: "failed-compact-model",
|
|
515
|
+
doGenerate: async () => {
|
|
516
|
+
throw new Error("summary unavailable");
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
const history_store = new SessionRecorderHistoryStore({ session_id, recorder });
|
|
520
|
+
const result = await history_store.compact({
|
|
521
|
+
model,
|
|
522
|
+
system: [],
|
|
523
|
+
keepLastMessages: 2,
|
|
524
|
+
maxInputTokensApprox: 1,
|
|
525
|
+
compactRatio: 0.5,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
assert.deepEqual(result, { compacted: false, reason: "summary_failed" });
|
|
529
|
+
assert.deepEqual(
|
|
530
|
+
(await recorder.list_messages()).items.map((message) => message.sequence),
|
|
531
|
+
[1, 2, 3, 4, 5, 6],
|
|
532
|
+
);
|
|
533
|
+
const segment_entries = await fs.readdir(path.join(path.dirname(file_path), "segments"));
|
|
534
|
+
assert.deepEqual(segment_entries, []);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("内部上下文读取完整快照并保留第 500 条之后的最新消息", async () => {
|
|
538
|
+
const session_id = "complete-context-snapshot-test";
|
|
539
|
+
const messages = Array.from(
|
|
540
|
+
{ length: 501 },
|
|
541
|
+
(_, index) => create_seeded_user_message(session_id, index + 1),
|
|
542
|
+
);
|
|
543
|
+
const { recorder } = await create_seeded_recorder(session_id, messages);
|
|
544
|
+
await recorder.compact_active({
|
|
545
|
+
through_sequence: 499,
|
|
546
|
+
summary: {
|
|
547
|
+
record_type: "summary",
|
|
548
|
+
session_id,
|
|
549
|
+
summary_id: "summary-through-499",
|
|
550
|
+
through_sequence: 499,
|
|
551
|
+
text: "summary through 499",
|
|
552
|
+
created_at: 502,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const active = await recorder.list_messages();
|
|
557
|
+
assert.deepEqual(active.items.map((message) => message.sequence), [500, 501]);
|
|
558
|
+
assert.equal(active.source, "active");
|
|
559
|
+
assert.equal(active.next_before_sequence, 500);
|
|
560
|
+
const previous = await recorder.list_messages({ before_sequence: 500 });
|
|
561
|
+
assert.equal(previous.source, "segment");
|
|
562
|
+
assert.equal(previous.items.length, 499);
|
|
563
|
+
assert.equal(previous.start_sequence, 1);
|
|
564
|
+
assert.equal(previous.end_sequence, 499);
|
|
565
|
+
|
|
566
|
+
const history_store = new SessionRecorderHistoryStore({ session_id, recorder });
|
|
567
|
+
const context = await history_store.list_records();
|
|
568
|
+
assert.deepEqual(
|
|
569
|
+
context.map((message) => message.parts[0]?.text),
|
|
570
|
+
["summary through 499", "message 500", "message 501"],
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("Session fork 可以选择并复制第 500 条之后的消息", async () => {
|
|
575
|
+
const session_id = "complete-fork-snapshot-test";
|
|
576
|
+
const messages = Array.from(
|
|
577
|
+
{ length: 501 },
|
|
578
|
+
(_, index) => create_seeded_user_message(session_id, index + 1),
|
|
579
|
+
);
|
|
580
|
+
const { recorder } = await create_seeded_recorder(session_id, messages);
|
|
581
|
+
let imported_messages = [];
|
|
582
|
+
const state_service = {
|
|
583
|
+
emit_action_event: async () => {},
|
|
584
|
+
get_config: () => ({}),
|
|
585
|
+
};
|
|
586
|
+
const view_service = new SessionViewService({
|
|
587
|
+
agent_id: "fork-agent",
|
|
588
|
+
project_root: os.tmpdir(),
|
|
589
|
+
session_id,
|
|
590
|
+
history_store: new SessionRecorderHistoryStore({ session_id, recorder }),
|
|
591
|
+
recorder,
|
|
592
|
+
state_service,
|
|
593
|
+
logger: { log: async () => {} },
|
|
594
|
+
is_executing: () => false,
|
|
595
|
+
get_instruction_system_blocks: () => [],
|
|
596
|
+
get_managed_plugin_system_blocks: async () => [],
|
|
597
|
+
get_plugin_system_blocks: async () => [],
|
|
598
|
+
create_fork_session: async () => ({
|
|
599
|
+
session: { id: "forked-session", set: async () => {} },
|
|
600
|
+
history_store: {},
|
|
601
|
+
recorder: {
|
|
602
|
+
import_messages: async (input) => {
|
|
603
|
+
imported_messages = input;
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
state_service: { set: async () => {} },
|
|
607
|
+
}),
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
await view_service.fork("user-501");
|
|
611
|
+
|
|
612
|
+
assert.equal(imported_messages.length, 501);
|
|
613
|
+
assert.equal(imported_messages[500].message_id, "user-501");
|
|
246
614
|
});
|
package/src/agent/local/Agent.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
} from "@/agent/local/services/AgentAssemblyService.js";
|
|
26
26
|
import { AgentSessions as LocalAgentSessions } from "@/agent/local/services/AgentSessions.js";
|
|
27
27
|
import { AgentBackgroundService } from "@/agent/local/services/AgentBackgroundService.js";
|
|
28
|
+
import { generateId } from "@/utils/Id.js";
|
|
29
|
+
import { PluginRegistry } from "@/plugin/core/PluginRegistry.js";
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* SDK 本地 Agent。
|
|
@@ -46,6 +48,7 @@ export class Agent {
|
|
|
46
48
|
private readonly SessionClass: AgentOptions["Session"];
|
|
47
49
|
private readonly backgroundService: AgentBackgroundService;
|
|
48
50
|
private readonly shell?: AgentOptions["shell"];
|
|
51
|
+
private readonly local_sessions: LocalAgentSessions;
|
|
49
52
|
|
|
50
53
|
private instruction: string[];
|
|
51
54
|
|
|
@@ -91,7 +94,19 @@ export class Agent {
|
|
|
91
94
|
});
|
|
92
95
|
const sessions = this.create_sessions(assembly);
|
|
93
96
|
this.sessions = sessions;
|
|
97
|
+
this.local_sessions = sessions;
|
|
94
98
|
sessions_ref = sessions;
|
|
99
|
+
if (this.plugins instanceof PluginRegistry) {
|
|
100
|
+
const plugin_registry = this.plugins;
|
|
101
|
+
plugin_registry.set_change_listener(({ type, plugin_name }) => {
|
|
102
|
+
const verb = type === "register" ? "registered" : "unregistered";
|
|
103
|
+
this.local_sessions.broadcast_plugins({
|
|
104
|
+
mutation_id: generateId(),
|
|
105
|
+
title: `Agent plugin ${plugin_name} ${verb}`,
|
|
106
|
+
plugins: plugin_registry.execution_view(),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
/**
|
|
@@ -120,7 +135,9 @@ export class Agent {
|
|
|
120
135
|
* 更新当前 SDK Agent 的静态基础指令。
|
|
121
136
|
*/
|
|
122
137
|
setInstruction(input: string | string[]): void {
|
|
123
|
-
|
|
138
|
+
const next_instruction = normalizeInstructionInput(input);
|
|
139
|
+
this.instruction.splice(0, this.instruction.length, ...next_instruction);
|
|
140
|
+
this.local_sessions.broadcast_instruction(this.instruction, generateId());
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
/**
|
|
@@ -145,7 +162,8 @@ export class Agent {
|
|
|
145
162
|
for (const key of Object.keys(this.env)) {
|
|
146
163
|
delete this.env[key];
|
|
147
164
|
}
|
|
148
|
-
this.
|
|
165
|
+
this.apply_env_patch(next);
|
|
166
|
+
this.local_sessions.broadcast_env(this.env, generateId());
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
/**
|
|
@@ -153,9 +171,19 @@ export class Agent {
|
|
|
153
171
|
*
|
|
154
172
|
* 关键点(中文)
|
|
155
173
|
* - `null` / `undefined` 表示删除该 key;其他值会强制转字符串后写入。
|
|
156
|
-
* -
|
|
174
|
+
* - configured env 原地更新;已有 Session 在下一模型 turn 提交 effective env。
|
|
157
175
|
*/
|
|
158
176
|
patchEnv(patch: Record<string, string | null | undefined>): void {
|
|
177
|
+
this.apply_env_patch(patch);
|
|
178
|
+
this.local_sessions.broadcast_env(this.env, generateId());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 原地应用一次 env patch,不触发重复广播。
|
|
183
|
+
*/
|
|
184
|
+
private apply_env_patch(
|
|
185
|
+
patch: Record<string, string | null | undefined>,
|
|
186
|
+
): void {
|
|
159
187
|
if (!patch || typeof patch !== "object") return;
|
|
160
188
|
for (const [raw_key, raw_value] of Object.entries(patch)) {
|
|
161
189
|
const key = String(raw_key || "").trim();
|
|
@@ -207,6 +235,9 @@ export class Agent {
|
|
|
207
235
|
get_agent_context: () => this.agentContext ?? assembly.agent_context,
|
|
208
236
|
get_shell: () => this.shell,
|
|
209
237
|
get_instruction: () => this.instruction,
|
|
238
|
+
get_agent_env: () => this.getEnv(),
|
|
239
|
+
get_agent_plugins: () =>
|
|
240
|
+
(this.plugins as PluginRegistry).execution_view(),
|
|
210
241
|
ensure_agent_ready: async () => {
|
|
211
242
|
await this.backgroundService.ready();
|
|
212
243
|
},
|
|
@@ -41,6 +41,7 @@ import type { SessionPort } from "@/types/runtime/agent/AgentContext.js";
|
|
|
41
41
|
import { createInstructionSystemBlocks } from "@/agent/local/AgentInstructions.js";
|
|
42
42
|
import type { Shell } from "@downcity/shell";
|
|
43
43
|
import type { SessionApproval } from "@/types/session/SessionApproval.js";
|
|
44
|
+
import type { AgentPluginExecutionRuntime } from "@/types/plugin/PluginRuntime.js";
|
|
44
45
|
|
|
45
46
|
function decodeMaybe(input: string): string {
|
|
46
47
|
try {
|
|
@@ -87,6 +88,12 @@ type AgentSessionsOptions = {
|
|
|
87
88
|
*/
|
|
88
89
|
get_instruction: () => string[];
|
|
89
90
|
|
|
91
|
+
/** 延迟读取当前 Agent configured env。 */
|
|
92
|
+
get_agent_env: () => Record<string, string>;
|
|
93
|
+
|
|
94
|
+
/** 创建当前 configured Plugin registry 的模型 turn 执行视图。 */
|
|
95
|
+
get_agent_plugins: () => AgentPluginExecutionRuntime;
|
|
96
|
+
|
|
90
97
|
/**
|
|
91
98
|
* 等待当前 Agent 后台能力启动完成。
|
|
92
99
|
*/
|
|
@@ -120,6 +127,8 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
|
|
|
120
127
|
private readonly get_agent_context: AgentSessionsOptions["get_agent_context"];
|
|
121
128
|
private readonly get_shell: AgentSessionsOptions["get_shell"];
|
|
122
129
|
private readonly get_instruction: AgentSessionsOptions["get_instruction"];
|
|
130
|
+
private readonly get_agent_env: AgentSessionsOptions["get_agent_env"];
|
|
131
|
+
private readonly get_agent_plugins: AgentSessionsOptions["get_agent_plugins"];
|
|
123
132
|
private readonly ensure_agent_ready: AgentSessionsOptions["ensure_agent_ready"];
|
|
124
133
|
private readonly default_model?: AgentModel;
|
|
125
134
|
private readonly default_model_id?: string;
|
|
@@ -136,6 +145,8 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
|
|
|
136
145
|
this.get_agent_context = options.get_agent_context;
|
|
137
146
|
this.get_shell = options.get_shell;
|
|
138
147
|
this.get_instruction = options.get_instruction;
|
|
148
|
+
this.get_agent_env = options.get_agent_env;
|
|
149
|
+
this.get_agent_plugins = options.get_agent_plugins;
|
|
139
150
|
this.ensure_agent_ready = options.ensure_agent_ready;
|
|
140
151
|
this.default_model = options.default_model;
|
|
141
152
|
this.default_model_id = options.default_model_id;
|
|
@@ -150,6 +161,54 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
|
|
|
150
161
|
return [...this.sessions_by_id.values()];
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
/**
|
|
165
|
+
* 把 Agent instruction 修改广播到已有 Session 的统一输入队列。
|
|
166
|
+
*/
|
|
167
|
+
broadcast_instruction(instruction: string[], mutation_id: string): void {
|
|
168
|
+
const instruction_blocks = createInstructionSystemBlocks(
|
|
169
|
+
instruction,
|
|
170
|
+
this.project_root,
|
|
171
|
+
);
|
|
172
|
+
for (const session of this.sessions_by_id.values()) {
|
|
173
|
+
session.enqueue_agent_config({
|
|
174
|
+
type: "instruction",
|
|
175
|
+
mutation_id,
|
|
176
|
+
instruction_blocks,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 把 Agent env 修改广播到已有 Session 的统一输入队列。
|
|
183
|
+
*/
|
|
184
|
+
broadcast_env(env: Record<string, string>, mutation_id: string): void {
|
|
185
|
+
for (const session of this.sessions_by_id.values()) {
|
|
186
|
+
session.enqueue_agent_config({
|
|
187
|
+
type: "env",
|
|
188
|
+
mutation_id,
|
|
189
|
+
env: { ...env },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 把 Plugin registry 修改广播到已有 Session 的统一输入队列。
|
|
196
|
+
*/
|
|
197
|
+
broadcast_plugins(input: {
|
|
198
|
+
mutation_id: string;
|
|
199
|
+
title: string;
|
|
200
|
+
plugins: AgentPluginExecutionRuntime;
|
|
201
|
+
}): void {
|
|
202
|
+
for (const session of this.sessions_by_id.values()) {
|
|
203
|
+
session.enqueue_agent_config({
|
|
204
|
+
type: "plugins",
|
|
205
|
+
mutation_id: input.mutation_id,
|
|
206
|
+
title: input.title,
|
|
207
|
+
plugins: input.plugins,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
153
212
|
/**
|
|
154
213
|
* 获取或创建一个 session runtime port。
|
|
155
214
|
*/
|
|
@@ -348,6 +407,8 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
|
|
|
348
407
|
tools: this.tools,
|
|
349
408
|
logger: this.logger,
|
|
350
409
|
getInstructionSystemBlocks: () => this.load_instruction_system_blocks(),
|
|
410
|
+
getAgentEnv: () => this.get_agent_env(),
|
|
411
|
+
get_agent_plugins: () => this.get_agent_plugins(),
|
|
351
412
|
getManagedPluginSystemBlocks: async () => [],
|
|
352
413
|
getPluginSystemBlocks: async () => await this.load_plugin_system_blocks(),
|
|
353
414
|
ensureConfigured: async (session) => {
|
|
@@ -219,15 +219,10 @@ export class HttpRemoteAgentTransport implements RemoteAgentTransport {
|
|
|
219
219
|
input?: ListSessionMessagesInput,
|
|
220
220
|
): Promise<SessionMessagePage> {
|
|
221
221
|
const query = new URLSearchParams();
|
|
222
|
-
if (input?.limit !== undefined) query.set("limit", String(input.limit));
|
|
223
|
-
if (input?.cursor) query.set("cursor", input.cursor);
|
|
224
222
|
if (input?.before_sequence !== undefined) {
|
|
225
223
|
query.set("before_sequence", String(input.before_sequence));
|
|
226
224
|
}
|
|
227
225
|
if (input?.include_internal) query.set("include_internal", "true");
|
|
228
|
-
if (input?.through_sequence !== undefined) {
|
|
229
|
-
query.set("through_sequence", String(input.through_sequence));
|
|
230
|
-
}
|
|
231
226
|
const payload = await read_http_json<{
|
|
232
227
|
success?: boolean;
|
|
233
228
|
error?: string;
|