@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.
- 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 +20 -11
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorder.js +148 -103
- 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 +100 -146
- 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 +183 -124
- 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,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("
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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("
|
|
91
|
-
const { recorder } = await create_recorder("tool-
|
|
92
|
-
const writer = await recorder.open_assistant_message({
|
|
93
|
-
|
|
94
|
-
|
|
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-
|
|
89
|
+
type: "tool-approval-request",
|
|
109
90
|
toolCallId: "call-1",
|
|
110
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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("
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
);
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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
|
|
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
|
-
);
|
|
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.
|
|
197
|
+
assert.equal(summary.parts[0].sequence, 1);
|
|
244
198
|
assert.equal((await history_store.list_records()).length, 4);
|
|
245
199
|
});
|
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
|
*
|