@downcity/agent 1.1.222 → 1.1.226
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent/local/Agent.d.ts +1 -34
- package/bin/agent/local/Agent.d.ts.map +1 -1
- package/bin/agent/local/Agent.js +1 -44
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
- package/bin/agent/local/services/AgentAssemblyService.js +35 -2
- package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
- package/bin/agent/local/services/AgentSessions.d.ts +6 -0
- package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +42 -0
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteAgent.d.ts +1 -35
- package/bin/agent/remote/RemoteAgent.d.ts.map +1 -1
- package/bin/agent/remote/RemoteAgent.js +1 -37
- package/bin/agent/remote/RemoteAgent.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +24 -45
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +74 -121
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +11 -27
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +7 -21
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -47
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +7 -20
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +8 -17
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -2
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/types/SessionHistoryPaths.d.ts +1 -1
- package/bin/index.d.ts +3 -3
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -1
- package/bin/index.js.map +1 -1
- package/bin/rpc/Client.d.ts +7 -36
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +28 -78
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +16 -8
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +66 -21
- package/bin/session/Session.js.map +1 -1
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +49 -18
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts +30 -24
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.js +163 -59
- package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -1
- package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -1
- package/bin/session/recorder/SessionMessageCodec.js +6 -1
- package/bin/session/recorder/SessionMessageCodec.js.map +1 -1
- package/bin/session/recorder/SessionRecorder.d.ts +23 -11
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +187 -112
- package/bin/session/recorder/SessionRecorder.js.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.js +1 -0
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +5 -5
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderHistoryStore.js +5 -5
- package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -1
- package/bin/session/runtime/SessionEventHub.d.ts +12 -5
- package/bin/session/runtime/SessionEventHub.d.ts.map +1 -1
- package/bin/session/runtime/SessionEventHub.js +11 -6
- package/bin/session/runtime/SessionEventHub.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +2 -2
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +35 -17
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +2 -2
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +6 -2
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +2 -7
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +2 -8
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/storage/Paths.d.ts +1 -1
- package/bin/session/storage/Paths.d.ts.map +1 -1
- package/bin/session/storage/Paths.js +2 -2
- package/bin/session/storage/Paths.js.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.d.ts +3 -4
- package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.js +2 -2
- package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +11 -7
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +35 -62
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +4 -4
- package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.js.map +1 -1
- package/bin/types/session/SessionApproval.d.ts +97 -0
- package/bin/types/session/SessionApproval.d.ts.map +1 -0
- package/bin/types/session/SessionApproval.js +7 -0
- package/bin/types/session/SessionApproval.js.map +1 -0
- package/bin/types/session/SessionMessage.d.ts +10 -4
- package/bin/types/session/SessionMessage.d.ts.map +1 -1
- package/bin/types/session/SessionMutation.d.ts +116 -0
- package/bin/types/session/SessionMutation.d.ts.map +1 -0
- package/bin/types/session/SessionMutation.js +19 -0
- package/bin/types/session/SessionMutation.js.map +1 -0
- package/bin/types/session/SessionOptions.d.ts +9 -0
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/remote-session-reconnect.test.mjs +7 -3
- package/scripts/session-history-inflight.test.mjs +2 -2
- package/scripts/session-prompt-runtime.test.mjs +3 -3
- package/scripts/session-recorder.test.mjs +141 -140
- package/src/agent/local/Agent.ts +1 -55
- package/src/agent/local/services/AgentAssemblyService.ts +39 -3
- package/src/agent/local/services/AgentSessions.ts +47 -0
- package/src/agent/remote/RemoteAgent.ts +1 -54
- package/src/agent/remote/RemoteSession.ts +105 -163
- package/src/agent/remote/RemoteTransport.ts +23 -34
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +33 -90
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +22 -39
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -2
- package/src/executor/types/SessionHistoryPaths.ts +1 -1
- package/src/index.ts +20 -12
- package/src/rpc/Client.ts +48 -106
- package/src/session/Session.ts +91 -34
- package/src/session/browse/Browse.ts +51 -22
- package/src/session/recorder/JsonlSessionMessageStore.ts +170 -86
- package/src/session/recorder/SessionMessageCodec.ts +6 -1
- package/src/session/recorder/SessionRecorder.ts +228 -134
- package/src/session/recorder/SessionRecorderCompaction.ts +1 -0
- package/src/session/recorder/SessionRecorderHistoryStore.ts +5 -5
- package/src/session/runtime/SessionEventHub.ts +25 -12
- package/src/session/runtime/SessionPromptRuntime.ts +37 -19
- package/src/session/services/SessionStateService.ts +8 -6
- package/src/session/services/SessionTurnService.ts +3 -14
- package/src/session/storage/Paths.ts +2 -2
- package/src/session/storage/RuntimeSessionPort.ts +8 -11
- package/src/types/agent/SessionActor.ts +23 -20
- package/src/types/rpc/RpcProtocol.ts +34 -71
- package/src/types/runtime/agent/AgentContext.ts +7 -11
- package/src/types/session/SessionApproval.ts +108 -0
- package/src/types/session/SessionMessage.ts +10 -4
- package/src/types/session/SessionMutation.ts +141 -0
- package/src/types/session/SessionOptions.ts +25 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/session/recorder/SessionMessageReducer.d.ts +0 -12
- package/bin/session/recorder/SessionMessageReducer.d.ts.map +0 -1
- package/bin/session/recorder/SessionMessageReducer.js +0 -109
- package/bin/session/recorder/SessionMessageReducer.js.map +0 -1
- package/bin/session/runtime/SessionMessageEventHub.d.ts +0 -15
- package/bin/session/runtime/SessionMessageEventHub.d.ts.map +0 -1
- package/bin/session/runtime/SessionMessageEventHub.js +0 -28
- package/bin/session/runtime/SessionMessageEventHub.js.map +0 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts +0 -303
- package/bin/types/sdk/AgentSessionEvent.d.ts.map +0 -1
- package/bin/types/sdk/AgentSessionEvent.js +0 -9
- package/bin/types/sdk/AgentSessionEvent.js.map +0 -1
- package/bin/types/session/SessionMessageMutation.d.ts +0 -92
- package/bin/types/session/SessionMessageMutation.d.ts.map +0 -1
- package/bin/types/session/SessionMessageMutation.js +0 -21
- package/bin/types/session/SessionMessageMutation.js.map +0 -1
- package/src/session/recorder/SessionMessageReducer.ts +0 -155
- package/src/session/runtime/SessionMessageEventHub.ts +0 -35
- package/src/types/sdk/AgentSessionEvent.ts +0 -372
- package/src/types/session/SessionMessageMutation.ts +0 -121
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 SessionRecorder
|
|
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,198 @@ 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("
|
|
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
|
|
54
|
-
|
|
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);
|
|
72
|
+
});
|
|
73
|
+
|
|
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" });
|
|
81
|
+
await writer.apply_chunk({
|
|
82
|
+
type: "tool-input-available",
|
|
83
|
+
toolCallId: "call-1",
|
|
84
|
+
toolName: "search",
|
|
85
|
+
input: { query: "downcity" },
|
|
55
86
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
],
|
|
75
|
-
);
|
|
87
|
+
assert.equal(recorder.get_message(writer.message_id).parts[1].state, "ready");
|
|
88
|
+
await writer.apply_chunk({
|
|
89
|
+
type: "tool-approval-request",
|
|
90
|
+
toolCallId: "call-1",
|
|
91
|
+
approvalId: "approval-1",
|
|
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 } });
|
|
98
|
+
await writer.complete();
|
|
99
|
+
|
|
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 });
|
|
76
105
|
assert.deepEqual(
|
|
77
|
-
|
|
78
|
-
.filter((
|
|
79
|
-
.map((
|
|
80
|
-
["
|
|
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"]],
|
|
81
110
|
);
|
|
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, "你好");
|
|
88
111
|
});
|
|
89
112
|
|
|
90
|
-
test("
|
|
91
|
-
const { recorder } = await create_recorder("
|
|
92
|
-
const writer = await recorder.open_assistant_message({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
});
|
|
113
|
+
test("空 Text Start 不会抢占后续 Tool 的真实顺序", async () => {
|
|
114
|
+
const { recorder, events, file_path } = await create_recorder("deferred-text-order-test");
|
|
115
|
+
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
116
|
+
|
|
117
|
+
await writer.apply_chunk({ type: "reasoning-start", id: "reasoning-0" });
|
|
118
|
+
await writer.apply_chunk({ type: "reasoning-delta", id: "reasoning-0", delta: "先执行命令" });
|
|
119
|
+
await writer.apply_chunk({ type: "reasoning-end", id: "reasoning-0" });
|
|
120
|
+
await writer.apply_chunk({ type: "text-start", id: "text-0" });
|
|
96
121
|
await writer.apply_chunk({
|
|
97
122
|
type: "tool-input-start",
|
|
98
123
|
toolCallId: "call-1",
|
|
99
|
-
toolName: "
|
|
124
|
+
toolName: "shell_exec",
|
|
100
125
|
});
|
|
101
126
|
await writer.apply_chunk({
|
|
102
127
|
type: "tool-input-available",
|
|
103
128
|
toolCallId: "call-1",
|
|
104
|
-
toolName: "
|
|
105
|
-
input: {
|
|
129
|
+
toolName: "shell_exec",
|
|
130
|
+
input: { cmd: "pwd" },
|
|
106
131
|
});
|
|
107
132
|
await writer.apply_chunk({
|
|
108
133
|
type: "tool-output-available",
|
|
109
134
|
toolCallId: "call-1",
|
|
110
|
-
output: {
|
|
135
|
+
output: { success: true },
|
|
111
136
|
});
|
|
137
|
+
await writer.apply_chunk({ type: "text-delta", id: "text-0", delta: "命令执行完成" });
|
|
138
|
+
await writer.apply_chunk({ type: "text-end", id: "text-0" });
|
|
112
139
|
await writer.complete();
|
|
113
140
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
141
|
+
const assistant = (await read_jsonl(file_path))[0];
|
|
142
|
+
assert.deepEqual(assistant.parts.map((part) => part.type), ["reasoning", "tool", "text"]);
|
|
143
|
+
assert.deepEqual(assistant.parts.map((part) => part.sequence), [1, 2, 3]);
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
events
|
|
146
|
+
.filter((event) => event.variant === "part")
|
|
147
|
+
.map((event) => [event.type, event.part.sequence]),
|
|
148
|
+
[
|
|
149
|
+
["reasoning", 1],
|
|
150
|
+
["reasoning", 1],
|
|
151
|
+
["tool", 2],
|
|
152
|
+
["tool", 2],
|
|
153
|
+
["tool", 2],
|
|
154
|
+
["text", 3],
|
|
155
|
+
["text", 3],
|
|
156
|
+
],
|
|
157
|
+
);
|
|
130
158
|
});
|
|
131
159
|
|
|
132
|
-
test("
|
|
133
|
-
const { recorder, file_path } = await create_recorder("recovery-test");
|
|
160
|
+
test("重启时将 Assistant 草稿收口为 stopped,并将运行中 Action 标记失败", async () => {
|
|
161
|
+
const { recorder, file_path, assistant_message_file_path } = await create_recorder("recovery-test");
|
|
134
162
|
await recorder.append_user_message({
|
|
135
163
|
turn_id: "turn-1",
|
|
136
164
|
input_type: "prompt",
|
|
137
165
|
parts: [{ part_id: "u1", type: "text", text: "one", state: "done" }],
|
|
138
166
|
});
|
|
139
|
-
await recorder.open_assistant_message({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
await recorder.open_action_message({
|
|
144
|
-
message_id: "running-action",
|
|
145
|
-
action_type: "fork",
|
|
146
|
-
title: "Forking",
|
|
147
|
-
});
|
|
167
|
+
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
168
|
+
await writer.apply_chunk({ type: "text-start", id: "text-1" });
|
|
169
|
+
await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "partial" });
|
|
170
|
+
await recorder.open_action_message({ message_id: "running-action", action_type: "fork", title: "Forking" });
|
|
148
171
|
|
|
149
172
|
const recovered_events = [];
|
|
150
173
|
const recovered = new SessionRecorder({
|
|
151
174
|
session_id: "recovery-test",
|
|
152
|
-
store: new JsonlSessionMessageStore({
|
|
153
|
-
session_id: "recovery-test",
|
|
154
|
-
file_path,
|
|
155
|
-
}),
|
|
175
|
+
store: new JsonlSessionMessageStore({ session_id: "recovery-test", file_path }),
|
|
156
176
|
publish: (mutation) => recovered_events.push(mutation),
|
|
157
177
|
});
|
|
158
178
|
await recovered.initialize();
|
|
159
179
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
assert.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
);
|
|
180
|
+
const page = await recovered.list_messages({ limit: 10 });
|
|
181
|
+
assert.deepEqual(page.items.map((message) => message.type), ["user", "assistant", "action"]);
|
|
182
|
+
assert.equal(page.items[1].status, "stopped");
|
|
183
|
+
assert.equal(page.items[1].parts[0].text, "partial");
|
|
184
|
+
assert.equal(page.items[2].status, "failed");
|
|
185
|
+
assert.deepEqual(recovered_events.map((event) => `${event.variant}:${event.type}`), [
|
|
186
|
+
"message:assistant",
|
|
187
|
+
"message:action",
|
|
188
|
+
]);
|
|
189
|
+
assert.equal(await fs.stat(assistant_message_file_path).then(() => true).catch(() => false), false);
|
|
190
|
+
});
|
|
176
191
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
limit: 100,
|
|
192
|
+
test("Action revision 追加完整快照,读取时只返回最新版本", async () => {
|
|
193
|
+
const { recorder, file_path } = await create_recorder("action-revision-test");
|
|
194
|
+
const action = await recorder.open_action_message({
|
|
195
|
+
message_id: "action-1",
|
|
196
|
+
action_type: "compact",
|
|
197
|
+
title: "Compacting",
|
|
184
198
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
);
|
|
199
|
+
await action.complete({ title: "Compacted" });
|
|
200
|
+
const history = await read_jsonl(file_path);
|
|
201
|
+
assert.equal(history.length, 2);
|
|
202
|
+
assert.deepEqual(history.map((message) => message.revision), [1, 2]);
|
|
203
|
+
const page = await recorder.list_messages();
|
|
204
|
+
assert.equal(page.items.length, 1);
|
|
205
|
+
assert.equal(page.items[0].title, "Compacted");
|
|
190
206
|
});
|
|
191
207
|
|
|
192
|
-
test("compact 追加 internal summary
|
|
208
|
+
test("compact 追加 internal summary 且不重写已有 Message 快照", async () => {
|
|
193
209
|
const { recorder, file_path } = await create_recorder("compact-test");
|
|
194
210
|
for (let index = 1; index <= 6; index += 1) {
|
|
195
211
|
await recorder.append_user_message({
|
|
196
212
|
turn_id: `turn-${String(index)}`,
|
|
197
213
|
input_type: "prompt",
|
|
198
|
-
parts: [{
|
|
199
|
-
part_id: `user-${String(index)}`,
|
|
200
|
-
type: "text",
|
|
201
|
-
text: `message ${String(index)}`,
|
|
202
|
-
state: "done",
|
|
203
|
-
}],
|
|
214
|
+
parts: [{ part_id: `user-${String(index)}`, type: "text", text: `message ${String(index)}`, state: "done" }],
|
|
204
215
|
});
|
|
205
216
|
}
|
|
206
217
|
const before = await fs.readFile(file_path, "utf8");
|
|
207
|
-
const history_store = new SessionRecorderHistoryStore({
|
|
208
|
-
session_id: "compact-test",
|
|
209
|
-
recorder,
|
|
210
|
-
});
|
|
218
|
+
const history_store = new SessionRecorderHistoryStore({ session_id: "compact-test", recorder });
|
|
211
219
|
const model = new MockLanguageModelV3({
|
|
212
220
|
modelId: "compact-model",
|
|
213
221
|
doGenerate: async () => ({
|
|
@@ -229,17 +237,10 @@ test("compact 追加 internal summary 且不重写 Mutation 日志", async () =>
|
|
|
229
237
|
compactRatio: 0.5,
|
|
230
238
|
});
|
|
231
239
|
assert.equal(result.compacted, true);
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
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
|
-
);
|
|
240
|
+
assert.equal((await fs.readFile(file_path, "utf8")).startsWith(before), true);
|
|
241
|
+
const all = await recorder.list_messages({ limit: 100, include_internal: true });
|
|
242
|
+
const summary = all.items.find((message) => message.type === "assistant" && message.kind === "summary");
|
|
242
243
|
assert.equal(summary.visibility, "internal");
|
|
243
|
-
assert.equal(summary.
|
|
244
|
+
assert.equal(summary.parts[0].sequence, 1);
|
|
244
245
|
assert.equal((await history_store.list_records()).length, 4);
|
|
245
246
|
});
|
package/src/agent/local/Agent.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
*
|