@downcity/agent 1.1.222 → 1.1.224

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 (166) hide show
  1. package/bin/agent/local/Agent.d.ts +1 -34
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +1 -44
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
  6. package/bin/agent/local/services/AgentAssemblyService.js +35 -2
  7. package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
  8. package/bin/agent/local/services/AgentSessions.d.ts +6 -0
  9. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  10. package/bin/agent/local/services/AgentSessions.js +42 -0
  11. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  12. package/bin/agent/remote/RemoteAgent.d.ts +1 -35
  13. package/bin/agent/remote/RemoteAgent.d.ts.map +1 -1
  14. package/bin/agent/remote/RemoteAgent.js +1 -37
  15. package/bin/agent/remote/RemoteAgent.js.map +1 -1
  16. package/bin/agent/remote/RemoteSession.d.ts +24 -45
  17. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  18. package/bin/agent/remote/RemoteSession.js +74 -121
  19. package/bin/agent/remote/RemoteSession.js.map +1 -1
  20. package/bin/agent/remote/RemoteTransport.d.ts +11 -27
  21. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  22. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +7 -21
  23. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  24. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -47
  25. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  26. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +7 -20
  27. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  28. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -17
  29. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  30. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -2
  31. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  32. package/bin/executor/types/SessionHistoryPaths.d.ts +1 -1
  33. package/bin/index.d.ts +3 -3
  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/rpc/Client.d.ts +7 -36
  38. package/bin/rpc/Client.d.ts.map +1 -1
  39. package/bin/rpc/Client.js +28 -78
  40. package/bin/rpc/Client.js.map +1 -1
  41. package/bin/session/Session.d.ts +16 -8
  42. package/bin/session/Session.d.ts.map +1 -1
  43. package/bin/session/Session.js +66 -21
  44. package/bin/session/Session.js.map +1 -1
  45. package/bin/session/browse/Browse.d.ts.map +1 -1
  46. package/bin/session/browse/Browse.js +49 -18
  47. package/bin/session/browse/Browse.js.map +1 -1
  48. package/bin/session/recorder/JsonlSessionMessageStore.d.ts +30 -24
  49. package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
  50. package/bin/session/recorder/JsonlSessionMessageStore.js +163 -59
  51. package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
  52. package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -1
  53. package/bin/session/recorder/SessionMessageCodec.js +6 -1
  54. package/bin/session/recorder/SessionMessageCodec.js.map +1 -1
  55. package/bin/session/recorder/SessionRecorder.d.ts +20 -11
  56. package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
  57. package/bin/session/recorder/SessionRecorder.js +148 -103
  58. package/bin/session/recorder/SessionRecorder.js.map +1 -1
  59. package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
  60. package/bin/session/recorder/SessionRecorderCompaction.js +1 -0
  61. package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
  62. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +5 -5
  63. package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
  64. package/bin/session/recorder/SessionRecorderHistoryStore.js +5 -5
  65. package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
  66. package/bin/session/runtime/SessionEventHub.d.ts +12 -5
  67. package/bin/session/runtime/SessionEventHub.d.ts.map +1 -1
  68. package/bin/session/runtime/SessionEventHub.js +11 -6
  69. package/bin/session/runtime/SessionEventHub.js.map +1 -1
  70. package/bin/session/runtime/SessionPromptRuntime.d.ts +2 -2
  71. package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
  72. package/bin/session/runtime/SessionPromptRuntime.js +35 -17
  73. package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
  74. package/bin/session/services/SessionStateService.d.ts +2 -2
  75. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  76. package/bin/session/services/SessionStateService.js +6 -2
  77. package/bin/session/services/SessionStateService.js.map +1 -1
  78. package/bin/session/services/SessionTurnService.d.ts +2 -7
  79. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  80. package/bin/session/services/SessionTurnService.js +2 -8
  81. package/bin/session/services/SessionTurnService.js.map +1 -1
  82. package/bin/session/storage/Paths.d.ts +1 -1
  83. package/bin/session/storage/Paths.d.ts.map +1 -1
  84. package/bin/session/storage/Paths.js +2 -2
  85. package/bin/session/storage/Paths.js.map +1 -1
  86. package/bin/session/storage/RuntimeSessionPort.d.ts +3 -4
  87. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  88. package/bin/session/storage/RuntimeSessionPort.js +2 -2
  89. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  90. package/bin/types/agent/SessionActor.d.ts +11 -7
  91. package/bin/types/agent/SessionActor.d.ts.map +1 -1
  92. package/bin/types/rpc/RpcProtocol.d.ts +35 -62
  93. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  94. package/bin/types/runtime/agent/AgentContext.d.ts +4 -4
  95. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  96. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  97. package/bin/types/session/SessionApproval.d.ts +97 -0
  98. package/bin/types/session/SessionApproval.d.ts.map +1 -0
  99. package/bin/types/session/SessionApproval.js +7 -0
  100. package/bin/types/session/SessionApproval.js.map +1 -0
  101. package/bin/types/session/SessionMessage.d.ts +10 -4
  102. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  103. package/bin/types/session/SessionMutation.d.ts +116 -0
  104. package/bin/types/session/SessionMutation.d.ts.map +1 -0
  105. package/bin/types/session/SessionMutation.js +19 -0
  106. package/bin/types/session/SessionMutation.js.map +1 -0
  107. package/bin/types/session/SessionOptions.d.ts +9 -0
  108. package/bin/types/session/SessionOptions.d.ts.map +1 -1
  109. package/package.json +3 -3
  110. package/scripts/remote-session-reconnect.test.mjs +7 -3
  111. package/scripts/session-history-inflight.test.mjs +2 -2
  112. package/scripts/session-prompt-runtime.test.mjs +3 -3
  113. package/scripts/session-recorder.test.mjs +100 -146
  114. package/src/agent/local/Agent.ts +1 -55
  115. package/src/agent/local/services/AgentAssemblyService.ts +39 -3
  116. package/src/agent/local/services/AgentSessions.ts +47 -0
  117. package/src/agent/remote/RemoteAgent.ts +1 -54
  118. package/src/agent/remote/RemoteSession.ts +105 -163
  119. package/src/agent/remote/RemoteTransport.ts +23 -34
  120. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +33 -90
  121. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +22 -39
  122. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -2
  123. package/src/executor/types/SessionHistoryPaths.ts +1 -1
  124. package/src/index.ts +20 -12
  125. package/src/rpc/Client.ts +48 -106
  126. package/src/session/Session.ts +91 -34
  127. package/src/session/browse/Browse.ts +51 -22
  128. package/src/session/recorder/JsonlSessionMessageStore.ts +170 -86
  129. package/src/session/recorder/SessionMessageCodec.ts +6 -1
  130. package/src/session/recorder/SessionRecorder.ts +183 -124
  131. package/src/session/recorder/SessionRecorderCompaction.ts +1 -0
  132. package/src/session/recorder/SessionRecorderHistoryStore.ts +5 -5
  133. package/src/session/runtime/SessionEventHub.ts +25 -12
  134. package/src/session/runtime/SessionPromptRuntime.ts +37 -19
  135. package/src/session/services/SessionStateService.ts +8 -6
  136. package/src/session/services/SessionTurnService.ts +3 -14
  137. package/src/session/storage/Paths.ts +2 -2
  138. package/src/session/storage/RuntimeSessionPort.ts +8 -11
  139. package/src/types/agent/SessionActor.ts +23 -20
  140. package/src/types/rpc/RpcProtocol.ts +34 -71
  141. package/src/types/runtime/agent/AgentContext.ts +7 -11
  142. package/src/types/session/SessionApproval.ts +108 -0
  143. package/src/types/session/SessionMessage.ts +10 -4
  144. package/src/types/session/SessionMutation.ts +141 -0
  145. package/src/types/session/SessionOptions.ts +25 -0
  146. package/tsconfig.tsbuildinfo +1 -1
  147. package/bin/session/recorder/SessionMessageReducer.d.ts +0 -12
  148. package/bin/session/recorder/SessionMessageReducer.d.ts.map +0 -1
  149. package/bin/session/recorder/SessionMessageReducer.js +0 -109
  150. package/bin/session/recorder/SessionMessageReducer.js.map +0 -1
  151. package/bin/session/runtime/SessionMessageEventHub.d.ts +0 -15
  152. package/bin/session/runtime/SessionMessageEventHub.d.ts.map +0 -1
  153. package/bin/session/runtime/SessionMessageEventHub.js +0 -28
  154. package/bin/session/runtime/SessionMessageEventHub.js.map +0 -1
  155. package/bin/types/sdk/AgentSessionEvent.d.ts +0 -303
  156. package/bin/types/sdk/AgentSessionEvent.d.ts.map +0 -1
  157. package/bin/types/sdk/AgentSessionEvent.js +0 -9
  158. package/bin/types/sdk/AgentSessionEvent.js.map +0 -1
  159. package/bin/types/session/SessionMessageMutation.d.ts +0 -92
  160. package/bin/types/session/SessionMessageMutation.d.ts.map +0 -1
  161. package/bin/types/session/SessionMessageMutation.js +0 -21
  162. package/bin/types/session/SessionMessageMutation.js.map +0 -1
  163. package/src/session/recorder/SessionMessageReducer.ts +0 -155
  164. package/src/session/runtime/SessionMessageEventHub.ts +0 -35
  165. package/src/types/sdk/AgentSessionEvent.ts +0 -372
  166. package/src/types/session/SessionMessageMutation.ts +0 -121
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file 验证 SessionRecorder 单一 Mutation 日志与实时/历史一致性。
2
+ * @file 验证 SessionRecorder Message 快照、Assistant 草稿与实时事件一致性。
3
3
  */
