@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.
Files changed (157) hide show
  1. package/bin/agent/local/Agent.d.ts +6 -1
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +30 -3
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentSessions.d.ts +23 -0
  6. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  7. package/bin/agent/local/services/AgentSessions.js +44 -0
  8. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  9. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  10. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +0 -7
  11. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  12. package/bin/executor/Executor.d.ts +25 -0
  13. package/bin/executor/Executor.d.ts.map +1 -1
  14. package/bin/executor/Executor.js +36 -0
  15. package/bin/executor/Executor.js.map +1 -1
  16. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +4 -0
  17. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  18. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -1
  19. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  20. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +15 -11
  21. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  22. package/bin/executor/core-engine/CoreEngineRunner.d.ts +11 -0
  23. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  24. package/bin/executor/core-engine/CoreEngineRunner.js +12 -8
  25. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  26. package/bin/executor/store/history/SessionHistoryStore.d.ts +3 -13
  27. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  28. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -1
  29. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  30. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  31. package/bin/executor/tools/plugin/PluginToolBridge.js +4 -2
  32. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  33. package/bin/index.d.ts +2 -1
  34. package/bin/index.d.ts.map +1 -1
  35. package/bin/index.js +1 -1
  36. package/bin/index.js.map +1 -1
  37. package/bin/model/CityModelAdapter.d.ts +6 -0
  38. package/bin/model/CityModelAdapter.d.ts.map +1 -1
  39. package/bin/model/CityModelAdapter.js +13 -0
  40. package/bin/model/CityModelAdapter.js.map +1 -1
  41. package/bin/plugin/core/PluginRegistry.d.ts +22 -1
  42. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  43. package/bin/plugin/core/PluginRegistry.js +46 -4
  44. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  45. package/bin/session/Session.d.ts +11 -1
  46. package/bin/session/Session.d.ts.map +1 -1
  47. package/bin/session/Session.js +76 -11
  48. package/bin/session/Session.js.map +1 -1
  49. package/bin/session/browse/Browse.d.ts +0 -8
  50. package/bin/session/browse/Browse.d.ts.map +1 -1
  51. package/bin/session/browse/Browse.js +72 -57
  52. package/bin/session/browse/Browse.js.map +1 -1
  53. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +69 -16
  54. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  55. package/bin/session/recorder/JsonlSessionMessageStore.js +283 -60
  56. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  57. package/bin/session/recorder/SessionRecorder.d.ts +20 -2
  58. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  59. package/bin/session/recorder/SessionRecorder.js +91 -46
  60. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  61. package/bin/session/recorder/SessionRecorderCompaction.d.ts +3 -3
  62. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  63. package/bin/session/recorder/SessionRecorderCompaction.js +50 -62
  64. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  65. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +2 -2
  66. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  67. package/bin/session/recorder/SessionRecorderHistoryStore.js +31 -36
  68. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  69. package/bin/session/runtime/SessionPromptRuntime.d.ts +17 -1
  70. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  71. package/bin/session/runtime/SessionPromptRuntime.js +67 -8
  72. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  73. package/bin/session/services/SessionStateService.d.ts +15 -1
  74. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  75. package/bin/session/services/SessionStateService.js +60 -39
  76. package/bin/session/services/SessionStateService.js.map +1 -1
  77. package/bin/session/services/SessionTurnService.d.ts +5 -0
  78. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  79. package/bin/session/services/SessionTurnService.js +17 -3
  80. package/bin/session/services/SessionTurnService.js.map +1 -1
  81. package/bin/session/services/SessionViewService.d.ts +1 -5
  82. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  83. package/bin/session/services/SessionViewService.js +4 -34
  84. package/bin/session/services/SessionViewService.js.map +1 -1
  85. package/bin/session/storage/Paths.d.ts +3 -15
  86. package/bin/session/storage/Paths.d.ts.map +1 -1
  87. package/bin/session/storage/Paths.js +5 -21
  88. package/bin/session/storage/Paths.js.map +1 -1
  89. package/bin/types/agent/SessionTypes.d.ts +2 -24
  90. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  91. package/bin/types/executor/SessionRunContext.d.ts +21 -0
  92. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  93. package/bin/types/plugin/PluginRuntime.d.ts +25 -0
  94. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  95. package/bin/types/runtime/agent/AgentContext.d.ts +17 -5
  96. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  97. package/bin/types/runtime/agent/AgentContext.js +29 -6
  98. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  99. package/bin/types/session/SessionConfigMutation.d.ts +68 -0
  100. package/bin/types/session/SessionConfigMutation.d.ts.map +1 -0
  101. package/bin/types/session/SessionConfigMutation.js +10 -0
  102. package/bin/types/session/SessionConfigMutation.js.map +1 -0
  103. package/bin/types/session/SessionLocalState.d.ts +8 -0
  104. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  105. package/bin/types/session/SessionMessage.d.ts +16 -12
  106. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  107. package/bin/types/session/SessionMutation.d.ts +1 -1
  108. package/bin/types/session/SessionMutation.js +1 -1
  109. package/bin/types/session/SessionOptions.d.ts +16 -0
  110. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  111. package/bin/types/session/SessionSegment.d.ts +57 -0
  112. package/bin/types/session/SessionSegment.d.ts.map +1 -0
  113. package/bin/types/session/SessionSegment.js +8 -0
  114. package/bin/types/session/SessionSegment.js.map +1 -0
  115. package/package.json +3 -3
  116. package/scripts/city-model-tool-loop.test.mjs +32 -0
  117. package/scripts/session-action-message.test.mjs +59 -13
  118. package/scripts/session-compaction-context-window.test.mjs +95 -0
  119. package/scripts/session-config-turn-boundary.test.mjs +224 -0
  120. package/scripts/session-list-title.test.mjs +21 -3
  121. package/scripts/session-prompt-runtime.test.mjs +85 -0
  122. package/scripts/session-recorder.test.mjs +340 -11
  123. package/src/agent/local/Agent.ts +34 -3
  124. package/src/agent/local/services/AgentSessions.ts +61 -0
  125. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +0 -5
  126. package/src/executor/Executor.ts +62 -0
  127. package/src/executor/composer/compaction/SessionCompactionComposer.ts +5 -0
  128. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +16 -14
  129. package/src/executor/core-engine/CoreEngineRunner.ts +24 -8
  130. package/src/executor/store/history/SessionHistoryStore.ts +3 -14
  131. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -1
  132. package/src/executor/tools/plugin/PluginToolBridge.ts +4 -2
  133. package/src/index.ts +8 -0
  134. package/src/model/CityModelAdapter.ts +15 -0
  135. package/src/plugin/core/PluginRegistry.ts +72 -4
  136. package/src/session/Session.ts +89 -11
  137. package/src/session/browse/Browse.ts +73 -69
  138. package/src/session/recorder/JsonlSessionMessageStore.ts +378 -60
  139. package/src/session/recorder/SessionRecorder.ts +116 -49
  140. package/src/session/recorder/SessionRecorderCompaction.ts +55 -80
  141. package/src/session/recorder/SessionRecorderHistoryStore.ts +29 -49
  142. package/src/session/runtime/SessionPromptRuntime.ts +100 -9
  143. package/src/session/services/SessionStateService.ts +79 -44
  144. package/src/session/services/SessionTurnService.ts +19 -3
  145. package/src/session/services/SessionViewService.ts +4 -50
  146. package/src/session/storage/Paths.ts +5 -38
  147. package/src/types/agent/SessionTypes.ts +2 -24
  148. package/src/types/executor/SessionRunContext.ts +25 -0
  149. package/src/types/plugin/PluginRuntime.ts +27 -0
  150. package/src/types/runtime/agent/AgentContext.ts +32 -7
  151. package/src/types/session/SessionConfigMutation.ts +78 -0
  152. package/src/types/session/SessionLocalState.ts +9 -0
  153. package/src/types/session/SessionMessage.ts +16 -12
  154. package/src/types/session/SessionMutation.ts +1 -1
  155. package/src/types/session/SessionOptions.ts +21 -0
  156. package/src/types/session/SessionSegment.ts +62 -0
  157. 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, "messages.jsonl");
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
- test("delta 只更新 Assistant 草稿,完成后才写入 messages.jsonl", async () => {
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({ limit: 10 });
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 追加 internal summary 且不重写已有 Message 快照", async () => {
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")).startsWith(before), true);
280
- const all = await recorder.list_messages({ limit: 100, include_internal: true });
281
- const summary = all.items.find((message) => message.type === "assistant" && message.kind === "summary");
282
- assert.equal(summary.visibility, "internal");
283
- assert.equal(summary.parts[0].sequence, 1);
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
  });
@@ -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
- this.instruction = normalizeInstructionInput(input);
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.patchEnv(next);
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
- * - 修改原地生效,所有持有 `context.env` 引用的模块立即可见。
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;