@downcity/agent 1.1.217 → 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 +43 -4
- 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 +26 -41
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +75 -107
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +15 -27
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +10 -21
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +24 -46
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +10 -20
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +10 -16
- 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/executor/types/SessionRecords.d.ts +2 -0
- package/bin/executor/types/SessionRecords.d.ts.map +1 -1
- package/bin/executor/types/SessionRecords.js.map +1 -1
- package/bin/index.d.ts +6 -3
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -0
- package/bin/index.js.map +1 -1
- package/bin/rpc/Client.d.ts +12 -34
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +31 -70
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +22 -4
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +95 -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 +51 -0
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
- package/bin/session/recorder/JsonlSessionMessageStore.js +231 -0
- package/bin/session/recorder/JsonlSessionMessageStore.js.map +1 -0
- package/bin/session/recorder/SessionMessageCodec.d.ts +20 -0
- package/bin/session/recorder/SessionMessageCodec.d.ts.map +1 -0
- package/bin/session/recorder/SessionMessageCodec.js +257 -0
- package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
- package/bin/session/recorder/SessionRecorder.d.ts +186 -0
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorder.js +655 -0
- package/bin/session/recorder/SessionRecorder.js.map +1 -0
- package/bin/session/recorder/SessionRecorderCompaction.d.ts +18 -0
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorderCompaction.js +142 -0
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts +66 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.js +195 -0
- package/bin/session/recorder/SessionRecorderHistoryStore.js.map +1 -0
- 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 +9 -15
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +48 -30
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +9 -5
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +138 -74
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +6 -7
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +84 -60
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts +8 -3
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +10 -5
- package/bin/session/services/SessionViewService.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/Persistence.d.ts +6 -0
- package/bin/session/storage/Persistence.d.ts.map +1 -1
- package/bin/session/storage/Persistence.js +7 -0
- package/bin/session/storage/Persistence.js.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.d.ts +3 -3
- 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 +15 -5
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +48 -61
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +5 -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 +223 -0
- package/bin/types/session/SessionMessage.d.ts.map +1 -0
- package/bin/types/session/SessionMessage.js +8 -0
- package/bin/types/session/SessionMessage.js.map +1 -0
- 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/agent-env.test.mjs +4 -4
- package/scripts/remote-session-reconnect.test.mjs +7 -3
- package/scripts/session-action-message.test.mjs +98 -27
- package/scripts/session-history-inflight.test.mjs +2 -2
- package/scripts/session-list-title.test.mjs +6 -19
- package/scripts/session-prompt-runtime.test.mjs +7 -19
- package/scripts/session-recorder.test.mjs +199 -0
- package/scripts/session-title-event.test.mjs +10 -18
- package/src/agent/local/Agent.ts +1 -55
- package/src/agent/local/services/AgentAssemblyService.ts +39 -3
- package/src/agent/local/services/AgentSessions.ts +48 -3
- package/src/agent/remote/RemoteAgent.ts +1 -54
- package/src/agent/remote/RemoteSession.ts +106 -136
- package/src/agent/remote/RemoteTransport.ts +30 -30
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +52 -77
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +30 -34
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -2
- package/src/executor/types/SessionHistoryPaths.ts +1 -1
- package/src/executor/types/SessionRecords.ts +2 -0
- package/src/index.ts +36 -10
- package/src/rpc/Client.ts +58 -95
- package/src/session/Session.ts +128 -37
- package/src/session/browse/Browse.ts +51 -22
- package/src/session/recorder/JsonlSessionMessageStore.ts +246 -0
- package/src/session/recorder/SessionMessageCodec.ts +284 -0
- package/src/session/recorder/SessionRecorder.ts +871 -0
- package/src/session/recorder/SessionRecorderCompaction.ts +188 -0
- package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
- package/src/session/runtime/SessionEventHub.ts +25 -12
- package/src/session/runtime/SessionPromptRuntime.ts +60 -43
- package/src/session/services/SessionStateService.ts +159 -86
- package/src/session/services/SessionTurnService.ts +91 -73
- package/src/session/services/SessionViewService.ts +25 -13
- package/src/session/storage/Paths.ts +2 -2
- package/src/session/storage/Persistence.ts +13 -0
- package/src/session/storage/RuntimeSessionPort.ts +8 -9
- package/src/types/agent/SessionActor.ts +31 -8
- package/src/types/rpc/RpcProtocol.ts +44 -67
- package/src/types/runtime/agent/AgentContext.ts +9 -7
- package/src/types/session/SessionApproval.ts +108 -0
- package/src/types/session/SessionMessage.ts +253 -0
- package/src/types/session/SessionMutation.ts +141 -0
- package/src/types/session/SessionOptions.ts +25 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/session/SessionEventMapper.d.ts +0 -32
- package/bin/session/SessionEventMapper.d.ts.map +0 -1
- package/bin/session/SessionEventMapper.js +0 -131
- package/bin/session/SessionEventMapper.js.map +0 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts +0 -302
- 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/sdk/InternalUiChunkEvent.d.ts +0 -100
- package/bin/types/sdk/InternalUiChunkEvent.d.ts.map +0 -1
- package/bin/types/sdk/InternalUiChunkEvent.js +0 -9
- package/bin/types/sdk/InternalUiChunkEvent.js.map +0 -1
- package/scripts/session-history-archive.test.mjs +0 -188
- package/src/session/SessionEventMapper.ts +0 -148
- package/src/types/sdk/AgentSessionEvent.ts +0 -370
- package/src/types/sdk/InternalUiChunkEvent.ts +0 -108
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionRecorder canonical 上下文压缩。
|
|
3
|
+
*
|
|
4
|
+
* 压缩只追加一条 internal summary Message,不删除、不归档、不重写 Mutation 日志。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { generateText } from "ai";
|
|
8
|
+
import { generateId } from "@/utils/Id.js";
|
|
9
|
+
import type { SessionHistoryCompactInput } from "@/executor/store/history/SessionHistoryStore.js";
|
|
10
|
+
import type { SessionActionRecordV1 } from "@/executor/types/SessionRecords.js";
|
|
11
|
+
import {
|
|
12
|
+
build_initial_session_compaction_prompt,
|
|
13
|
+
build_update_session_compaction_prompt,
|
|
14
|
+
SESSION_COMPACTION_SYSTEM_PROMPT,
|
|
15
|
+
} from "@executor/composer/compaction/jsonl/JsonlSessionCompactionPrompts.js";
|
|
16
|
+
import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
|
|
17
|
+
import { to_executor_ui_message } from "@/session/recorder/SessionMessageCodec.js";
|
|
18
|
+
import type { SessionAssistantMessage, SessionMessage } from "@/types/session/SessionMessage.js";
|
|
19
|
+
|
|
20
|
+
/** 必要时把较早上下文追加为 canonical internal summary。 */
|
|
21
|
+
export async function compact_session_recorder_messages(input: {
|
|
22
|
+
/** 当前 Session Recorder。 */
|
|
23
|
+
recorder: SessionRecorder;
|
|
24
|
+
/** 压缩策略和模型。 */
|
|
25
|
+
compact_input: SessionHistoryCompactInput;
|
|
26
|
+
}): Promise<{ compacted: boolean; reason?: string }> {
|
|
27
|
+
const page = await input.recorder.list_messages({
|
|
28
|
+
limit: 500,
|
|
29
|
+
include_internal: true,
|
|
30
|
+
});
|
|
31
|
+
const context_messages = project_current_context(page.items);
|
|
32
|
+
if (context_messages.length < 4) {
|
|
33
|
+
return { compacted: false, reason: "small_messages" };
|
|
34
|
+
}
|
|
35
|
+
const system_text = input.compact_input.system
|
|
36
|
+
.map((message) => String(message.content || ""))
|
|
37
|
+
.join("\n\n");
|
|
38
|
+
const estimated_tokens = Math.ceil(
|
|
39
|
+
`${system_text}\n${JSON.stringify(context_messages)}`.length / 3,
|
|
40
|
+
);
|
|
41
|
+
if (estimated_tokens <= input.compact_input.maxInputTokensApprox) {
|
|
42
|
+
return { compacted: false, reason: "under_budget" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ratio = Math.max(0.1, Math.min(0.9, input.compact_input.compactRatio));
|
|
46
|
+
const compact_count = Math.max(
|
|
47
|
+
2,
|
|
48
|
+
Math.min(context_messages.length - 1, Math.floor(context_messages.length * ratio)),
|
|
49
|
+
);
|
|
50
|
+
const older = context_messages.slice(0, compact_count);
|
|
51
|
+
const boundary = [...older]
|
|
52
|
+
.reverse()
|
|
53
|
+
.find((message) => message.type !== "assistant" || message.kind !== "summary");
|
|
54
|
+
if (!boundary) return { compacted: false, reason: "nothing_to_compact" };
|
|
55
|
+
|
|
56
|
+
const action_id = `compacting:${input.recorder.session_id}:${generateId()}`;
|
|
57
|
+
await publish_action(input.compact_input.onAction, {
|
|
58
|
+
id: action_id,
|
|
59
|
+
session_id: input.recorder.session_id,
|
|
60
|
+
title: "Compacting session messages",
|
|
61
|
+
state: "running",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const previous_summary = older.find(
|
|
65
|
+
(message): message is SessionAssistantMessage =>
|
|
66
|
+
message.type === "assistant" && message.kind === "summary",
|
|
67
|
+
);
|
|
68
|
+
const conversation_text = older
|
|
69
|
+
.filter((message) => message !== previous_summary)
|
|
70
|
+
.map((message) => message_to_compaction_text(message))
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join("\n")
|
|
73
|
+
.slice(-24_000);
|
|
74
|
+
const previous_summary_text = previous_summary
|
|
75
|
+
? assistant_text(previous_summary)
|
|
76
|
+
: "";
|
|
77
|
+
const prompt = previous_summary_text
|
|
78
|
+
? build_update_session_compaction_prompt({
|
|
79
|
+
previous_summary: previous_summary_text,
|
|
80
|
+
new_conversation_text: conversation_text || "(none)",
|
|
81
|
+
})
|
|
82
|
+
: build_initial_session_compaction_prompt({
|
|
83
|
+
conversation_text: conversation_text || "(none)",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let summary = "";
|
|
87
|
+
try {
|
|
88
|
+
const result = await generateText({
|
|
89
|
+
model: input.compact_input.model,
|
|
90
|
+
system: [{ role: "system", content: SESSION_COMPACTION_SYSTEM_PROMPT }],
|
|
91
|
+
prompt,
|
|
92
|
+
});
|
|
93
|
+
summary = String(result.text || "").trim();
|
|
94
|
+
} catch {
|
|
95
|
+
summary = "Earlier conversation was compacted because the context window was exceeded.";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await input.recorder.append_completed_assistant_message({
|
|
99
|
+
turn_id: `compact:${input.recorder.session_id}:${generateId()}`,
|
|
100
|
+
kind: "summary",
|
|
101
|
+
visibility: "internal",
|
|
102
|
+
summary_through_message_id: boundary.message_id,
|
|
103
|
+
parts: [{
|
|
104
|
+
part_id: `summary-text:${generateId()}`,
|
|
105
|
+
sequence: 1,
|
|
106
|
+
type: "text",
|
|
107
|
+
text: summary,
|
|
108
|
+
state: "done",
|
|
109
|
+
}],
|
|
110
|
+
});
|
|
111
|
+
await publish_action(input.compact_input.onAction, {
|
|
112
|
+
id: action_id,
|
|
113
|
+
session_id: input.recorder.session_id,
|
|
114
|
+
title: "Session messages compacted",
|
|
115
|
+
description: `Compacted through Message ${boundary.message_id}.`,
|
|
116
|
+
state: "completed",
|
|
117
|
+
});
|
|
118
|
+
return { compacted: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function project_current_context(messages: SessionMessage[]): SessionMessage[] {
|
|
122
|
+
const latest_summary = [...messages].reverse().find(
|
|
123
|
+
(message): message is SessionAssistantMessage =>
|
|
124
|
+
message.type === "assistant" &&
|
|
125
|
+
message.kind === "summary" &&
|
|
126
|
+
message.visibility === "internal",
|
|
127
|
+
);
|
|
128
|
+
if (!latest_summary?.summary_through_message_id) {
|
|
129
|
+
return messages.filter(
|
|
130
|
+
(message) => message.type === "user" || message.type === "assistant",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const boundary_index = messages.findIndex(
|
|
134
|
+
(message) => message.message_id === latest_summary.summary_through_message_id,
|
|
135
|
+
);
|
|
136
|
+
return [
|
|
137
|
+
latest_summary,
|
|
138
|
+
...messages.slice(boundary_index + 1).filter(
|
|
139
|
+
(message) =>
|
|
140
|
+
message !== latest_summary &&
|
|
141
|
+
(message.type === "user" ||
|
|
142
|
+
message.type === "assistant" && message.kind !== "summary"),
|
|
143
|
+
),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function message_to_compaction_text(message: SessionMessage): string {
|
|
148
|
+
const projected = to_executor_ui_message(message);
|
|
149
|
+
if (!projected) return "";
|
|
150
|
+
const text = projected.parts
|
|
151
|
+
.flatMap((part) => part.type === "text" ? [String(part.text || "")] : [])
|
|
152
|
+
.join("\n")
|
|
153
|
+
.trim();
|
|
154
|
+
return text ? `${message.type}: ${text}` : "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function assistant_text(message: SessionAssistantMessage): string {
|
|
158
|
+
return message.parts
|
|
159
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
160
|
+
.join("\n")
|
|
161
|
+
.trim();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function publish_action(
|
|
165
|
+
callback: SessionHistoryCompactInput["onAction"],
|
|
166
|
+
input: {
|
|
167
|
+
id: string;
|
|
168
|
+
session_id: string;
|
|
169
|
+
title: string;
|
|
170
|
+
description?: string;
|
|
171
|
+
state: "running" | "completed";
|
|
172
|
+
},
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
if (!callback) return;
|
|
175
|
+
const action: SessionActionRecordV1 = {
|
|
176
|
+
type: "action",
|
|
177
|
+
id: input.id,
|
|
178
|
+
title: input.title,
|
|
179
|
+
...(input.description ? { description: input.description } : {}),
|
|
180
|
+
state: input.state,
|
|
181
|
+
metadata: {
|
|
182
|
+
v: 1,
|
|
183
|
+
ts: Date.now(),
|
|
184
|
+
sessionId: input.session_id,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
await callback(action);
|
|
188
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - Recorder 的 messages.jsonl 与 assistant_message.json 共同组成持久化事实源。
|
|
6
|
+
* - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
|
|
7
|
+
* - 流式草稿写入由 AssistantMessageWriter 负责,Executor 的旧接口只提供投影兼容。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { nanoid } from "nanoid";
|
|
11
|
+
import type {
|
|
12
|
+
SessionActionRecordV1,
|
|
13
|
+
SessionMetadataV1,
|
|
14
|
+
SessionRecordV1,
|
|
15
|
+
} from "@/executor/types/SessionRecords.js";
|
|
16
|
+
import { is_session_action_record } from "@/executor/types/SessionRecords.js";
|
|
17
|
+
import type {
|
|
18
|
+
SessionHistoryCompactInput,
|
|
19
|
+
SessionHistoryStore,
|
|
20
|
+
} from "@/executor/store/history/SessionHistoryStore.js";
|
|
21
|
+
import { SessionRecorder } from "@/session/recorder/SessionRecorder.js";
|
|
22
|
+
import {
|
|
23
|
+
from_ui_assistant_parts,
|
|
24
|
+
from_ui_user_parts,
|
|
25
|
+
to_executor_ui_message,
|
|
26
|
+
} from "@/session/recorder/SessionMessageCodec.js";
|
|
27
|
+
import { compact_session_recorder_messages } from "@/session/recorder/SessionRecorderCompaction.js";
|
|
28
|
+
|
|
29
|
+
/** Recorder History Store 构造参数。 */
|
|
30
|
+
export interface SessionRecorderHistoryStoreOptions {
|
|
31
|
+
/** 当前 Session 标识。 */
|
|
32
|
+
session_id: string;
|
|
33
|
+
/** 当前 Session Recorder。 */
|
|
34
|
+
recorder: SessionRecorder;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 只把 canonical SessionMessage 投影给 Executor 的 Store。 */
|
|
38
|
+
export class SessionRecorderHistoryStore implements SessionHistoryStore {
|
|
39
|
+
readonly sessionId: string;
|
|
40
|
+
|
|
41
|
+
private readonly recorder: SessionRecorder;
|
|
42
|
+
|
|
43
|
+
constructor(options: SessionRecorderHistoryStoreOptions) {
|
|
44
|
+
this.sessionId = String(options.session_id || "").trim();
|
|
45
|
+
this.recorder = options.recorder;
|
|
46
|
+
if (!this.sessionId) {
|
|
47
|
+
throw new Error("SessionRecorderHistoryStore requires session_id");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 把外部 user/assistant/action 写入请求收口到 Recorder。 */
|
|
52
|
+
async write_record(message: SessionRecordV1): Promise<void> {
|
|
53
|
+
if (is_session_action_record(message)) {
|
|
54
|
+
const action = message as SessionActionRecordV1;
|
|
55
|
+
const existing = this.recorder.get_message(action.id);
|
|
56
|
+
if (existing?.type === "action") {
|
|
57
|
+
if (action.state !== "running") {
|
|
58
|
+
await this.recorder.update_action_message(
|
|
59
|
+
action.id,
|
|
60
|
+
action.state,
|
|
61
|
+
{
|
|
62
|
+
title: action.title,
|
|
63
|
+
description: action.description,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const writer = await this.recorder.open_action_message({
|
|
70
|
+
message_id: action.id,
|
|
71
|
+
turn_id: action.metadata.turnId,
|
|
72
|
+
action_type: infer_action_type(action.id),
|
|
73
|
+
title: action.title,
|
|
74
|
+
description: action.description,
|
|
75
|
+
});
|
|
76
|
+
if (action.state === "completed") await writer.complete();
|
|
77
|
+
if (action.state === "failed") await writer.fail(action.description || action.title);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (message.role === "user") {
|
|
82
|
+
const metadata = message.metadata || {
|
|
83
|
+
v: 1 as const,
|
|
84
|
+
ts: Date.now(),
|
|
85
|
+
sessionId: this.sessionId,
|
|
86
|
+
};
|
|
87
|
+
await this.recorder.append_user_message({
|
|
88
|
+
turn_id: String(metadata.turnId || `external:${nanoid(10)}`),
|
|
89
|
+
input_type:
|
|
90
|
+
metadata.extra?.inputType === "steer" ? "steer" : "prompt",
|
|
91
|
+
parts: from_ui_user_parts(message.parts),
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const metadata = message.metadata || {
|
|
97
|
+
v: 1 as const,
|
|
98
|
+
ts: Date.now(),
|
|
99
|
+
sessionId: this.sessionId,
|
|
100
|
+
};
|
|
101
|
+
await this.recorder.append_completed_assistant_message({
|
|
102
|
+
turn_id: metadata.turnId,
|
|
103
|
+
parts: from_ui_assistant_parts(message.parts),
|
|
104
|
+
visibility:
|
|
105
|
+
metadata.extra?.visibility === "internal"
|
|
106
|
+
? "internal"
|
|
107
|
+
: "visible",
|
|
108
|
+
kind: metadata.kind === "summary" ? "summary" : "normal",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 从 Recorder 当前快照投影运行中的 Assistant。 */
|
|
113
|
+
async read_inflight(): Promise<SessionRecordV1 | null> {
|
|
114
|
+
const page = await this.recorder.list_messages({
|
|
115
|
+
limit: 500,
|
|
116
|
+
include_internal: true,
|
|
117
|
+
});
|
|
118
|
+
const assistant = [...page.items]
|
|
119
|
+
.reverse()
|
|
120
|
+
.find(
|
|
121
|
+
(message) =>
|
|
122
|
+
message.type === "assistant" && message.status === "streaming",
|
|
123
|
+
);
|
|
124
|
+
return assistant ? to_executor_ui_message(assistant) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Executor 草稿写入口不持久化;流式内容由 Recorder writer 负责。 */
|
|
128
|
+
async write_inflight(_message: SessionRecordV1): Promise<void> {}
|
|
129
|
+
|
|
130
|
+
/** Executor 草稿收口入口不持久化;Assistant writer 负责 complete。 */
|
|
131
|
+
async finalize_inflight(_message?: SessionRecordV1 | null): Promise<void> {}
|
|
132
|
+
|
|
133
|
+
/** 返回 Context Composer 使用的 UIMessage 投影。 */
|
|
134
|
+
async list_records(): Promise<SessionRecordV1[]> {
|
|
135
|
+
const page = await this.recorder.list_messages({
|
|
136
|
+
limit: 500,
|
|
137
|
+
include_internal: true,
|
|
138
|
+
});
|
|
139
|
+
const messages = project_context_messages(page.items);
|
|
140
|
+
return messages.flatMap((message) => {
|
|
141
|
+
const projected = to_executor_ui_message(message);
|
|
142
|
+
return projected ? [projected] : [];
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 返回 Context Message 区间。 */
|
|
147
|
+
async slice_records(start: number, end: number): Promise<SessionRecordV1[]> {
|
|
148
|
+
return (await this.list_records()).slice(start, end);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 返回 Context Message 数量。 */
|
|
152
|
+
async record_count(): Promise<number> {
|
|
153
|
+
return (await this.list_records()).length;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Recorder metadata 由 SessionStateService 管理。 */
|
|
157
|
+
async meta(): Promise<Record<string, unknown>> {
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 通过 Recorder 追加 internal summary,不重写 Mutation 历史。 */
|
|
162
|
+
async compact(input: SessionHistoryCompactInput): Promise<{
|
|
163
|
+
compacted: boolean;
|
|
164
|
+
reason?: string;
|
|
165
|
+
}> {
|
|
166
|
+
return await compact_session_recorder_messages({
|
|
167
|
+
recorder: this.recorder,
|
|
168
|
+
compact_input: input,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 构造临时 user UIMessage。 */
|
|
173
|
+
userText(input: {
|
|
174
|
+
text: string;
|
|
175
|
+
metadata: Omit<SessionMetadataV1, "v" | "ts"> &
|
|
176
|
+
Partial<Pick<SessionMetadataV1, "ts">>;
|
|
177
|
+
id?: string;
|
|
178
|
+
}): SessionRecordV1 {
|
|
179
|
+
const { ts, ...metadata } = input.metadata;
|
|
180
|
+
return {
|
|
181
|
+
id: input.id || `u:${this.sessionId}:${nanoid(10)}`,
|
|
182
|
+
role: "user",
|
|
183
|
+
metadata: {
|
|
184
|
+
v: 1,
|
|
185
|
+
ts: typeof ts === "number" ? ts : Date.now(),
|
|
186
|
+
...metadata,
|
|
187
|
+
source: "ingress",
|
|
188
|
+
kind: "normal",
|
|
189
|
+
},
|
|
190
|
+
parts: [{ type: "text", text: String(input.text || "") }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 构造临时 assistant UIMessage。 */
|
|
195
|
+
assistantText(input: {
|
|
196
|
+
text: string;
|
|
197
|
+
metadata: Omit<SessionMetadataV1, "v" | "ts"> &
|
|
198
|
+
Partial<Pick<SessionMetadataV1, "ts">>;
|
|
199
|
+
id?: string;
|
|
200
|
+
kind?: "normal" | "summary";
|
|
201
|
+
source?: "egress" | "compact";
|
|
202
|
+
}): SessionRecordV1 {
|
|
203
|
+
const { ts, ...metadata } = input.metadata;
|
|
204
|
+
return {
|
|
205
|
+
id: input.id || `a:${this.sessionId}:${nanoid(10)}`,
|
|
206
|
+
role: "assistant",
|
|
207
|
+
metadata: {
|
|
208
|
+
v: 1,
|
|
209
|
+
ts: typeof ts === "number" ? ts : Date.now(),
|
|
210
|
+
...metadata,
|
|
211
|
+
source: input.source || "egress",
|
|
212
|
+
kind: input.kind || "normal",
|
|
213
|
+
},
|
|
214
|
+
parts: [{ type: "text", text: String(input.text || "") }],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 返回 action 原结构,真正持久化仍经过 write_record。 */
|
|
219
|
+
action(input: {
|
|
220
|
+
action: SessionActionRecordV1;
|
|
221
|
+
metadata: Pick<SessionMetadataV1, "sessionId"> &
|
|
222
|
+
Partial<Pick<SessionMetadataV1, "ts">>;
|
|
223
|
+
id?: string;
|
|
224
|
+
}): SessionActionRecordV1 {
|
|
225
|
+
return {
|
|
226
|
+
...input.action,
|
|
227
|
+
id: input.id || input.action.id,
|
|
228
|
+
metadata: {
|
|
229
|
+
...input.action.metadata,
|
|
230
|
+
sessionId: input.metadata.sessionId,
|
|
231
|
+
ts:
|
|
232
|
+
typeof input.metadata.ts === "number"
|
|
233
|
+
? input.metadata.ts
|
|
234
|
+
: input.action.metadata.ts,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function project_context_messages(messages: import("@/types/session/SessionMessage.js").SessionMessage[]) {
|
|
241
|
+
const latest_summary = [...messages]
|
|
242
|
+
.reverse()
|
|
243
|
+
.find(
|
|
244
|
+
(message) =>
|
|
245
|
+
message.type === "assistant" &&
|
|
246
|
+
message.kind === "summary" &&
|
|
247
|
+
message.visibility === "internal",
|
|
248
|
+
);
|
|
249
|
+
if (
|
|
250
|
+
!latest_summary ||
|
|
251
|
+
latest_summary.type !== "assistant" ||
|
|
252
|
+
!latest_summary.summary_through_message_id
|
|
253
|
+
) {
|
|
254
|
+
return messages.filter(
|
|
255
|
+
(message) => message.type === "user" || message.type === "assistant",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const boundary_index = messages.findIndex(
|
|
259
|
+
(message) => message.message_id === latest_summary.summary_through_message_id,
|
|
260
|
+
);
|
|
261
|
+
return [
|
|
262
|
+
latest_summary,
|
|
263
|
+
...messages
|
|
264
|
+
.slice(boundary_index + 1)
|
|
265
|
+
.filter(
|
|
266
|
+
(message) =>
|
|
267
|
+
message !== latest_summary &&
|
|
268
|
+
message.type === "user" ||
|
|
269
|
+
(message.type === "assistant" && message.kind !== "summary"),
|
|
270
|
+
),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function infer_action_type(message_id: string): string {
|
|
275
|
+
return String(message_id || "").split(":")[0] || "action";
|
|
276
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionEventHub
|
|
2
|
+
* SessionEventHub:统一 Session Mutation 分发器。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 收口 `Session.subscribe()` 的订阅/取消订阅能力。
|
|
@@ -7,24 +7,36 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
SessionMutation,
|
|
11
|
+
SessionMutationReply,
|
|
12
|
+
SessionMutationSubscriber,
|
|
13
|
+
SessionMutationUnsubscribe,
|
|
14
|
+
} from "@/types/session/SessionMutation.js";
|
|
15
|
+
|
|
16
|
+
/** SessionEventHub 构造参数。 */
|
|
17
|
+
export interface SessionEventHubOptions {
|
|
18
|
+
/** 为当前 Mutation 创建绑定 Session 和 Tool Part 的回复入口。 */
|
|
19
|
+
create_reply: (mutation: SessionMutation) => SessionMutationReply;
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* SessionEventHub:最小事件总线实现。
|
|
17
24
|
*/
|
|
18
25
|
export class SessionEventHub {
|
|
19
|
-
private readonly subscribers = new Map<number,
|
|
20
|
-
private
|
|
26
|
+
private readonly subscribers = new Map<number, SessionMutationSubscriber>();
|
|
27
|
+
private readonly create_reply: SessionEventHubOptions["create_reply"];
|
|
28
|
+
private next_subscriber_id = 1;
|
|
29
|
+
|
|
30
|
+
constructor(options: SessionEventHubOptions) {
|
|
31
|
+
this.create_reply = options.create_reply;
|
|
32
|
+
}
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* 注册一个新的 Session 事件订阅者。
|
|
24
36
|
*/
|
|
25
|
-
subscribe(subscriber:
|
|
26
|
-
const id = this.
|
|
27
|
-
this.
|
|
37
|
+
subscribe(subscriber: SessionMutationSubscriber): SessionMutationUnsubscribe {
|
|
38
|
+
const id = this.next_subscriber_id;
|
|
39
|
+
this.next_subscriber_id += 1;
|
|
28
40
|
this.subscribers.set(id, subscriber);
|
|
29
41
|
|
|
30
42
|
return () => {
|
|
@@ -35,10 +47,11 @@ export class SessionEventHub {
|
|
|
35
47
|
/**
|
|
36
48
|
* 广播一条 Session 事件。
|
|
37
49
|
*/
|
|
38
|
-
publish(
|
|
50
|
+
publish(mutation: SessionMutation): void {
|
|
51
|
+
const reply = this.create_reply(mutation);
|
|
39
52
|
for (const subscriber of this.subscribers.values()) {
|
|
40
53
|
try {
|
|
41
|
-
subscriber(
|
|
54
|
+
Promise.resolve(subscriber(mutation, reply)).catch(() => undefined);
|
|
42
55
|
} catch {
|
|
43
56
|
// ignore single subscriber failures
|
|
44
57
|
}
|