4
4
 
5
5
  import test from "node:test";
@@ -14,10 +14,9 @@ import { SessionRecorderHistoryStore } from "../bin/session/recorder/SessionReco
14
14
  import { MockLanguageModelV3 } from "ai/test";
15
15
 
16
16
  async function create_recorder(session_id = "session-recorder-test") {
17
- const root_path = await fs.mkdtemp(
18
- path.join(os.tmpdir(), "downcity-session-recorder-"),
19
- );
17
+ const root_path = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-session-recorder-"));
20
18
  const file_path = path.join(root_path, "messages.jsonl");
19
+ const assistant_message_file_path = path.join(root_path, "assistant_message.json");
21
20
  const events = [];
22
21
  const recorder = new SessionRecorder({
23
22
  session_id,
@@ -25,189 +24,151 @@ async function create_recorder(session_id = "session-recorder-test") {
25
24
  publish: (mutation) => events.push(mutation),
26
25
  });
27
26
  await recorder.initialize();
28
- return { recorder, events, file_path };
27
+ return { recorder, events, file_path, assistant_message_file_path };
28
+ }
29
+
30
+ async function read_jsonl(file_path) {
31
+ const raw = await fs.readFile(file_path, "utf8");
32
+ return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
29
33
  }
30
34
 
31
- test("每个 assistant delta 都先持久化为独立 Mutation 再发布", async () => {
32
- const { recorder, events, file_path } = await create_recorder();
35
+ test("delta 只更新 Assistant 草稿,完成后才写入 messages.jsonl", async () => {
36
+ const { recorder, events, file_path, assistant_message_file_path } = await create_recorder();
33
37
  await recorder.append_user_message({
34
38
  turn_id: "turn-1",
35
39
  input_type: "prompt",
36
- parts: [{
37
- part_id: "user-text-1",
38
- type: "text",
39
- text: "你好",
40
- state: "done",
41
- }],
42
- });
43
- const writer = await recorder.open_assistant_message({
44
- turn_id: "turn-1",
45
- segment_index: 1,
40
+ parts: [{ part_id: "user-text-1", type: "text", text: "你好", state: "done" }],
46
41
  });
42
+ const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
47
43
  await writer.apply_chunk({ type: "text-start", id: "text-1" });
48
44
  await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "你" });
49
45
  await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "好" });
46
+
47
+ const history_during_stream = await read_jsonl(file_path);
48
+ const draft = JSON.parse(await fs.readFile(assistant_message_file_path, "utf8"));
49
+ assert.equal(history_during_stream.length, 1);
50
+ assert.equal(history_during_stream[0].type, "user");
51
+ assert.equal(draft.type, "assistant");
52
+ assert.equal(draft.parts[0].text, "你好");
53
+ assert.equal(draft.parts[0].sequence, 1);
54
+
50
55
  await writer.apply_chunk({ type: "text-end", id: "text-1" });
51
56
  await writer.complete();
52
57
 
53
- const changes = await recorder.list_message_changes({
54
- after_commit_sequence: 0,
55
- });
56
- const persisted_lines = (await fs.readFile(file_path, "utf8"))
57
- .trim()
58
- .split("\n");
59
- assert.equal(persisted_lines.length, changes.items.length);
60
- assert.deepEqual(
61
- changes.items.map((item) => item.commit_sequence),
62
- events.map((item) => item.commit_sequence),
63
- );
64
- assert.deepEqual(
65
- changes.items.map((item) => `${item.variant}:${item.type}`),
66
- [
67
- "message:user",
68
- "message:assistant",
69
- "part:text",
70
- "delta:text",
71
- "delta:text",
72
- "part:text",
73
- "message:assistant",
74
- ],
75
- );
76
- assert.deepEqual(
77
- changes.items
78
- .filter((item) => item.variant === "delta")
79
- .map((item) => item.delta),
80
- ["你", "好"],
81
- );
82
-
83
- const page = await recorder.list_messages();
84
- assert.deepEqual(page.items.map((item) => item.type), ["user", "assistant"]);
85
- const assistant = page.items[1];
86
- assert.equal(assistant.status, "completed");
87
- assert.equal(assistant.parts[0].text, "你好");
58
+ const history = await read_jsonl(file_path);
59
+ assert.deepEqual(history.map((message) => message.type), ["user", "assistant"]);
60
+ assert.equal(history[1].parts[0].text, "你好");
61
+ assert.equal(await fs.stat(assistant_message_file_path).then(() => true).catch(() => false), false);
62
+ assert.deepEqual(events.map((event) => `${event.variant}:${event.type}`), [
63
+ "message:user",
64
+ "message:assistant",
65
+ "part:text",
66
+ "delta:text",
67
+ "delta:text",
68
+ "part:text",
69
+ "message:assistant",
70
+ ]);
71
+ assert.equal(history.some((value) => "variant" in value), false);
88
72
  });
