@downcity/agent 1.1.217 → 1.1.222
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/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +1 -4
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +9 -3
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +19 -4
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +7 -3
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +5 -2
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +25 -11
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +5 -2
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +5 -2
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +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 +5 -2
- 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 +11 -4
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +14 -3
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +13 -3
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +47 -18
- package/bin/session/Session.js.map +1 -1
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts +45 -0
- package/bin/session/recorder/JsonlSessionMessageStore.d.ts.map +1 -0
- package/bin/session/recorder/JsonlSessionMessageStore.js +127 -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 +252 -0
- package/bin/session/recorder/SessionMessageCodec.js.map +1 -0
- package/bin/session/recorder/SessionMessageReducer.d.ts +12 -0
- package/bin/session/recorder/SessionMessageReducer.d.ts.map +1 -0
- package/bin/session/recorder/SessionMessageReducer.js +109 -0
- package/bin/session/recorder/SessionMessageReducer.js.map +1 -0
- package/bin/session/recorder/SessionRecorder.d.ts +177 -0
- package/bin/session/recorder/SessionRecorder.d.ts.map +1 -0
- package/bin/session/recorder/SessionRecorder.js +610 -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 +141 -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/SessionMessageEventHub.d.ts +15 -0
- package/bin/session/runtime/SessionMessageEventHub.d.ts.map +1 -0
- package/bin/session/runtime/SessionMessageEventHub.js +28 -0
- package/bin/session/runtime/SessionMessageEventHub.js.map +1 -0
- package/bin/session/runtime/SessionPromptRuntime.d.ts +7 -13
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +13 -13
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +7 -3
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +132 -72
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +4 -0
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +82 -52
- 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/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 -2
- package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
- package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +10 -4
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +20 -6
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +4 -3
- package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.js.map +1 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts +2 -1
- package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
- package/bin/types/session/SessionMessage.d.ts +217 -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/SessionMessageMutation.d.ts +92 -0
- package/bin/types/session/SessionMessageMutation.d.ts.map +1 -0
- package/bin/types/session/SessionMessageMutation.js +21 -0
- package/bin/types/session/SessionMessageMutation.js.map +1 -0
- package/package.json +3 -3
- package/scripts/agent-env.test.mjs +4 -4
- package/scripts/session-action-message.test.mjs +98 -27
- package/scripts/session-list-title.test.mjs +6 -19
- package/scripts/session-prompt-runtime.test.mjs +4 -16
- package/scripts/session-recorder.test.mjs +245 -0
- package/scripts/session-title-event.test.mjs +10 -18
- package/src/agent/local/services/AgentSessions.ts +1 -3
- package/src/agent/remote/RemoteSession.ts +35 -7
- package/src/agent/remote/RemoteTransport.ts +17 -6
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +45 -13
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +19 -6
- package/src/executor/types/SessionRecords.ts +2 -0
- package/src/index.ts +23 -5
- package/src/rpc/Client.ts +29 -8
- package/src/session/Session.ts +65 -31
- package/src/session/recorder/JsonlSessionMessageStore.ts +162 -0
- package/src/session/recorder/SessionMessageCodec.ts +279 -0
- package/src/session/recorder/SessionMessageReducer.ts +155 -0
- package/src/session/recorder/SessionRecorder.ts +812 -0
- package/src/session/recorder/SessionRecorderCompaction.ts +187 -0
- package/src/session/recorder/SessionRecorderHistoryStore.ts +276 -0
- package/src/session/runtime/SessionMessageEventHub.ts +35 -0
- package/src/session/runtime/SessionPromptRuntime.ts +23 -24
- package/src/session/services/SessionStateService.ts +151 -80
- package/src/session/services/SessionTurnService.ts +88 -59
- package/src/session/services/SessionViewService.ts +25 -13
- package/src/session/storage/Persistence.ts +13 -0
- package/src/session/storage/RuntimeSessionPort.ts +6 -4
- package/src/types/agent/SessionActor.ts +26 -6
- package/src/types/rpc/RpcProtocol.ts +20 -6
- package/src/types/runtime/agent/AgentContext.ts +8 -2
- package/src/types/sdk/AgentSessionEvent.ts +2 -0
- package/src/types/session/SessionMessage.ts +247 -0
- package/src/types/session/SessionMessageMutation.ts +121 -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/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/InternalUiChunkEvent.ts +0 -108
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
type: "text",
|
|
106
|
+
text: summary,
|
|
107
|
+
state: "done",
|
|
108
|
+
}],
|
|
109
|
+
});
|
|
110
|
+
await publish_action(input.compact_input.onAction, {
|
|
111
|
+
id: action_id,
|
|
112
|
+
session_id: input.recorder.session_id,
|
|
113
|
+
title: "Session messages compacted",
|
|
114
|
+
description: `Compacted through Message ${boundary.message_id}.`,
|
|
115
|
+
state: "completed",
|
|
116
|
+
});
|
|
117
|
+
return { compacted: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function project_current_context(messages: SessionMessage[]): SessionMessage[] {
|
|
121
|
+
const latest_summary = [...messages].reverse().find(
|
|
122
|
+
(message): message is SessionAssistantMessage =>
|
|
123
|
+
message.type === "assistant" &&
|
|
124
|
+
message.kind === "summary" &&
|
|
125
|
+
message.visibility === "internal",
|
|
126
|
+
);
|
|
127
|
+
if (!latest_summary?.summary_through_message_id) {
|
|
128
|
+
return messages.filter(
|
|
129
|
+
(message) => message.type === "user" || message.type === "assistant",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const boundary_index = messages.findIndex(
|
|
133
|
+
(message) => message.message_id === latest_summary.summary_through_message_id,
|
|
134
|
+
);
|
|
135
|
+
return [
|
|
136
|
+
latest_summary,
|
|
137
|
+
...messages.slice(boundary_index + 1).filter(
|
|
138
|
+
(message) =>
|
|
139
|
+
message !== latest_summary &&
|
|
140
|
+
(message.type === "user" ||
|
|
141
|
+
message.type === "assistant" && message.kind !== "summary"),
|
|
142
|
+
),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function message_to_compaction_text(message: SessionMessage): string {
|
|
147
|
+
const projected = to_executor_ui_message(message);
|
|
148
|
+
if (!projected) return "";
|
|
149
|
+
const text = projected.parts
|
|
150
|
+
.flatMap((part) => part.type === "text" ? [String(part.text || "")] : [])
|
|
151
|
+
.join("\n")
|
|
152
|
+
.trim();
|
|
153
|
+
return text ? `${message.type}: ${text}` : "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assistant_text(message: SessionAssistantMessage): string {
|
|
157
|
+
return message.parts
|
|
158
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
159
|
+
.join("\n")
|
|
160
|
+
.trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function publish_action(
|
|
164
|
+
callback: SessionHistoryCompactInput["onAction"],
|
|
165
|
+
input: {
|
|
166
|
+
id: string;
|
|
167
|
+
session_id: string;
|
|
168
|
+
title: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
state: "running" | "completed";
|
|
171
|
+
},
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
if (!callback) return;
|
|
174
|
+
const action: SessionActionRecordV1 = {
|
|
175
|
+
type: "action",
|
|
176
|
+
id: input.id,
|
|
177
|
+
title: input.title,
|
|
178
|
+
...(input.description ? { description: input.description } : {}),
|
|
179
|
+
state: input.state,
|
|
180
|
+
metadata: {
|
|
181
|
+
v: 1,
|
|
182
|
+
ts: Date.now(),
|
|
183
|
+
sessionId: input.session_id,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
await callback(action);
|
|
187
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionRecorder 到 Executor SessionHistoryStore 的内部投影适配器。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - Recorder messages.jsonl 是唯一持久化事实源。
|
|
6
|
+
* - Executor 继续读取临时 UIMessage 投影,不直接拥有文件写入权。
|
|
7
|
+
* - inflight 写入已经由 AssistantMessageWriter 取代,因此对应接口只提供投影兼容。
|
|
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 已经逐 delta 保存 assistant,不再维护第二份 inflight。 */
|
|
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
|
+
/** 旧 inflight 写入口已废弃;流式内容由 Recorder writer 负责。 */
|
|
128
|
+
async write_inflight(_message: SessionRecordV1): Promise<void> {}
|
|
129
|
+
|
|
130
|
+
/** 旧 inflight 收口入口已废弃;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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionMessage mutation 事件总线。
|
|
3
|
+
*
|
|
4
|
+
* EventHub 只广播 Recorder 已成功提交的 mutation,不创建或转换事件。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
SessionMessageMutation,
|
|
9
|
+
SessionMessageMutationSubscriber,
|
|
10
|
+
SessionMessageMutationUnsubscribe,
|
|
11
|
+
} from "@/types/session/SessionMessageMutation.js";
|
|
12
|
+
|
|
13
|
+
/** SessionMessage mutation 的进程内广播器。 */
|
|
14
|
+
export class SessionMessageEventHub {
|
|
15
|
+
private readonly subscribers = new Set<SessionMessageMutationSubscriber>();
|
|
16
|
+
|
|
17
|
+
/** 订阅未来成功提交的 mutation。 */
|
|
18
|
+
subscribe(subscriber: SessionMessageMutationSubscriber): SessionMessageMutationUnsubscribe {
|
|
19
|
+
this.subscribers.add(subscriber);
|
|
20
|
+
return () => {
|
|
21
|
+
this.subscribers.delete(subscriber);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 广播一条已持久化 mutation。 */
|
|
26
|
+
publish(mutation: SessionMessageMutation): void {
|
|
27
|
+
for (const subscriber of this.subscribers) {
|
|
28
|
+
try {
|
|
29
|
+
subscriber(mutation);
|
|
30
|
+
} catch {
|
|
31
|
+
// 单个订阅者失败不能影响提交结果和其他订阅者。
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -96,21 +96,16 @@ export interface SessionPromptRuntimeOptions {
|
|
|
96
96
|
*/
|
|
97
97
|
createAndPersistUserMessage: (
|
|
98
98
|
input: AgentSessionPromptInput,
|
|
99
|
+
turn_id: string,
|
|
100
|
+
input_type: "prompt" | "steer",
|
|
99
101
|
) => Promise<SessionUserMessageV1>;
|
|
100
102
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
emit_steer_action: (input: {
|
|
105
|
-
/**
|
|
106
|
-
* steer message 所属的当前 turn 标识。
|
|
107
|
-
*/
|
|
103
|
+
/** 持久化一条用户可见的 turn 错误 Message。 */
|
|
104
|
+
appendErrorMessage: (input: {
|
|
105
|
+
/** 错误所属 turn。 */
|
|
108
106
|
turn_id: string;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* 已成功持久化的 steer message。
|
|
112
|
-
*/
|
|
113
|
-
message: SessionUserMessageV1;
|
|
107
|
+
/** 用户可见错误文本。 */
|
|
108
|
+
message: string;
|
|
114
109
|
}) => Promise<void>;
|
|
115
110
|
|
|
116
111
|
/**
|
|
@@ -141,7 +136,7 @@ export class SessionPromptRuntime {
|
|
|
141
136
|
private readonly sessionId: string;
|
|
142
137
|
private readonly publish: SessionPromptRuntimeOptions["publish"];
|
|
143
138
|
private readonly createAndPersistUserMessage: SessionPromptRuntimeOptions["createAndPersistUserMessage"];
|
|
144
|
-
private readonly
|
|
139
|
+
private readonly appendErrorMessage: SessionPromptRuntimeOptions["appendErrorMessage"];
|
|
145
140
|
private readonly executeTurn: SessionPromptRuntimeOptions["executeTurn"];
|
|
146
141
|
private readonly stopTurn: SessionPromptRuntimeOptions["stopTurn"];
|
|
147
142
|
private readonly queue: QueuedPrompt[] = [];
|
|
@@ -152,7 +147,7 @@ export class SessionPromptRuntime {
|
|
|
152
147
|
this.sessionId = String(options.sessionId || "").trim();
|
|
153
148
|
this.publish = options.publish;
|
|
154
149
|
this.createAndPersistUserMessage = options.createAndPersistUserMessage;
|
|
155
|
-
this.
|
|
150
|
+
this.appendErrorMessage = options.appendErrorMessage;
|
|
156
151
|
this.executeTurn = options.executeTurn;
|
|
157
152
|
this.stopTurn = options.stopTurn;
|
|
158
153
|
if (!this.sessionId) {
|
|
@@ -237,7 +232,7 @@ export class SessionPromptRuntime {
|
|
|
237
232
|
current.deferredHandle.resolve(createTurnHandle(activeTurn));
|
|
238
233
|
|
|
239
234
|
try {
|
|
240
|
-
await this.createAndPersistUserMessage(current.input);
|
|
235
|
+
await this.createAndPersistUserMessage(current.input, turnId, "prompt");
|
|
241
236
|
const result = await this.executeTurn({
|
|
242
237
|
turnId,
|
|
243
238
|
promptInput: current.input,
|
|
@@ -283,6 +278,14 @@ export class SessionPromptRuntime {
|
|
|
283
278
|
};
|
|
284
279
|
activeTurn.result = finalResult;
|
|
285
280
|
if (message !== TURN_STOPPED_MESSAGE) {
|
|
281
|
+
try {
|
|
282
|
+
await this.appendErrorMessage({
|
|
283
|
+
turn_id: turnId,
|
|
284
|
+
message,
|
|
285
|
+
});
|
|
286
|
+
} catch {
|
|
287
|
+
// Error Message 写入失败不能阻止 turn handle 收口。
|
|
288
|
+
}
|
|
286
289
|
this.publish({
|
|
287
290
|
type: "error",
|
|
288
291
|
message,
|
|
@@ -348,17 +351,13 @@ export class SessionPromptRuntime {
|
|
|
348
351
|
for (let index = 0; index < drained.length; index += 1) {
|
|
349
352
|
const item = drained[index];
|
|
350
353
|
try {
|
|
351
|
-
const message = await this.createAndPersistUserMessage(
|
|
354
|
+
const message = await this.createAndPersistUserMessage(
|
|
355
|
+
item.input,
|
|
356
|
+
activeTurn.turnId,
|
|
357
|
+
"steer",
|
|
358
|
+
);
|
|
352
359
|
item.deferredHandle.resolve(createTurnHandle(activeTurn));
|
|
353
360
|
merged.push(message);
|
|
354
|
-
try {
|
|
355
|
-
await this.emit_steer_action({
|
|
356
|
-
turn_id: activeTurn.turnId,
|
|
357
|
-
message,
|
|
358
|
-
});
|
|
359
|
-
} catch {
|
|
360
|
-
// action 发送失败不应阻断已经持久化的 steer message。
|
|
361
|
-
}
|
|
362
361
|
} catch {
|
|
363
362
|
// 关键点(中文):若某条消息持久化失败,把未处理部分重新放回队列头部,避免静默丢失。
|
|
364
363
|
const remaining = drained.slice(index);
|