@downcity/agent 1.1.228 → 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 +20 -2
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +91 -46
- 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 +340 -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 +116 -49
- 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",
|
|
@@ -216,7 +255,7 @@ test("重启时将 Assistant 草稿收口为 stopped,并将运行中 Action
|
|
|
216
255
|
});
|
|
217
256
|
await recovered.initialize();
|
|
218
257
|
|
|
219
|
-
const page = await recovered.list_messages(
|
|
258
|
+
const page = await recovered.list_messages();
|
|
220
259
|
assert.deepEqual(page.items.map((message) => message.type), ["user", "assistant", "action"]);
|
|
221
260
|
assert.equal(page.items[1].status, "stopped");
|
|
222
261
|
assert.equal(page.items[1].parts[0].text, "partial");
|
|
@@ -244,7 +283,7 @@ test("Action revision 追加完整快照,读取时只返回最新版本", asyn
|
|
|
244
283
|
assert.equal(page.items[0].title, "Compacted");
|
|
245
284
|
});
|
|
246
285
|
|
|
247
|
-
test("compact
|
|
286
|
+
test("compact 把 Active 前缀关闭为带累计 Summary 的 Segment", async () => {
|
|
248
287
|
const { recorder, file_path } = await create_recorder("compact-test");
|
|
249
288
|
for (let index = 1; index <= 6; index += 1) {
|
|
250
289
|
await recorder.append_user_message({
|
|
@@ -253,7 +292,6 @@ test("compact 追加 internal summary 且不重写已有 Message 快照", async
|
|
|
253
292
|
parts: [{ part_id: `user-${String(index)}`, type: "text", text: `message ${String(index)}`, state: "done" }],
|
|
254
293
|
});
|
|
255
294
|
}
|
|
256
|
-
const before = await fs.readFile(file_path, "utf8");
|
|
257
295
|
const history_store = new SessionRecorderHistoryStore({ session_id: "compact-test", recorder });
|
|
258
296
|
const model = new MockLanguageModelV3({
|
|
259
297
|
modelId: "compact-model",
|
|
@@ -272,14 +310,305 @@ test("compact 追加 internal summary 且不重写已有 Message 快照", async
|
|
|
272
310
|
system: [],
|
|
273
311
|
keepLastMessages: 2,
|
|
274
312
|
maxInputTokensApprox: 1,
|
|
275
|
-
archiveOnCompact: false,
|
|
276
313
|
compactRatio: 0.5,
|
|
277
314
|
});
|
|
278
315
|
assert.equal(result.compacted, true);
|
|
279
|
-
assert.equal((await fs.readFile(file_path, "utf8")).
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
assert.equal(
|
|
283
|
-
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");
|
|
284
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");
|
|
285
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;
|