89
73
 
90
- test("tool action 在原 Message 和 part 上更新", async () => {
91
- const { recorder } = await create_recorder("tool-action-test");
92
- const writer = await recorder.open_assistant_message({
93
- turn_id: "turn-1",
94
- segment_index: 1,
95
- });
96
- await writer.apply_chunk({
97
- type: "tool-input-start",
98
- toolCallId: "call-1",
99
- toolName: "search",
100
- });
74
+ test("Tool Part 在文本中间创建后,输入、审批和输出都保持原顺序", async () => {
75
+ const { recorder, events, file_path } = await create_recorder("tool-order-test");
76
+ const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
77
+ await writer.apply_chunk({ type: "text-start", id: "text-1" });
78
+ await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "before" });
79
+ await writer.apply_chunk({ type: "text-end", id: "text-1" });
80
+ await writer.apply_chunk({ type: "tool-input-start", toolCallId: "call-1", toolName: "search" });
101
81
  await writer.apply_chunk({
102
82
  type: "tool-input-available",
103
83
  toolCallId: "call-1",
104
84
  toolName: "search",
105
85
  input: { query: "downcity" },
106
86
  });
87
+ assert.equal(recorder.get_message(writer.message_id).parts[1].state, "ready");
107
88
  await writer.apply_chunk({
108
- type: "tool-output-available",
89
+ type: "tool-approval-request",
109
90
  toolCallId: "call-1",
110
- output: { count: 1 },
91
+ approvalId: "approval-1",
111
92
  });
93
+ assert.equal(recorder.get_message(writer.message_id).parts[1].state, "approval-required");
94
+ await writer.apply_chunk({ type: "text-start", id: "text-2" });
95
+ await writer.apply_chunk({ type: "text-delta", id: "text-2", delta: "after" });
96
+ await writer.apply_chunk({ type: "text-end", id: "text-2" });
97
+ await writer.apply_chunk({ type: "tool-output-available", toolCallId: "call-1", output: { count: 1 } });
112
98
  await writer.complete();
113
99
 
114
- const action = await recorder.open_action_message({
115
- message_id: "action-1",
116
- action_type: "compact",
117
- title: "Compacting",
118
- });
119
- const running = recorder.get_message("action-1");
120
- await action.complete({ title: "Compacted" });
121
- const completed = recorder.get_message("action-1");
122
-
123
- const assistant = (await recorder.list_messages()).items[0];
124
- assert.equal(assistant.parts.length, 1);
125
- assert.equal(assistant.parts[0].state, "completed");
126
- assert.deepEqual(assistant.parts[0].output, { count: 1 });
127
- assert.equal(running.sequence, completed.sequence);
128
- assert.equal(completed.revision, running.revision + 1);
129
- assert.equal(completed.status, "completed");
100
+ const assistant = (await read_jsonl(file_path))[0];
101
+ assert.deepEqual(assistant.parts.map((part) => part.type), ["text", "tool", "text"]);
102
+ assert.deepEqual(assistant.parts.map((part) => part.sequence), [1, 2, 3]);
103
+ assert.equal(assistant.parts[1].state, "completed");
104
+ assert.deepEqual(assistant.parts[1].output, { count: 1 });
105
+ assert.deepEqual(
106
+ events
107
+ .filter((event) => event.variant === "part" && event.type === "tool")
108
+ .map((event) => [event.part.sequence, event.part.state]),
109
+ [[2, "input-streaming"], [2, "ready"], [2, "approval-required"], [2, "completed"]],
110
+ );
130
111
  });
