@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
@@ -55,7 +55,7 @@ import { SessionStateService } from "@/session/services/SessionStateService.js";
55
55
  import { SessionTurnService } from "@/session/services/SessionTurnService.js";
56
56
  import { SessionViewService } from "@/session/services/SessionViewService.js";
57
57
  import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
58
- import type { AgentSessionConfigMutation } from "@/types/session/SessionConfigMutation.js";
58
+ import type { AgentSessionCommand } from "@/types/session/SessionQueueCommand.js";
59
59
  import type {
60
60
  SessionComposerFactoryContext,
61
61
  SessionComposerInput,
@@ -243,8 +243,8 @@ export class Session implements AgentSession {
243
243
  */
244
244
  async set(input: AgentSessionSetInput): Promise<void> {
245
245
  const configured = await this.stateService.set(input);
246
- if (configured.mutation) {
247
- this.turnService.enqueue_config(configured.mutation);
246
+ if (configured.command) {
247
+ this.turnService.enqueue_command(configured.command);
248
248
  }
249
249
  }
250
250
 
@@ -263,39 +263,47 @@ export class Session implements AgentSession {
263
263
  }
264
264
 
265
265
  /**
266
- * Agent configured state 修改加入当前 Session 的统一输入队列。
266
+ * 把一次显式历史压缩加入当前 Session 的有序输入队列。
267
267
  */
268
- enqueue_agent_config(mutation: AgentSessionConfigMutation): void {
269
- this.turnService.enqueue_config({
270
- mutation_id: mutation.mutation_id,
268
+ async compact(): Promise<void> {
269
+ await this.turnService.compact();
270
+ }
271
+
272
+ /**
273
+ * 把 Agent configured state command 加入当前 Session 的统一输入队列。
274
+ */
275
+ enqueue_agent_command(command: AgentSessionCommand): void {
276
+ this.turnService.enqueue_command({
277
+ type: "command",
278
+ command_id: command.command_id,
271
279
  scope: "agent",
272
- apply: async ({ turn_id }) => {
273
- if (mutation.type === "instruction") {
274
- this.effective_instruction_system_blocks = mutation
280
+ execute: async ({ turn_id }) => {
281
+ if (command.type === "instruction") {
282
+ this.effective_instruction_system_blocks = command
275
283
  .instruction_blocks
276
284
  .map((block) => ({ ...block }));
277
- await this.stateService.emit_action_event({
278
- id: `agent-instruction:${this.id}:${mutation.mutation_id}`,
285
+ await this.stateService.emit_config_action_event({
286
+ id: `agent-instruction:${this.id}:${command.command_id}`,
279
287
  title: "Agent instruction updated",
280
288
  state: "completed",
281
289
  turnId: turn_id,
282
290
  });
283
291
  return;
284
292
  }
285
- if (mutation.type === "env") {
286
- this.effective_agent_env = { ...mutation.env };
287
- await this.stateService.emit_action_event({
288
- id: `agent-env:${this.id}:${mutation.mutation_id}`,
293
+ if (command.type === "env") {
294
+ this.effective_agent_env = { ...command.env };
295
+ await this.stateService.emit_config_action_event({
296
+ id: `agent-env:${this.id}:${command.command_id}`,
289
297
  title: "Agent environment updated",
290
298
  state: "completed",
291
299
  turnId: turn_id,
292
300
  });
293
301
  return;
294
302
  }
295
- this.effective_agent_plugins = mutation.plugins;
296
- await this.stateService.emit_action_event({
297
- id: `agent-plugins:${this.id}:${mutation.mutation_id}`,
298
- title: mutation.title,
303
+ this.effective_agent_plugins = command.plugins;
304
+ await this.stateService.emit_config_action_event({
305
+ id: `agent-plugins:${this.id}:${command.command_id}`,
306
+ title: command.title,
299
307
  state: "completed",
300
308
  turnId: turn_id,
301
309
  });
@@ -253,7 +253,10 @@ export class SessionSystemBuilder implements SessionSystemComposer {
253
253
  timezone: this.getSessionTimezone(),
254
254
  getInstructionSystemBlocks: this.getInstructionSystemBlocks,
255
255
  getManagedPluginSystemBlocks: this.getManagedPluginSystemBlocks,
256
- getPluginSystemBlocks: this.getPluginSystemBlocks,
256
+ getPluginSystemBlocks: async () =>
257
+ run_context.agentPlugins
258
+ ? await run_context.agentPlugins.systemBlocks()
259
+ : await this.getPluginSystemBlocks(),
257
260
  });
258
261
  }
259
262
  }
@@ -13,10 +13,23 @@ import {
13
13
  build_update_session_compaction_prompt,
14
14
  SESSION_COMPACTION_SYSTEM_PROMPT,
15
15
  } from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
16
+ import { fold_compacted_text } from "@executor/core-engine/CoreEngineContextCompaction.js";
16
17
  import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
17
18
  import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
18
19
  import type { SessionMessage } from "@/types/session/SessionMessage.js";
19
20
 
21
+ /** 单次摘要请求中 conversation 文本的最大字符数。 */
22
+ const SUMMARY_CHUNK_MAX_CHARS = 20_000;
23
+
24
+ /** 单条 canonical Message 投影到摘要输入后的最大字符数。 */
25
+ const SUMMARY_MESSAGE_MAX_CHARS = 12_000;
26
+
27
+ /** 上一轮累计 Summary 进入下一次 reduce 请求时的最大字符数。 */
28
+ const PREVIOUS_SUMMARY_MAX_CHARS = 12_000;
29
+
30
+ /** 摘要模型的最大输出 token,用于避免 Summary 自身无限增长。 */
31
+ const SUMMARY_MAX_OUTPUT_TOKENS = 4_000;
32
+
20
33
  /** 必要时把 Active 前缀关闭为带累计 Summary 的 Segment。 */
21
34
  export async function compact_session_recorder_messages(input: {
22
35
  /** 当前 Session Recorder。 */
@@ -28,31 +41,10 @@ export async function compact_session_recorder_messages(input: {
28
41
  const context_messages = snapshot.messages.filter(
29
42
  (message) => message.type === "user" || message.type === "assistant",
30
43
  );
31
- if (context_messages.length < 4) {
32
- return { compacted: false, reason: "small_messages" };
33
- }
34
- const system_text = input.compact_input.system
35
- .map((message) => String(message.content || ""))
36
- .join("\n\n");
37
- const estimated_tokens = Math.ceil(
38
- `${system_text}\n${snapshot.summary?.text || ""}\n${JSON.stringify(context_messages)}`.length / 3,
39
- );
40
- if (estimated_tokens <= input.compact_input.maxInputTokensApprox) {
41
- return { compacted: false, reason: "under_budget" };
42
- }
43
-
44
- const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
45
- const keep_last_messages = Math.max(1, input.compact_input.keepLastMessages);
46
- const max_compact_count = context_messages.length - keep_last_messages;
47
- if (max_compact_count < 2) {
48
- return { compacted: false, reason: "keep_last_messages" };
44
+ if (!input.compact_input.force) {
45
+ return { compacted: false, reason: "not_requested" };
49
46
  }
50
- const compact_count = Math.min(
51
- max_compact_count,
52
- Math.max(2, Math.floor(context_messages.length * ratio)),
53
- );
54
- const older = context_messages.slice(0, compact_count);
55
- const boundary = older.at(-1);
47
+ const boundary = context_messages.at(-1);
56
48
  if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
57
49
 
58
50
  const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
@@ -63,47 +55,42 @@ export async function compact_session_recorder_messages(input: {
63
55
  state: "running",
64
56
  });
65
57
 
66
- const conversation_text = older
58
+ const conversation_messages = context_messages
67
59
  .map((message) => message_to_compaction_text(message))
68
- .filter(Boolean)
69
- .join("\n");
70
- const previous_summary_text = String(snapshot.summary?.text || "").trim();
71
- const prompt = previous_summary_text
72
- ? build_update_session_compaction_prompt({
73
- previous_summary: previous_summary_text,
74
- new_conversation_text: conversation_text || "(none)",
75
- })
76
- : build_initial_session_compaction_prompt({
77
- conversation_text: conversation_text || "(none)",
60
+ .filter(Boolean);
61
+ const chunks = build_summary_chunks(conversation_messages);
62
+ let summary = String(snapshot.summary?.text || "").trim();
63
+ let used_fallback = false;
64
+ for (const chunk of chunks) {
65
+ const prompt = summary
66
+ ? build_update_session_compaction_prompt({
67
+ previous_summary: fold_compacted_text(
68
+ summary,
69
+ PREVIOUS_SUMMARY_MAX_CHARS,
70
+ ),
71
+ new_conversation_text: chunk || "(none)",
72
+ })
73
+ : build_initial_session_compaction_prompt({
74
+ conversation_text: chunk || "(none)",
75
+ });
76
+ try {
77
+ const result = await generateText({
78
+ model: input.compact_input.model,
79
+ system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
80
+ prompt,
81
+ maxOutputTokens: SUMMARY_MAX_OUTPUT_TOKENS,
78
82
  });
79
-
80
- let summary: string;
81
- try {
82
- const result = await generateText({
83
- model: input.compact_input.model,
84
- system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
85
- prompt,
86
- });
87
- summary = String(result.text || "").trim();
88
- } catch (error) {
89
- await publish_action(input.compact_input.onAction, {
90
- id: action_id,
91
- session_id: input.recorder.session_id,
92
- title: "Session message compaction failed",
93
- description: error instanceof Error ? error.message : String(error),
94
- state: "failed",
95
- });
96
- return { compacted: false, reason: "summary_failed" };
83
+ const next_summary = String(result.text || "").trim();
84
+ if (!next_summary) throw new Error("Compaction model returned an empty Summary");
85
+ summary = next_summary;
86
+ } catch {
87
+ used_fallback = true;
88
+ summary = build_deterministic_summary(summary, chunk);
89
+ }
97
90
  }
98
91
  if (!summary) {
99
- await publish_action(input.compact_input.onAction, {
100
- id: action_id,
101
- session_id: input.recorder.session_id,
102
- title: "Session message compaction failed",
103
- description: "The compaction model returned an empty Summary.",
104
- state: "failed",
105
- });
106
- return { compacted: false, reason: "summary_failed" };
92
+ used_fallback = true;
93
+ summary = build_deterministic_summary("", "(no textual context)");
107
94
  }
108
95
 
109
96
  await input.recorder.compact_active({
@@ -121,7 +108,9 @@ export async function compact_session_recorder_messages(input: {
121
108
  id: action_id,
122
109
  session_id: input.recorder.session_id,
123
110
  title: "Session messages compacted",
124
- description: `Closed Active through Message ${boundary.message_id}.`,
111
+ description: used_fallback
112
+ ? `Closed Active through Message ${boundary.message_id} with deterministic fallback Summary.`
113
+ : `Closed Active through Message ${boundary.message_id}.`,
125
114
  state: "completed",
126
115
  });
127
116
  return { compacted: true };
@@ -130,10 +119,69 @@ export async function compact_session_recorder_messages(input: {
130
119
  function message_to_compaction_text(message: SessionMessage): string {
131
120
  const projected = to_executor_ui_message(message);
132
121
  if (!projected) return "";
133
- return JSON.stringify({
122
+ const parts = projected.parts
123
+ .filter((part) => part.type !== "reasoning")
124
+ .map((part) => {
125
+ const serialized = safe_stringify(part);
126
+ return serialized.length <= SUMMARY_MESSAGE_MAX_CHARS
127
+ ? part
128
+ : {
129
+ type: part.type,
130
+ compacted_preview: fold_compacted_text(
131
+ serialized,
132
+ SUMMARY_MESSAGE_MAX_CHARS,
133
+ ),
134
+ };
135
+ });
136
+ return fold_compacted_text(safe_stringify({
134
137
  role: projected.role,
135
- parts: projected.parts,
136
- });
138
+ parts,
139
+ }), SUMMARY_MESSAGE_MAX_CHARS);
140
+ }
141
+
142
+ /** 按 Message 边界把摘要输入拆为固定字符上限的 chunk。 */
143
+ function build_summary_chunks(messages: string[]): string[] {
144
+ const chunks: string[] = [];
145
+ let current = "";
146
+ for (const message of messages) {
147
+ const value = fold_compacted_text(message, SUMMARY_CHUNK_MAX_CHARS);
148
+ const candidate = current ? `${current}\n${value}` : value;
149
+ if (current && candidate.length > SUMMARY_CHUNK_MAX_CHARS) {
150
+ chunks.push(current);
151
+ current = value;
152
+ continue;
153
+ }
154
+ current = candidate;
155
+ }
156
+ if (current) chunks.push(current);
157
+ return chunks;
158
+ }
159
+
160
+ /** 摘要模型不可用时仍可完成归档的确定性 checkpoint。 */
161
+ function build_deterministic_summary(
162
+ previous_summary: string,
163
+ new_context: string,
164
+ ): string {
165
+ const body = [
166
+ String(previous_summary || "").trim(),
167
+ String(new_context || "").trim(),
168
+ ]
169
+ .filter(Boolean)
170
+ .join("\n\n");
171
+ return [
172
+ "## Compacted Context",
173
+ "The summary model was unavailable. The following deterministic checkpoint preserves the beginning and latest context.",
174
+ "",
175
+ fold_compacted_text(body || "(none)", PREVIOUS_SUMMARY_MAX_CHARS),
176
+ ].join("\n");
177
+ }
178
+
179
+ function safe_stringify(value: unknown): string {
180
+ try {
181
+ return JSON.stringify(value) || String(value || "");
182
+ } catch {
183
+ return String(value || "");
184
+ }
137
185
  }
138
186
 
139
187
  async function publish_action(
@@ -16,7 +16,7 @@ import type {
16
16
  import type { SessionMutation } from "@/types/session/SessionMutation.js";
17
17
  import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
18
18
  import type { AgentSessionStopResult } from "@/types/sdk/AgentSessionStop.js";
19
- import type { SessionRuntimeConfigMutation } from "@/types/session/SessionConfigMutation.js";
19
+ import type { SessionQueueCommand } from "@/types/session/SessionQueueCommand.js";
20
20
  import type {
21
21
  AgentSessionTurnHandle,
22
22
  AgentSessionTurnResult,
@@ -26,7 +26,7 @@ const TURN_STOPPED_MESSAGE = "Turn stopped";
26
26
  const QUEUED_PROMPT_CANCELLED_MESSAGE =
27
27
  "Prompt cancelled because session was stopped";
28
28
 
29
- type QueuedPrompt = {
29
+ type SessionQueuePrompt = {
30
30
  /** 当前队列项固定为 prompt。 */
31
31
  type: "prompt";
32
32
 
@@ -42,20 +42,9 @@ type QueuedPrompt = {
42
42
  };
43
43
 
44
44
  /**
45
- * 等待在模型 turn 边界提交的配置队列项。
45
+ * Session queue 的统一有序输入。
46
46
  */
47
- type QueuedConfigMutation = {
48
- /** 当前队列项固定为配置 mutation。 */
49
- type: "config";
50
-
51
- /** 当前待提交的配置 mutation。 */
52
- mutation: SessionRuntimeConfigMutation;
53
- };
54
-
55
- /**
56
- * Session actor 的统一有序输入。
57
- */
58
- type QueuedSessionInput = QueuedPrompt | QueuedConfigMutation;
47
+ type SessionQueueInput = SessionQueuePrompt | SessionQueueCommand;
59
48
 
60
49
  /**
61
50
  * Promise 延迟控制器。
@@ -95,9 +84,6 @@ interface ActiveTurnState {
95
84
  * 当前 turn 的取消控制器。
96
85
  */
97
86
  abortController: AbortController;
98
-
99
- /** 当前公开 turn 已开始的模型 turn 数量。 */
100
- model_turn_index: number;
101
87
  }
102
88
 
103
89
  /**
@@ -162,7 +148,7 @@ export class SessionPromptRuntime {
162
148
  private readonly appendErrorMessage: SessionPromptRuntimeOptions["appendErrorMessage"];
163
149
  private readonly executeTurn: SessionPromptRuntimeOptions["executeTurn"];
164
150
  private readonly stopTurn: SessionPromptRuntimeOptions["stopTurn"];
165
- private readonly queue: QueuedSessionInput[] = [];
151
+ private readonly queue: SessionQueueInput[] = [];
166
152
  private processingPromise: Promise<void> | null = null;
167
153
  private activeTurn: ActiveTurnState | null = null;
168
154
 
@@ -193,22 +179,26 @@ export class SessionPromptRuntime {
193
179
  }
194
180
 
195
181
  /**
196
- * 把一次已成功写入 configured state 的配置修改加入统一输入队列。
182
+ * 把一次已成功创建的 command 加入统一输入队列。
197
183
  */
198
- enqueue_config(mutation: SessionRuntimeConfigMutation): void {
199
- this.queue.push({
200
- type: "config",
201
- mutation,
202
- });
184
+ enqueue_command(command: SessionQueueCommand): void {
185
+ this.queue.push(command);
203
186
  }
204
187
 
205
188
  /**
206
- * 判断是否存在等待并入模型 turn 的 prompt。
189
+ * 判断是否存在等待并入下一 Session step 的 prompt。
207
190
  */
208
191
  has_pending_prompt(): boolean {
209
192
  return this.queue.some((item) => item.type === "prompt");
210
193
  }
211
194
 
195
+ /**
196
+ * 判断是否存在等待在下一 Session step 检查点执行的 command。
197
+ */
198
+ has_pending_command(): boolean {
199
+ return this.queue.some((item) => item.type === "command");
200
+ }
201
+
212
202
  /**
213
203
  * 返回当前 actor prompt 调度器是否仍处于活跃态。
214
204
  *
@@ -281,9 +271,9 @@ export class SessionPromptRuntime {
281
271
  current.deferredHandle.resolve(createTurnHandle(activeTurn));
282
272
 
283
273
  try {
284
- await this.apply_config_mutations(
274
+ await this.execute_commands(
285
275
  queued_before_prompt.filter(
286
- (item): item is QueuedConfigMutation => item.type === "config",
276
+ (item): item is SessionQueueCommand => item.type === "command",
287
277
  ),
288
278
  activeTurn,
289
279
  );
@@ -368,10 +358,10 @@ export class SessionPromptRuntime {
368
358
 
369
359
  private cancelQueuedPrompts(): number {
370
360
  const cancelled = this.queue.filter(
371
- (item): item is QueuedPrompt => item.type === "prompt",
361
+ (item): item is SessionQueuePrompt => item.type === "prompt",
372
362
  );
373
363
  if (cancelled.length <= 0) return 0;
374
- const retained = this.queue.filter((item) => item.type === "config");
364
+ const retained = this.queue.filter((item) => item.type === "command");
375
365
  this.queue.splice(0, this.queue.length, ...retained);
376
366
  for (const item of cancelled) {
377
367
  const turnId = `turn:${this.sessionId}:cancelled:${Date.now()}:${nanoid(6)}`;
@@ -414,26 +404,24 @@ export class SessionPromptRuntime {
414
404
  }
415
405
 
416
406
  /**
417
- * 在下一模型 turn 开始前按入队顺序提交配置并持久化 steer。
407
+ * 在下一 Session step 检查点按入队顺序提交配置并持久化 steer。
418
408
  */
419
409
  private async drain_queued_inputs(
420
410
  activeTurn: ActiveTurnState,
421
411
  ): Promise<SessionUserMessageV1[]> {
422
- activeTurn.model_turn_index += 1;
423
412
  if (this.queue.length <= 0) return [];
424
413
  const drained = this.queue.splice(0, this.queue.length);
425
414
  const merged: SessionUserMessageV1[] = [];
426
415
 
427
416
  for (let index = 0; index < drained.length; index += 1) {
428
417
  const item = drained[index];
429
- if (item.type === "config") {
418
+ if (item.type === "command") {
430
419
  try {
431
- await item.mutation.apply({
420
+ await item.execute({
432
421
  turn_id: activeTurn.turnId,
433
- model_turn_index: activeTurn.model_turn_index,
434
422
  });
435
423
  } catch {
436
- // 配置实现负责写 failed action;单条失败不能吞掉后续 steer 或配置。
424
+ // command 自己负责失败观测;单条失败不能吞掉后续 steer 或 command。
437
425
  }
438
426
  continue;
439
427
  }
@@ -457,22 +445,20 @@ export class SessionPromptRuntime {
457
445
  }
458
446
 
459
447
  /**
460
- * 提交一组已经从统一队列截取的配置 mutation
448
+ * 执行一组已经从统一队列截取的 command
461
449
  */
462
- private async apply_config_mutations(
463
- mutations: QueuedConfigMutation[],
450
+ private async execute_commands(
451
+ commands: SessionQueueCommand[],
464
452
  active_turn: ActiveTurnState,
465
453
  ): Promise<void> {
466
- if (mutations.length <= 0) return;
467
- active_turn.model_turn_index += 1;
468
- for (const item of mutations) {
454
+ if (commands.length <= 0) return;
455
+ for (const command of commands) {
469
456
  try {
470
- await item.mutation.apply({
457
+ await command.execute({
471
458
  turn_id: active_turn.turnId,
472
- model_turn_index: active_turn.model_turn_index,
473
459
  });
474
460
  } catch {
475
- // 配置实现负责写 failed action;单条失败不能阻断当前 prompt。
461
+ // command 自己负责失败观测;单条失败不能阻断当前 prompt。
476
462
  }
477
463
  }
478
464
  }
@@ -516,7 +502,6 @@ function createActiveTurnState(turnId: string): ActiveTurnState {
516
502
  result: null,
517
503
  deferredFinished: createDeferred<AgentSessionTurnResult>(),
518
504
  abortController: new AbortController(),
519
- model_turn_index: 0,
520
505
  };
521
506
  }
522
507
 
@@ -37,7 +37,7 @@ import type {
37
37
  SessionUserMessageV1,
38
38
  } from "@/executor/types/SessionRecords.js";
39
39
  import type { SessionLocalState } from "@/types/session/SessionLocalState.js";
40
- import type { SessionRuntimeConfigMutation } from "@/types/session/SessionConfigMutation.js";
40
+ import type { SessionQueueCommand } from "@/types/session/SessionQueueCommand.js";
41
41
  import { generateId } from "@/utils/Id.js";
42
42
  import type { Logger } from "@/utils/logger/Logger.js";
43
43
  import type { AgentModel } from "@/model/CityModelAdapter.js";
@@ -119,15 +119,15 @@ type EmitActionInput = SessionActionRecordInputV1 | SessionActionRecordV1;
119
119
  /**
120
120
  * Session 配置成功写入后的队列提交结果。
121
121
  */
122
- export interface SessionConfiguredMutationResult {
122
+ export interface SessionConfiguredCommandResult {
123
123
  /**
124
- * 等待在下一模型 turn 提交的 mutation
124
+ * 等待在下一 Session step 检查点执行的 command
125
125
  *
126
126
  * 说明(中文)
127
127
  * - 输入未产生实际配置变化时为空。
128
- * - fork 等内部初始化路径可以选择立即提交而不返回 mutation
128
+ * - fork 等内部初始化路径可以选择立即提交而不返回 command
129
129
  */
130
- mutation?: SessionRuntimeConfigMutation;
130
+ command?: SessionQueueCommand;
131
131
  }
132
132
 
133
133
  /**
@@ -267,7 +267,7 @@ export class SessionStateService {
267
267
  async set(
268
268
  input: AgentSessionSetInput,
269
269
  options?: SessionSetOptions,
270
- ): Promise<SessionConfiguredMutationResult> {
270
+ ): Promise<SessionConfiguredCommandResult> {
271
271
  const should_emit_action = options?.emit_action !== false;
272
272
  const previous_model_label = this.state.sessionConfig.modelLabel;
273
273
  const previous_model_id = String(
@@ -320,7 +320,7 @@ export class SessionStateService {
320
320
  ...next_config,
321
321
  };
322
322
  if (!should_emit_model_switch_action) return;
323
- await this.emit_action_event({
323
+ await this.emit_config_action_event({
324
324
  id: action_id,
325
325
  title: `Session model switched from ${previous_model_name} to ${next_model_name}`,
326
326
  state: "completed",
@@ -333,26 +333,14 @@ export class SessionStateService {
333
333
  return {};
334
334
  }
335
335
 
336
- const mutation_id = generateId();
336
+ const command_id = generateId();
337
337
  return {
338
- mutation: {
339
- mutation_id,
338
+ command: {
339
+ type: "command",
340
+ command_id,
340
341
  scope: "session",
341
- apply: async ({ turn_id }) => {
342
- try {
343
- await apply_effective_config(turn_id);
344
- } catch (error) {
345
- if (should_emit_model_switch_action) {
346
- await this.emit_action_event({
347
- id: action_id,
348
- title: `Session model switch from ${previous_model_name} to ${next_model_name} failed`,
349
- description: error instanceof Error ? error.message : String(error),
350
- state: "failed",
351
- turnId: turn_id,
352
- });
353
- }
354
- throw error;
355
- }
342
+ execute: async ({ turn_id }) => {
343
+ await apply_effective_config(turn_id);
356
344
  },
357
345
  },
358
346
  };
@@ -547,6 +535,27 @@ export class SessionStateService {
547
535
  });
548
536
  }
549
537
 
538
+ /**
539
+ * 尽力写入配置生效 action,不让 timeline 故障改变 effective state。
540
+ */
541
+ async emit_config_action_event(input: EmitActionInput): Promise<boolean> {
542
+ try {
543
+ await this.emit_action_event(input);
544
+ return true;
545
+ } catch (error) {
546
+ try {
547
+ await this.logger.log("warn", "[agent] config action persistence failed", {
548
+ sessionId: this.session_id,
549
+ actionId: String(input.id || ""),
550
+ error: error instanceof Error ? error.message : String(error),
551
+ });
552
+ } catch {
553
+ // 配置已经提交,日志失败也不能反向改变 effective state。
554
+ }
555
+ return false;
556
+ }
557
+ }
558
+
550
559
  /**
551
560
  * 构造并持久化一条 prompt user 消息。
552
561
  */