@downcity/agent 1.1.232 → 1.1.236

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/bin/agent/local/Agent.d.ts +1 -1
  2. package/bin/agent/local/Agent.js +2 -2
  3. package/bin/agent/local/Agent.js.map +1 -1
  4. package/bin/agent/local/services/AgentSessions.d.ts +4 -4
  5. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  6. package/bin/agent/local/services/AgentSessions.js +8 -8
  7. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  8. package/bin/agent/remote/RemoteSession.d.ts +2 -0
  9. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  10. package/bin/agent/remote/RemoteSession.js +4 -0
  11. package/bin/agent/remote/RemoteSession.js.map +1 -1
  12. package/bin/agent/remote/RemoteTransport.d.ts +2 -0
  13. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  14. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
  15. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  16. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -0
  17. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  18. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
  19. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  20. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
  21. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  22. package/bin/executor/Executor.d.ts +20 -2
  23. package/bin/executor/Executor.d.ts.map +1 -1
  24. package/bin/executor/Executor.js +102 -18
  25. package/bin/executor/Executor.js.map +1 -1
  26. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +6 -0
  27. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  28. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -9
  29. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  30. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -38
  31. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  32. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
  33. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  34. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
  35. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  36. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
  37. package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
  38. package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
  39. package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
  40. package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
  41. package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
  42. package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
  43. package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
  44. package/bin/executor/core-engine/CoreEngineRunner.d.ts +9 -5
  45. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  46. package/bin/executor/core-engine/CoreEngineRunner.js +146 -35
  47. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  48. package/bin/executor/store/history/SessionHistoryStore.d.ts +4 -10
  49. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  50. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  51. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -3
  52. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  53. package/bin/executor/types/SessionRun.d.ts +8 -0
  54. package/bin/executor/types/SessionRun.d.ts.map +1 -1
  55. package/bin/index.d.ts +1 -1
  56. package/bin/index.d.ts.map +1 -1
  57. package/bin/index.js +1 -1
  58. package/bin/index.js.map +1 -1
  59. package/bin/plugin/core/PluginLocalExecution.js +3 -3
  60. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  61. package/bin/plugin/core/PluginRegistry.d.ts +20 -2
  62. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  63. package/bin/plugin/core/PluginRegistry.js +100 -3
  64. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  65. package/bin/rpc/Client.d.ts +4 -0
  66. package/bin/rpc/Client.d.ts.map +1 -1
  67. package/bin/rpc/Client.js +11 -0
  68. package/bin/rpc/Client.js.map +1 -1
  69. package/bin/session/Session.d.ts +7 -3
  70. package/bin/session/Session.d.ts.map +1 -1
  71. package/bin/session/Session.js +26 -19
  72. package/bin/session/Session.js.map +1 -1
  73. package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
  74. package/bin/session/SessionSystemBuilder.js +3 -1
  75. package/bin/session/SessionSystemBuilder.js.map +1 -1
  76. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  77. package/bin/session/recorder/SessionRecorderCompaction.js +102 -60
  78. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  79. package/bin/session/runtime/SessionPromptRuntime.d.ts +11 -7
  80. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  81. package/bin/session/runtime/SessionPromptRuntime.js +22 -24
  82. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  83. package/bin/session/services/SessionStateService.d.ts +10 -6
  84. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  85. package/bin/session/services/SessionStateService.js +29 -20
  86. package/bin/session/services/SessionStateService.js.map +1 -1
  87. package/bin/session/services/SessionTurnService.d.ts +13 -3
  88. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  89. package/bin/session/services/SessionTurnService.js +90 -7
  90. package/bin/session/services/SessionTurnService.js.map +1 -1
  91. package/bin/types/agent/SessionActor.d.ts +8 -0
  92. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  93. package/bin/types/executor/SessionRunContext.d.ts +16 -8
  94. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  95. package/bin/types/plugin/PluginRuntime.d.ts +36 -4
  96. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  97. package/bin/types/plugin/PluginState.d.ts +10 -0
  98. package/bin/types/plugin/PluginState.d.ts.map +1 -1
  99. package/bin/types/rpc/RpcProtocol.d.ts +10 -0
  100. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  101. package/bin/types/runtime/agent/AgentContext.d.ts +2 -2
  102. package/bin/types/runtime/agent/AgentContext.js +1 -1
  103. package/bin/types/session/SessionLocalState.d.ts +2 -2
  104. package/bin/types/session/SessionOptions.d.ts +5 -5
  105. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  106. package/bin/types/session/SessionQueueCommand.d.ts +68 -0
  107. package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
  108. package/bin/types/session/SessionQueueCommand.js +10 -0
  109. package/bin/types/session/SessionQueueCommand.js.map +1 -0
  110. package/bin/utils/logger/Logger.d.ts +7 -9
  111. package/bin/utils/logger/Logger.d.ts.map +1 -1
  112. package/bin/utils/logger/Logger.js +11 -9
  113. package/bin/utils/logger/Logger.js.map +1 -1
  114. package/package.json +3 -3
  115. package/scripts/core-engine-context-compaction.test.mjs +411 -0
  116. package/scripts/logger-isolation.test.mjs +55 -0
  117. package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
  118. package/scripts/plugin-tool-bridge.test.mjs +52 -0
  119. package/scripts/remote-session-reconnect.test.mjs +18 -0
  120. package/scripts/session-compaction-context-window.test.mjs +27 -26
  121. package/scripts/session-config-turn-boundary.test.mjs +80 -26
  122. package/scripts/session-prompt-runtime.test.mjs +23 -15
  123. package/scripts/session-recorder.test.mjs +15 -24
  124. package/src/agent/local/Agent.ts +2 -2
  125. package/src/agent/local/services/AgentSessions.ts +10 -10
  126. package/src/agent/remote/RemoteSession.ts +5 -0
  127. package/src/agent/remote/RemoteTransport.ts +2 -0
  128. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -0
  129. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
  130. package/src/executor/Executor.ts +111 -19
  131. package/src/executor/composer/compaction/SessionCompactionComposer.ts +7 -0
  132. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -57
  133. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
  134. package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
  135. package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
  136. package/src/executor/core-engine/CoreEngineRunner.ts +200 -48
  137. package/src/executor/store/history/SessionHistoryStore.ts +4 -12
  138. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -3
  139. package/src/executor/types/SessionRun.ts +9 -0
  140. package/src/index.ts +1 -1
  141. package/src/plugin/core/PluginLocalExecution.ts +3 -3
  142. package/src/plugin/core/PluginRegistry.ts +116 -3
  143. package/src/rpc/Client.ts +12 -0
  144. package/src/session/Session.ts +28 -20
  145. package/src/session/SessionSystemBuilder.ts +4 -1
  146. package/src/session/recorder/SessionRecorderCompaction.ts +113 -65
  147. package/src/session/runtime/SessionPromptRuntime.ts +31 -46
  148. package/src/session/services/SessionStateService.ts +34 -25
  149. package/src/session/services/SessionTurnService.ts +97 -8
  150. package/src/types/agent/SessionActor.ts +9 -0
  151. package/src/types/executor/SessionRunContext.ts +17 -8
  152. package/src/types/plugin/PluginRuntime.ts +38 -4
  153. package/src/types/plugin/PluginState.ts +10 -0
  154. package/src/types/rpc/RpcProtocol.ts +11 -0
  155. package/src/types/runtime/agent/AgentContext.ts +2 -2
  156. package/src/types/session/SessionLocalState.ts +2 -2
  157. package/src/types/session/SessionOptions.ts +6 -6
  158. package/src/types/session/SessionQueueCommand.ts +78 -0
  159. package/src/utils/logger/Logger.ts +10 -10
  160. package/tsconfig.tsbuildinfo +1 -1
  161. package/bin/types/session/SessionConfigMutation.d.ts +0 -68
  162. package/bin/types/session/SessionConfigMutation.d.ts.map +0 -1
  163. package/bin/types/session/SessionConfigMutation.js +0 -10
  164. package/bin/types/session/SessionConfigMutation.js.map +0 -1
  165. package/src/types/session/SessionConfigMutation.ts +0 -78