131
112
 
132
- test("snapshot、changes 分页和重启恢复保持单调顺序", async () => {
133
- const { recorder, file_path } = await create_recorder("recovery-test");
113
+ test("重启时将 Assistant 草稿收口为 stopped,并将运行中 Action 标记失败", async () => {
114
+ const { recorder, file_path, assistant_message_file_path } = await create_recorder("recovery-test");
134
115
  await recorder.append_user_message({
135
116
  turn_id: "turn-1",
136
117
  input_type: "prompt",
137
118
  parts: [{ part_id: "u1", type: "text", text: "one", state: "done" }],
138
119
  });
139
- await recorder.open_assistant_message({
140
- turn_id: "turn-1",
141
- segment_index: 1,
142
- });
143
- await recorder.open_action_message({
144
- message_id: "running-action",
145
- action_type: "fork",
146
- title: "Forking",
147
- });
120
+ const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
121
+ await writer.apply_chunk({ type: "text-start", id: "text-1" });
122
+ await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "partial" });
123
+ await recorder.open_action_message({ message_id: "running-action", action_type: "fork", title: "Forking" });
148
124
 
149
125
  const recovered_events = [];
150
126
  const recovered = new SessionRecorder({
151
127
  session_id: "recovery-test",
152
- store: new JsonlSessionMessageStore({
153
- session_id: "recovery-test",
154
- file_path,
155
- }),
128
+ store: new JsonlSessionMessageStore({ session_id: "recovery-test", file_path }),
156
129
  publish: (mutation) => recovered_events.push(mutation),
157
130
  });
158
131
  await recovered.initialize();
159
132
 
160
- const first_page = await recovered.list_messages({ limit: 2 });
161
- const second_page = await recovered.list_messages({
162
- limit: 2,
163
- cursor: first_page.next_cursor,
164
- });
165
- assert.equal(first_page.items.length, 2);
166
- assert.equal(second_page.items.length, 1);
167
- assert.ok(first_page.items[1].sequence < second_page.items[0].sequence);
168
-
169
- const messages = [...first_page.items, ...second_page.items];
170
- assert.equal(messages[1].status, "stopped");
171
- assert.equal(messages[2].status, "failed");
172
- assert.deepEqual(
173
- recovered_events.map((item) => `${item.variant}:${item.type}`),
174
- ["message:assistant", "message:action"],
175
- );
133
+ const page = await recovered.list_messages({ limit: 10 });
134
+ assert.deepEqual(page.items.map((message) => message.type), ["user", "assistant", "action"]);
135
+ assert.equal(page.items[1].status, "stopped");
136
+ assert.equal(page.items[1].parts[0].text, "partial");
137
+ assert.equal(page.items[2].status, "failed");
138
+ assert.deepEqual(recovered_events.map((event) => `${event.variant}:${event.type}`), [
139
+ "message:assistant",
140
+ "message:action",
141
+ ]);
142
+ assert.equal(await fs.stat(assistant_message_file_path).then(() => true).catch(() => false), false);
143
+ });
176
144
 
177
- const first_changes = await recovered.list_message_changes({
178
- after_commit_sequence: 0,
179
- limit: 2,
180
- });
181
- const next_changes = await recovered.list_message_changes({
182
- after_commit_sequence: first_changes.next_commit_sequence,
183
- limit: 100,
145
+ test("Action revision 追加完整快照,读取时只返回最新版本", async () => {
146
+ const { recorder, file_path } = await create_recorder("action-revision-test");
147
+ const action = await recorder.open_action_message({
148
+ message_id: "action-1",
149
+ action_type: "compact",
150
+ title: "Compacting",
184
151
  });
185
- assert.equal(first_changes.has_more, true);
186
- assert.ok(
187
- first_changes.items.at(-1).commit_sequence <
188
- next_changes.items[0].commit_sequence,
189
- );
152
+ await action.complete({ title: "Compacted" });
153
+ const history = await read_jsonl(file_path);
154
+ assert.equal(history.length, 2);
155
+ assert.deepEqual(history.map((message) => message.revision), [1, 2]);
156
+ const page = await recorder.list_messages();
157
+ assert.equal(page.items.length, 1);
158
+ assert.equal(page.items[0].title, "Compacted");
190
159
  });
191
160
 
192
- test("compact 追加 internal summary 且不重写 Mutation 日志", async () => {
161
+ test("compact 追加 internal summary 且不重写已有 Message 快照", async () => {
193
162
  const { recorder, file_path } = await create_recorder("compact-test");
194
163
  for (let index = 1; index <= 6; index += 1) {
195
164
  await recorder.append_user_message({
196
165
  turn_id: `turn-${String(index)}`,
197
166
  input_type: "prompt",
198
- parts: [{
199
- part_id: `user-${String(index)}`,
200
- type: "text",
201
- text: `message ${String(index)}`,
202
- state: "done",
203
- }],
167
+ parts: [{ part_id: `user-${String(index)}`, type: "text", text: `message ${String(index)}`, state: "done" }],
204
168
  });
205
169
  }
206
170
  const before = await fs.readFile(file_path, "utf8");
207
- const history_store = new SessionRecorderHistoryStore({
208
- session_id: "compact-test",
209
- recorder,
210
- });
171
+ const history_store = new SessionRecorderHistoryStore({ session_id: "compact-test", recorder });
211
172
  const model = new MockLanguageModelV3({
212
173
  modelId: "compact-model",
213
174
  doGenerate: async () => ({
@@ -229,17 +190,10 @@ test("compact 追加 internal summary 且不重写 Mutation 日志", async () =>
229
190
  compactRatio: 0.5,
230
191
  });
231
192
  assert.equal(result.compacted, true);
232
-
233
- const after = await fs.readFile(file_path, "utf8");
234
- assert.equal(after.startsWith(before), true);
235
- const all = await recorder.list_messages({
236
- limit: 100,
237
- include_internal: true,
238
- });
239
- const summary = all.items.find(
240
- (message) => message.type === "assistant" && message.kind === "summary",
241
- );
193
+ assert.equal((await fs.readFile(file_path, "utf8")).startsWith(before), true);
194
+ const all = await recorder.list_messages({ limit: 100, include_internal: true });
195
+ const summary = all.items.find((message) => message.type === "assistant" && message.kind === "summary");
242
196
  assert.equal(summary.visibility, "internal");
243
- assert.equal(summary.summary_through_message_id, all.items[2].message_id);
197
+ assert.equal(summary.parts[0].sequence, 1);
244
198
  assert.equal((await history_store.list_records()).length, 4);
245
199
  });
@@ -15,12 +15,6 @@ import type { AgentModel } from "@/model/CityModelAdapter.js";
15
15
  import type { AgentOptions } from "@/types/agent/AgentOptions.js";
16
16
  import type { AgentSessions as AgentSessionsApi } from "@/types/agent/SessionActor.js";
17
17
  import type {
18
- ShellApprovalMode,
19
- ShellApprovalDecisionResult,
20
- ShellApprovalModeUpdateResult,
21
- ShellApprovalModeOption,
22
- ShellSessionApprovalModeView,
23
- ShellApprovalView,
24
18
  Shell,
25
19
  } from "@downcity/shell";
26
20
  import { Logger } from "@/utils/logger/Logger.js";
@@ -122,55 +116,6 @@ export class Agent {
122
116
  await this.backgroundService.dispose();
123
117
  }
124
118
 
125
- /**
126
- * 列出当前 shell pending approvals。
127
- */
128
- approvals(): ShellApprovalView[] {
129
- return this.shell?.approvals() || [];
130
- }
131
-
132
- /**
133
- * 列出当前 shell 显式设置过的 approval 模式。
134
- */
135
- approval_modes(): ShellApprovalModeOption[] {
136
- return this.shell?.approval_modes() || [];
137
- }
138
-
139
- /**
140
- * 读取当前 shell 指定 session 的 approval 模式。
141
- */
142
- approval_mode(input: { session_id: string }): ShellSessionApprovalModeView {
143
- if (!this.shell) throw new Error("Agent shell is not configured");
144
- return this.shell.approval_mode(input);
145
- }
146
-
147
- /**
148
- * 设置当前 shell 指定 session 的 approval 模式。
149
- */
150
- set_approval_mode(input: {
151
- session_id: string;
152
- mode: ShellApprovalMode;
153
- }): ShellApprovalModeUpdateResult {
154
- if (!this.shell) throw new Error("Agent shell is not configured");
155
- return this.shell.set_approval_mode(input);
156
- }
157
-
158
- /**
159
- * 批准当前 shell pending approval。
160
- */
161
- async approve(input: { approval_id: string }): Promise<ShellApprovalDecisionResult> {
162
- if (!this.shell) throw new Error("Agent shell is not configured");
163
- return await this.shell.approve(input);
164
- }
165
-
166
- /**
167
- * 拒绝当前 shell pending approval。
168
- */
169
- async deny(input: { approval_id: string }): Promise<ShellApprovalDecisionResult> {
170
- if (!this.shell) throw new Error("Agent shell is not configured");
171
- return await this.shell.deny(input);
172
- }
173
-
174
119
  /**
175
120
  * 更新当前 SDK Agent 的静态基础指令。
176
121
  */
@@ -260,6 +205,7 @@ export class Agent {
260
205
  tools: this.tools || assembly.tools,
261
206
  logger: this.logger || assembly.logger,
262
207
  get_agent_context: () => this.agentContext ?? assembly.agent_context,
208
+ get_shell: () => this.shell,
263
209
  get_instruction: () => this.instruction,
264
210
  ensure_agent_ready: async () => {
265
211
  await this.backgroundService.ready();
@@ -32,7 +32,7 @@ import {
32
32
  } from "@executor/tools/plugin/PluginToolDefinition.js";
33
33
  import type { AgentManagedSession } from "@/types/session/SessionOptions.js";
34
34
  import type { SessionPort } from "@/types/runtime/agent/AgentContext.js";
35
- import type { AgentSessionEvent } from "@/types/sdk/AgentSessionEvent.js";
35
+ import type { SessionApprovalRuntimeEvent } from "@/types/session/SessionApproval.js";
36
36
 
37
37
  type AgentAssemblyServiceOptions = {
38
38
  /**
@@ -215,10 +215,12 @@ export class AgentAssemblyService {
215
215
  env,
216
216
  agent_id: id,
217
217
  logger,
218
- emit_event: (event) => {
218
+ emit_event: async (event) => {
219
219
  const session_id = String(event.session_id || "").trim();
220
220
  if (!session_id) return;
221
- agent_context!.session.get(session_id).publishEvent(event as unknown as AgentSessionEvent);
221
+ const approval_event = normalize_approval_runtime_event(event);
222
+ if (!approval_event) return;
223
+ await agent_context!.session.get(session_id).publishEvent(approval_event);
222
224
  },
223
225
  });
224
226
  Object.assign(tools, shell.tools);
@@ -269,3 +271,37 @@ export class AgentAssemblyService {
269
271
  return false;
270
272
  }
271
273
  }
274
+
275
+ function normalize_approval_runtime_event(
276
+ event: Record<string, unknown>,
277
+ ): SessionApprovalRuntimeEvent | null {
278
+ if (event.type === "tool-approval-request") {
279
+ return {
280
+ type: "tool-approval-request",
281
+ turn_id: String(event.turnId || ""),
282
+ tool_call_id: String(event.toolCallId || ""),
283
+ tool_name: String(event.toolName || "unknown"),
284
+ approval_id: String(event.approvalId || ""),
285
+ command: String(event.cmd || ""),
286
+ cwd: String(event.cwd || ""),
287
+ reason: String(event.reason || ""),
288
+ operation:
289
+ event.operation === "start" || event.operation === "write"
290
+ ? event.operation
291
+ : "exec",
292
+ };
293
+ }
294
+ if (event.type === "tool-approval-result") {
295
+ const decision = event.decision === "approved" || event.decision === "denied"
296
+ ? event.decision
297
+ : "expired";
298
+ return {
299
+ type: "tool-approval-result",
300
+ turn_id: String(event.turnId || ""),
301
+ tool_call_id: String(event.toolCallId || ""),
302
+ approval_id: String(event.approvalId || ""),
303
+ decision,
304
+ };
305
+ }
306
+ return null;
307
+ }
@@ -39,6 +39,8 @@ import {
39
39
  import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
40
40
  import type { SessionPort } from "@/types/runtime/agent/AgentContext.js";
41
41
  import { createInstructionSystemBlocks } from "@/agent/local/AgentInstructions.js";
42
+ import type { Shell } from "@downcity/shell";
43
+ import type { SessionApproval } from "@/types/session/SessionApproval.js";
42
44
 
43
45
  function decodeMaybe(input: string): string {
44
46
  try {
@@ -77,6 +79,9 @@ type AgentSessionsOptions = {
77
79
  */
78
80
  get_agent_context: () => AgentContext;
79
81
 
82
+ /** 延迟读取当前 Agent 挂载的 Shell Runtime。 */
83
+ get_shell: () => Shell | undefined;
84
+
80
85
  /**
81
86
  * 当前静态 instruction 文本集合。
82
87
  */
@@ -113,6 +118,7 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
113
118
  private readonly tools: Record<string, Tool>;
114
119
  private readonly logger: Logger;
115
120
  private readonly get_agent_context: AgentSessionsOptions["get_agent_context"];
121
+ private readonly get_shell: AgentSessionsOptions["get_shell"];
116
122
  private readonly get_instruction: AgentSessionsOptions["get_instruction"];
117
123
  private readonly ensure_agent_ready: AgentSessionsOptions["ensure_agent_ready"];
118
124
  private readonly default_model?: AgentModel;
@@ -128,6 +134,7 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
128
134
  this.tools = options.tools;
129
135
  this.logger = options.logger;
130
136
  this.get_agent_context = options.get_agent_context;
137
+ this.get_shell = options.get_shell;
131
138
  this.get_instruction = options.get_instruction;
132
139
  this.ensure_agent_ready = options.ensure_agent_ready;
133
140
  this.default_model = options.default_model;
@@ -348,11 +355,51 @@ export class AgentSessions implements AgentSessionsApi<AgentSession> {
348
355
  await this.apply_session_defaults(session);
349
356
  },
350
357
  ...(this.resolve_model ? { resolve_model: this.resolve_model } : {}),
358
+ list_approvals: async (session_id) => this.list_session_approvals(session_id),
359
+ get_approval_mode: async (session_id) => {
360
+ const shell = this.require_shell();
361
+ return shell.approval_mode({ session_id });
362
+ },
363
+ set_approval_mode: async (session_id, { mode }) => {
364
+ const shell = this.require_shell();
365
+ return shell.set_approval_mode({ session_id, mode });
366
+ },
367
+ resolve_approval: async (session_id, { approval_id, decision }) => {
368
+ const approval = this.list_session_approvals(session_id)
369
+ .find((item) => item.approval_id === approval_id);
370
+ if (!approval) return { success: false, approval_id, decision };
371
+ return decision === "approved"
372
+ ? await this.require_shell().approve({ approval_id })
373
+ : await this.require_shell().deny({ approval_id });
374
+ },
351
375
  });
352
376
  this.sessions_by_id.set(resolved_session_id, created);
353
377
  return created;
354
378
  }
355
379
 
380
+ private require_shell(): Shell {
381
+ const shell = this.get_shell();
382
+ if (!shell) throw new Error("Session shell is not configured");
383
+ return shell;
384
+ }
385
+
386
+ private list_session_approvals(session_id: string): SessionApproval[] {
387
+ return (this.get_shell()?.approvals() || [])
388
+ .filter((approval) => approval.session_id === session_id)
389
+ .map((approval) => ({
390
+ approval_id: approval.approval_id,
391
+ session_id,
392
+ ...(approval.turn_id ? { turn_id: approval.turn_id } : {}),
393
+ ...(approval.tool_call_id ? { tool_call_id: approval.tool_call_id } : {}),
394
+ tool_name: approval.tool_name,
395
+ command: approval.cmd,
396
+ cwd: approval.cwd,
397
+ reason: approval.reason,
398
+ operation: approval.operation,
399
+ created_at: approval.created_at,
400
+ }));
401
+ }
402
+
356
403
  private async apply_session_defaults(
357
404
  session: AgentManagedSession,
358
405
  ): Promise<void> {
@@ -23,14 +23,6 @@ import type {
23
23
  RemoteAgentPluginActionInput,
24
24
  RemoteAgentPluginActionResult,
25
25
  } from "@/types/agent/RemoteAgentPluginAction.js";
26
- import type {
27
- ShellApprovalMode,
28
- ShellApprovalDecisionResult,
29
- ShellApprovalModeUpdateResult,
30
- ShellApprovalModeOption,
31
- ShellSessionApprovalModeView,
32
- ShellApprovalView,
33
- } from "@downcity/shell";
34
26
  import type { RemoteAgentTransport } from "@/agent/remote/RemoteTransport.js";
35
27
  import { RemoteSession } from "@/agent/remote/RemoteSession.js";
36
28
  import { create_remote_agent_transport } from "@/agent/remote/TransportFactory.js";
@@ -57,7 +49,7 @@ export class RemoteAgent {
57
49
  *
58
50
  * 关键点(中文)
59
51
  * - 这是 RemoteAgent 顶层能力,不绑定某个 session。
60
- * - Shell approval 请使用 `approvals()` / `approve()` / `deny()`。
52
+ * - Shell approval 通过具体 RemoteSession 处理。
61
53
  */
62
54
  async runPluginAction(
63
55
  input: RemoteAgentPluginActionInput,
@@ -77,51 +69,6 @@ export class RemoteAgent {
77
69
  });
78
70
  }
79
71
 
80
- /**
81
- * 列出远程 Agent 的 shell pending approvals。
82
- */
83
- async approvals(): Promise<ShellApprovalView[]> {
84
- return await this.transport.approvals();
85
- }
86
-
87
- /**
88
- * 列出远程 Agent 显式设置过的 shell approval 模式。
89
- */
90
- async approval_modes(): Promise<ShellApprovalModeOption[]> {
91
- return await this.transport.approval_modes();
92
- }
93
-
94
- /**
95
- * 读取远程 Agent 指定 session 的 shell approval 模式。
96
- */
97
- async approval_mode(input: { session_id: string }): Promise<ShellSessionApprovalModeView> {
98
- return await this.transport.approval_mode(input);
99
- }
100
-
101
- /**
102
- * 设置远程 Agent 指定 session 的 shell approval 模式。
103
- */
104
- async set_approval_mode(input: {
105
- session_id: string;
106
- mode: ShellApprovalMode;
107
- }): Promise<ShellApprovalModeUpdateResult> {
108
- return await this.transport.set_approval_mode(input);
109
- }
110
-
111
- /**
112
- * 批准远程 Agent 的 shell approval。
113
- */
114
- async approve(input: { approval_id: string }): Promise<ShellApprovalDecisionResult> {
115
- return await this.transport.approve(input);
116
- }
117
-
118
- /**
119
- * 拒绝远程 Agent 的 shell approval。
120
- */
121
- async deny(input: { approval_id: string }): Promise<ShellApprovalDecisionResult> {
122
- return await this.transport.deny(input);
123
- }
124
-
125
72
  /**
126
73
  * 关闭远程 transport。
127
74
  *