@@ -139,8 +139,9 @@ test("SessionPromptRuntime merges queued prompts at the next step boundary", asy
139
139
  );
140
140
  });
141
141
 
142
- test("SessionPromptRuntime applies config and steer in queue order before the next model turn", async () => {
142
+ test("SessionPromptRuntime applies config and steer in queue order at the same step checkpoint", async () => {
143
143
  const lifecycle = [];
144
+ const applied_turn_ids = [];
144
145
  const execution_finished = createDeferred();
145
146
  let step_merge = null;
146
147
 
@@ -164,20 +165,24 @@ test("SessionPromptRuntime applies config and steer in queue order before the ne
164
165
  stopTurn: () => false,
165
166
  });
166
167
 
167
- runtime.enqueue_config({
168
- mutation_id: "config-1",
168
+ runtime.enqueue_command({
169
+ type: "command",
170
+ command_id: "config-1",
169
171
  scope: "agent",
170
- apply: async ({ model_turn_index }) => {
171
- lifecycle.push(`config:1:${String(model_turn_index)}`);
172
+ execute: async ({ turn_id }) => {
173
+ applied_turn_ids.push(turn_id);
174
+ lifecycle.push("config:1");
172
175
  },
173
176
  });
174
177
  const first_turn = await runtime.prompt({ query: "first" });
175
178
  const merge = await waitUntil(() => step_merge);
176
- runtime.enqueue_config({
177
- mutation_id: "config-2",
179
+ runtime.enqueue_command({
180
+ type: "command",
181
+ command_id: "config-2",
178
182
  scope: "session",
179
- apply: async ({ model_turn_index }) => {
180
- lifecycle.push(`config:2:${String(model_turn_index)}`);
183
+ execute: async ({ turn_id }) => {
184
+ applied_turn_ids.push(turn_id);
185
+ lifecycle.push("config:2");
181
186
  },
182
187
  });
183
188
  const second_turn_promise = runtime.prompt({ query: "second" });
@@ -191,17 +196,18 @@ test("SessionPromptRuntime applies config and steer in queue order before the ne
191
196
  ["second"],
192
197
  );
193
198
  assert.deepEqual(lifecycle, [
194
- "config:1:1",
199
+ "config:1",
195
200
  "prompt:first",
196
- "config:2:2",
201
+ "config:2",
197
202
  "steer:second",
198
203
  ]);
204
+ assert.deepEqual(applied_turn_ids, [first_turn.id, first_turn.id]);
199
205
 
200
206
  execution_finished.resolve();
201
207
  await first_turn.finished;
202
208
  });
203
209
 
204
- test("SessionPromptRuntime keeps config queued without starting a turn", async () => {
210
+ test("SessionPromptRuntime keeps command queued without starting a turn", async () => {
205
211
  let apply_count = 0;
206
212
  const runtime = new SessionPromptRuntime({
207
213
  sessionId: "test",
@@ -212,15 +218,17 @@ test("SessionPromptRuntime keeps config queued without starting a turn", async (
212
218
  stopTurn: () => false,
213
219
  });
214
220
 
215
- runtime.enqueue_config({
216
- mutation_id: "config-only",
221
+ runtime.enqueue_command({
222
+ type: "command",
223
+ command_id: "config-only",
217
224
  scope: "agent",
218
- apply: async () => {
225
+ execute: async () => {
219
226
  apply_count += 1;
220
227
  },
221
228
  });
222
229
 
223
230
  assert.equal(runtime.isActive(), false);
231
+ assert.equal(runtime.has_pending_command(), true);
224
232
  assert.equal(apply_count, 0);
225
233
  });
226
234
 
@@ -308,23 +308,21 @@ test("compact 把 Active 前缀关闭为带累计 Summary 的 Segment", async ()
308
308
  const result = await history_store.compact({
309
309
  model,
310
310
  system: [],
311
- keepLastMessages: 2,
312
- maxInputTokensApprox: 1,
313
- compactRatio: 0.5,
311
+ force: true,
314
312
  });
315
313
  assert.equal(result.compacted, true);
316
314
  assert.equal((await fs.readFile(file_path, "utf8")).includes("message 1"), false);
317
315
  const active = await recorder.list_messages({ include_internal: true });
318
- assert.deepEqual(active.items.map((message) => message.sequence), [4, 5, 6]);
316
+ assert.deepEqual(active.items.map((message) => message.sequence), []);
319
317
  assert.equal(active.source, "active");
320
318
  assert.equal(active.has_more, true);
321
319
  const segment = await recorder.list_messages({
322
- before_sequence: active.start_sequence,
320
+ before_sequence: 7,
323
321
  include_internal: true,
324
322
  });
325
- assert.deepEqual(segment.items.map((message) => message.sequence), [1, 2, 3]);
323
+ assert.deepEqual(segment.items.map((message) => message.sequence), [1, 2, 3, 4, 5, 6]);
326
324
  assert.equal(segment.source, "segment");
327
- assert.equal((await history_store.list_records()).length, 4);
325
+ assert.equal((await history_store.list_records()).length, 1);
328
326
  await assert.rejects(
329
327
  recorder.list_messages({ before_sequence: 0 }),
330
328
  /before_sequence must be a positive integer/,
@@ -469,9 +467,7 @@ test("连续 Compact 生成按 sequence 连续的 Segment 与累计 Summary", as
469
467
  const compact = async () => await history_store.compact({
470
468
  model,
471
469
  system: [],
472
- keepLastMessages: 2,
473
- maxInputTokensApprox: 1,
474
- compactRatio: 0.5,
470
+ force: true,
475
471
  });
476
472
 
477
473
  await append_messages(1, 8);
@@ -483,19 +479,19 @@ test("连续 Compact 生成按 sequence 连续的 Segment 与累计 Summary", as
483
479
  assert.equal(prompts[1].includes("<previous-summary>"), true);
484
480
  assert.equal(prompts[1].includes("Summary 1"), true);
485
481
  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]);
482
+ assert.deepEqual(active.items.map((message) => message.sequence), []);
483
+ const latest_segment = await recorder.list_messages({ before_sequence: 13 });
484
+ assert.deepEqual(latest_segment.items.map((message) => message.sequence), [9, 10, 11, 12]);
489
485
  assert.equal(latest_segment.has_more, true);
490
486
  const earliest_segment = await recorder.list_messages({
491
487
  before_sequence: latest_segment.start_sequence,
492
488
  });
493
- assert.deepEqual(earliest_segment.items.map((message) => message.sequence), [1, 2, 3, 4]);
489
+ assert.deepEqual(earliest_segment.items.map((message) => message.sequence), [1, 2, 3, 4, 5, 6, 7, 8]);
494
490
  const context = await history_store.list_records();
495
491
  assert.equal(context[0].parts[0]?.text, "Summary 2");
496
492
  });
497
493
 
498
- test("Summary 生成失败时保留完整 Active 且不创建 Segment", async () => {
494
+ test("Summary 生成失败时使用确定性 Summary 完成归档", async () => {
499
495
  const session_id = "compact-summary-failure-test";
500
496
  const { recorder, file_path } = await create_recorder(session_id);
501
497
  for (let index = 1; index <= 6; index += 1) {
@@ -520,18 +516,13 @@ test("Summary 生成失败时保留完整 Active 且不创建 Segment", async ()
520
516
  const result = await history_store.compact({
521
517
  model,
522
518
  system: [],
523
- keepLastMessages: 2,
524
- maxInputTokensApprox: 1,
525
- compactRatio: 0.5,
519
+ force: true,
526
520
  });
527
521
 
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
- );
522
+ assert.deepEqual(result, { compacted: true });
523
+ assert.deepEqual((await recorder.list_messages()).items, []);
533
524
  const segment_entries = await fs.readdir(path.join(path.dirname(file_path), "segments"));
534
- assert.deepEqual(segment_entries, []);
525
+ assert.equal(segment_entries.length, 1);
535
526
  });
536
527
 
537
528
  test("内部上下文读取完整快照并保留第 500 条之后的最新消息", async () => {
@@ -101,7 +101,7 @@ export class Agent {
101
101
  plugin_registry.set_change_listener(({ type, plugin_name }) => {
102
102
  const verb = type === "register" ? "registered" : "unregistered";
103
103
  this.local_sessions.broadcast_plugins({
104
- mutation_id: generateId(),
104
+ command_id: generateId(),
105
105
  title: `Agent plugin ${plugin_name} ${verb}`,
106
106
  plugins: plugin_registry.execution_view(),
107
107
  });
@@ -171,7 +171,7 @@ export class Agent {
171
171
  *
172
172
  * 关键点(中文)
173
173
  * - `null` / `undefined` 表示删除该 key;其他值会强制转字符串后写入。
174
- * - configured env 原地更新;已有 Session 在下一模型 turn 提交 effective env。
174
+ * - configured env 原地更新;已有 Session 在下一 step 检查点提交 effective env。
175
175
  */
176
176
  patchEnv(patch: Record<string, string | null | undefined>): void {
177
177
  this.apply_env_patch(patch);
@@ -91,7 +91,7 @@ type AgentSessionsOptions = {
91
91
  /** 延迟读取当前 Agent configured env。 */
92
92
  get_agent_env: () => Record<string, string>;
93
93
 
94
- /** 创建当前 configured Plugin registry 的模型 turn 执行视图。 */
94
+ /** 创建当前 configured Plugin registry Session step 执行视图。 */
95
95
  get_agent_plugins: () => AgentPluginExecutionRuntime;
96
96
 
97
97
  /**
@@ -164,15 +164,15 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
164
164
  /**
165
165
  * 把 Agent instruction 修改广播到已有 Session 的统一输入队列。
166
166
  */
167
- broadcast_instruction(instruction: string[], mutation_id: string): void {
167
+ broadcast_instruction(instruction: string[], command_id: string): void {
168
168
  const instruction_blocks = createInstructionSystemBlocks(
169
169
  instruction,
170
170
  this.project_root,
171
171
  );
172
172
  for (const session of this.sessions_by_id.values()) {
173
- session.enqueue_agent_config({
173
+ session.enqueue_agent_command({
174
174
  type: "instruction",
175
- mutation_id,
175
+ command_id,
176
176
  instruction_blocks,
177
177
  });
178
178
  }
@@ -181,11 +181,11 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
181
181
  /**
182
182
  * 把 Agent env 修改广播到已有 Session 的统一输入队列。
183
183
  */
184
- broadcast_env(env: Record<string, string>, mutation_id: string): void {
184
+ broadcast_env(env: Record<string, string>, command_id: string): void {
185
185
  for (const session of this.sessions_by_id.values()) {
186
- session.enqueue_agent_config({
186
+ session.enqueue_agent_command({
187
187
  type: "env",
188
- mutation_id,
188
+ command_id,
189
189
  env: { ...env },
190
190
  });
191
191
  }
@@ -195,14 +195,14 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
195
195
  * 把 Plugin registry 修改广播到已有 Session 的统一输入队列。
196
196
  */
197
197
  broadcast_plugins(input: {
198
- mutation_id: string;
198
+ command_id: string;
199
199
  title: string;
200
200
  plugins: AgentPluginExecutionRuntime;
201
201
  }): void {
202
202
  for (const session of this.sessions_by_id.values()) {
203
- session.enqueue_agent_config({
203
+ session.enqueue_agent_command({
204
204
  type: "plugins",
205
- mutation_id: input.mutation_id,
205
+ command_id: input.command_id,
206
206
  title: input.title,
207
207
  plugins: input.plugins,
208
208
  });
@@ -123,6 +123,11 @@ export class RemoteSession implements RemoteAgentSession {
123
123
  return await this.transport.stop(this.id);
124
124
  }
125
125
 
126
+ /** 把一次显式历史压缩加入当前远程 Session 的有序输入队列。 */
127
+ async compact(): Promise<void> {
128
+ await this.transport.compact(this.id);
129
+ }
130
+
126
131
  /** 订阅当前 Session 的全部未来 Mutation。 */
127
132
  subscribe(subscriber: SessionMutationSubscriber): SessionMutationUnsubscribe {
128
133
  this.event_subscriber_count += 1;
@@ -62,6 +62,8 @@ export type RemoteSessionTransport = {
62
62
  ): Promise<{ id: string }>;
63
63
  /** 停止当前 session turn,并取消未吸收队列。 */
64
64
  stop(session_id: string): Promise<AgentSessionStopResult>;
65
+ /** 把一次显式历史压缩加入远程 Session 的有序输入队列。 */
66
+ compact(session_id: string): Promise<void>;
65
67
  /** 订阅 session 事件。 */
66
68
  subscribe(params: {
67
69
  session_id: string;
@@ -166,6 +166,21 @@ export class HttpRemoteAgentTransport implements RemoteAgentTransport {
166
166
  return payload.result;
167
167
  }
168
168
 
169
+ async compact(session_id: string): Promise<void> {
170
+ const payload = await read_http_json<{
171
+ success?: boolean;
172
+ error?: string;
173
+ }>(`${this.base_url}/api/sdk/sessions/${encodeURIComponent(session_id)}/compact`, {
174
+ method: "POST",
175
+ headers: this.headers({
176
+ "Content-Type": "application/json",
177
+ }),
178
+ });
179
+ if (!payload.success) {
180
+ throw new Error(String(payload.error || "Remote session compact failed"));
181
+ }
182
+ }
183
+
169
184
  async subscribe(params: {
170
185
  session_id: string;
171
186
  on_ready: () => void;
@@ -83,6 +83,10 @@ export class RpcRemoteAgentTransport implements RemoteAgentTransport {
83
83
  return await this.client.stop_session(session_id);
84
84
  }
85
85
 
86
+ async compact(session_id: string): Promise<void> {
87
+ await this.client.compact_session(session_id);
88
+ }
89
+
86
90
  async subscribe(params: {
87
91
  session_id: string;
88
92
  on_ready: () => void;
@@ -342,6 +342,7 @@ export class Executor implements SessionExecutor {
342
342
  );
343
343
  return result;
344
344
  } finally {
345
+ await this.release_step_plugins(run_context);
345
346
  this.recovery_policy.reset_run_state();
346
347
  if (this.abort_controller === abort_controller) {
347
348
  this.abort_controller = null;
@@ -364,6 +365,7 @@ export class Executor implements SessionExecutor {
364
365
  throw new Error("Executor.run requires historyComposer.sessionId");
365
366
  }
366
367
 
368
+ await this.refresh_step_runtime(run_context);
367
369
  const composed_context = await this.contextComposer.compose(run_context);
368
370
  const tools = this.bind_run_scope_to_tools(composed_context.tools, run_context);
369
371
  const system = await this.systemComposer.resolve(run_context);
@@ -386,14 +388,17 @@ export class Executor implements SessionExecutor {
386
388
  };
387
389
  const context_window = this.get_model_context_window?.();
388
390
 
389
- await this.compactionComposer.run({
390
- historyStore: this.historyStore,
391
- model,
392
- ...(context_window !== undefined ? { context_window } : {}),
393
- system,
394
- retryCount: retry_count,
395
- onAction: emit_compaction_action,
396
- });
391
+ if (retry_count > 0) {
392
+ await this.compactionComposer.run({
393
+ historyStore: this.historyStore,
394
+ model,
395
+ ...(context_window !== undefined ? { context_window } : {}),
396
+ system,
397
+ retryCount: retry_count,
398
+ force: true,
399
+ onAction: emit_compaction_action,
400
+ });
401
+ }
397
402
  } catch (error) {
398
403
  await this.emitAction(run_context, {
399
404
  type: "action",
@@ -440,31 +445,79 @@ export class Executor implements SessionExecutor {
440
445
  execute_input: input,
441
446
  model,
442
447
  run_context,
443
- resolve_model_turn: async () =>
444
- await this.resolve_model_turn(run_context),
448
+ resolve_step_inputs: async () =>
449
+ await this.resolve_step_inputs(run_context),
445
450
  });
446
451
  }
447
452
 
448
453
  /**
449
- * 解析下一次 provider 请求实际使用的运行配置。
454
+ * Assistant writer 完成后归档当前 canonical 历史。
455
+ *
456
+ * 关键点(中文):持久化 compact 不能发生在流式 Assistant 草稿仍打开时,
457
+ * 因此由 SessionTurnService 在 writer complete/fail 之后显式调用。
458
+ */
459
+ async compact_history(run_context: SessionRunContext): Promise<{
460
+ compacted: boolean;
461
+ reason?: string;
462
+ }> {
463
+ let compaction_action_id = "";
464
+ try {
465
+ await this.refresh_step_runtime(run_context);
466
+ const model = this.resolveModelOrThrow();
467
+ const system = await this.systemComposer.resolve(run_context);
468
+ const emit_compaction_action = async (
469
+ action: SessionActionRecordV1,
470
+ ): Promise<void> => {
471
+ if (action.state === "running") compaction_action_id = action.id;
472
+ await this.emitAction(run_context, action);
473
+ };
474
+ const context_window = this.get_model_context_window?.();
475
+ return await this.compactionComposer.run({
476
+ historyStore: this.historyStore,
477
+ model,
478
+ ...(context_window !== undefined ? { context_window } : {}),
479
+ system,
480
+ retryCount: 0,
481
+ force: true,
482
+ onAction: emit_compaction_action,
483
+ });
484
+ } catch (error) {
485
+ await this.emitAction(run_context, {
486
+ type: "action",
487
+ id:
488
+ compaction_action_id ||
489
+ `compacting:${this.sessionId}:failed:${Date.now()}:${generateId()}`,
490
+ title: "Session records compact failed",
491
+ description: error instanceof Error ? error.message : String(error),
492
+ state: "failed",
493
+ metadata: {
494
+ v: 1,
495
+ ts: Date.now(),
496
+ sessionId: this.sessionId,
497
+ },
498
+ });
499
+ return { compacted: false, reason: "compact_failed" };
500
+ } finally {
501
+ await this.release_step_plugins(run_context);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * 解析下一 Session step 实际使用的运行配置。
450
507
  *
451
508
  * 关键点(中文)
452
509
  * - 调用方必须先提交 Session 统一输入队列,再调用本方法。
453
510
  * - 每次调用只读取一次 model、system 与 tools,并把它们传给同一个 `streamText()`。
454
511
  */
455
- private async resolve_model_turn(run_context: SessionRunContext): Promise<{
512
+ private async resolve_step_inputs(run_context: SessionRunContext): Promise<{
456
513
  model: LanguageModel;
457
514
  system: SessionExecuteInput["system"];
458
515
  tools: SessionExecuteInput["tools"];
516
+ context_window?: number;
459
517
  }> {
460
- run_context.agentEnv = Object.freeze({ ...this.getEnv() });
461
- if (this.get_systems) {
462
- run_context.agentSystems = Object.freeze([...this.get_systems()]);
463
- }
464
- if (this.get_plugins) {
465
- run_context.agentPlugins = this.get_plugins();
466
- }
518
+ await this.refresh_step_runtime(run_context);
467
519
  const composed_context = await this.contextComposer.compose(run_context);
520
+ const context_window = this.get_model_context_window?.();
468
521
  return {
469
522
  model: this.resolveModelOrThrow(),
470
523
  system: await this.systemComposer.resolve(run_context),
@@ -472,9 +525,42 @@ export class Executor implements SessionExecutor {
472
525
  composed_context.tools,
473
526
  run_context,
474
527
  ),
528
+ ...(context_window !== undefined ? { context_window } : {}),
475
529
  };
476
530
  }
477
531
 
532
+ /**
533
+ * 刷新当前 Session step 的 effective Agent 运行视图。
534
+ */
535
+ private async refresh_step_runtime(
536
+ run_context: SessionRunContext,
537
+ ): Promise<void> {
538
+ await this.release_step_plugins(run_context);
539
+ run_context.agentEnv = Object.freeze({ ...this.getEnv() });
540
+ if (this.get_systems) {
541
+ run_context.agentSystems = Object.freeze([...this.get_systems()]);
542
+ } else {
543
+ delete run_context.agentSystems;
544
+ }
545
+ if (this.get_plugins) {
546
+ run_context.agentPlugins = this.get_plugins().acquire();
547
+ } else {
548
+ delete run_context.agentPlugins;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * 释放当前 Session step 持有的 Plugin execution lease。
554
+ */
555
+ private async release_step_plugins(
556
+ run_context: SessionRunContext,
557
+ ): Promise<void> {
558
+ const plugins = run_context.agentPlugins;
559
+ if (!plugins) return;
560
+ delete run_context.agentPlugins;
561
+ await plugins.release();
562
+ }
563
+
478
564
  /**
479
565
  * 为所有 tool 的 execute callback 绑定 ShellRunScope。
480
566
  *
@@ -532,6 +618,12 @@ export class Executor implements SessionExecutor {
532
618
  ...(typeof input?.onStepCallback === "function"
533
619
  ? { onStepCallback: input.onStepCallback }
534
620
  : {}),
621
+ ...(typeof input?.hasPendingStepInput === "function"
622
+ ? { hasPendingStepInput: input.hasPendingStepInput }
623
+ : {}),
624
+ ...(typeof input?.consume_history_reload === "function"
625
+ ? { consume_history_reload: input.consume_history_reload }
626
+ : {}),
535
627
  ...(typeof input?.onAssistantStepCallback === "function"
536
628
  ? { onAssistantStepCallback: input.onAssistantStepCallback }
537
629
  : {}),
@@ -47,6 +47,13 @@ export type SessionCompactionInput = {
47
47
  * - 回调由上层 Session 负责转成订阅事件与 action record。
48
48
  */
49
49
  onAction?: (action: SessionActionRecordV1) => Promise<void>;
50
+
51
+ /**
52
+ * 是否明确要求持久化 deep compact。
53
+ *
54
+ * 普通模型调用不应因为历史字符数量而触发 compact;仅 usage 反馈或超限恢复会设置此值。
55
+ */
56
+ force?: boolean;
50
57
  };
51
58
 
52
59
  /**
@@ -11,76 +11,23 @@ import type {
11
11
  SessionCompactionInput,
12
12
  } from "@executor/composer/compaction/SessionCompactionComposer.js";
13
13
 
14
- type JsonlSessionCompactionComposerOptions = {
15
- keepLastMessages?: number;
16
- maxInputTokensApprox?: number;
17
- compactRatio?: number;
18
- };
19
-
20
14
  /**
21
15
  * JsonlSessionCompactionComposer 默认实现。
22
16
  */
23
17
  export class JsonlSessionCompactionComposer implements SessionCompactionComposer {
24
18
  readonly name = "summary_compaction_composer";
25
- private readonly options: JsonlSessionCompactionComposerOptions;
26
-
27
- constructor(options?: JsonlSessionCompactionComposerOptions) {
28
- this.options = options || {};
29
- }
30
-
31
- private resolvePolicy(retryCount: number, context_window?: number): {
32
- keepLastMessages: number;
33
- maxInputTokensApprox: number;
34
- compactRatio: number;
35
- } {
36
- const baseKeepLastMessages =
37
- typeof this.options.keepLastMessages === "number"
38
- ? Math.max(6, Math.min(5000, Math.floor(this.options.keepLastMessages)))
39
- : 30;
40
- const configured_max_input_tokens =
41
- typeof this.options.maxInputTokensApprox === "number"
42
- && Number.isFinite(this.options.maxInputTokensApprox)
43
- ? Math.max(2000, Math.floor(this.options.maxInputTokensApprox))
44
- : undefined;
45
- const model_context_window =
46
- Number.isSafeInteger(context_window) && Number(context_window) > 0
47
- ? Number(context_window)
48
- : undefined;
49
- const baseMaxInputTokensApprox = configured_max_input_tokens
50
- ?? (model_context_window !== undefined
51
- ? Math.max(1, Math.floor(model_context_window * 0.8))
52
- : 128000);
53
- const retryFactor = Math.max(1, Math.pow(2, retryCount));
54
- const keepLastMessages = Math.max(6, Math.floor(baseKeepLastMessages / retryFactor));
55
- const minimum_input_tokens = Math.min(2000, baseMaxInputTokensApprox);
56
- const maxInputTokensApprox = Math.max(
57
- minimum_input_tokens,
58
- Math.floor(baseMaxInputTokensApprox / retryFactor),
59
- );
60
- const compactRatioRaw =
61
- typeof this.options.compactRatio === "number" &&
62
- Number.isFinite(this.options.compactRatio)
63
- ? this.options.compactRatio
64
- : 0.5;
65
- const compactRatio = Math.max(0.1, Math.min(0.9, compactRatioRaw));
66
- return {
67
- keepLastMessages,
68
- maxInputTokensApprox,
69
- compactRatio,
70
- };
71
- }
72
19
 
73
20
  async run(input: SessionCompactionInput): Promise<{
74
21
  compacted: boolean;
75
22
  reason?: string;
76
23
  }> {
77
- const policy = this.resolvePolicy(input.retryCount, input.context_window);
24
+ if (!input.force && input.retryCount <= 0) {
25
+ return { compacted: false, reason: "not_requested" };
26
+ }
78
27
  return await input.historyStore.compact({
79
28
  model: input.model,
80
29
  system: input.system,
81
- keepLastMessages: policy.keepLastMessages,
82
- maxInputTokensApprox: policy.maxInputTokensApprox,
83
- compactRatio: policy.compactRatio,
30
+ force: true,
84
31
  ...(input.onAction ? { onAction: input.onAction } : {}),
85
32
  });
86
33
  }