@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,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionRecorder:Session Message 的唯一持久化与发布入口。
|
|
3
|
+
*
|
|
4
|
+
* Mutation 先写入单一 messages.jsonl,成功后更新内存 snapshot,最后发布同一个对象。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UIMessage, UIMessageChunk } from "ai";
|
|
8
|
+
import { generateId } from "@/utils/Id.js";
|
|
9
|
+
import { JsonlSessionMessageStore } from "@/session/recorder/JsonlSessionMessageStore.js";
|
|
10
|
+
import { reduce_session_message } from "@/session/recorder/SessionMessageReducer.js";
|
|
11
|
+
import type { JsonObject, JsonValue } from "@/types/common/Json.js";
|
|
12
|
+
import type {
|
|
13
|
+
ListSessionMessagesInput,
|
|
14
|
+
SessionActionMessage,
|
|
15
|
+
SessionAssistantMessage,
|
|
16
|
+
SessionAssistantMessagePart,
|
|
17
|
+
SessionAssistantToolPart,
|
|
18
|
+
SessionErrorMessage,
|
|
19
|
+
SessionMessage,
|
|
20
|
+
SessionMessagePage,
|
|
21
|
+
SessionUserMessage,
|
|
22
|
+
SessionUserMessagePart,
|
|
23
|
+
} from "@/types/session/SessionMessage.js";
|
|
24
|
+
import type {
|
|
25
|
+
ListSessionMessageChangesInput,
|
|
26
|
+
SessionMessageMutation,
|
|
27
|
+
SessionMessageMutationPage,
|
|
28
|
+
SessionMessageMutationSubscriber,
|
|
29
|
+
SessionMessageMutationUnsubscribe,
|
|
30
|
+
SessionMessageSnapshotMutation,
|
|
31
|
+
SessionPartSnapshotMutation,
|
|
32
|
+
} from "@/types/session/SessionMessageMutation.js";
|
|
33
|
+
|
|
34
|
+
/** SessionRecorder 构造参数。 */
|
|
35
|
+
export interface SessionRecorderOptions {
|
|
36
|
+
/** 当前 Session 标识。 */
|
|
37
|
+
session_id: string;
|
|
38
|
+
/** 单一 JSONL Mutation store。 */
|
|
39
|
+
store: JsonlSessionMessageStore;
|
|
40
|
+
/** Mutation 成功提交后的发布函数。 */
|
|
41
|
+
publish: (mutation: SessionMessageMutation) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** User Message 创建参数。 */
|
|
45
|
+
export interface AppendSessionUserMessageInput {
|
|
46
|
+
/** 当前输入所属 turn。 */
|
|
47
|
+
turn_id: string;
|
|
48
|
+
/** 普通 prompt 或 steering 输入。 */
|
|
49
|
+
input_type: "prompt" | "steer";
|
|
50
|
+
/** User 结构化 parts。 */
|
|
51
|
+
parts: SessionUserMessagePart[];
|
|
52
|
+
/** 可选指定 Message ID,通常由 Recorder 生成。 */
|
|
53
|
+
message_id?: string;
|
|
54
|
+
/** 默认展示范围。 */
|
|
55
|
+
visibility?: "visible" | "internal";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Assistant Message 创建参数。 */
|
|
59
|
+
export interface OpenSessionAssistantMessageInput {
|
|
60
|
+
/** 当前 assistant 所属 turn。 */
|
|
61
|
+
turn_id: string;
|
|
62
|
+
/** 当前 assistant 在 turn 内的 segment 序号。 */
|
|
63
|
+
segment_index: number;
|
|
64
|
+
/** 普通 assistant 或 compact summary。 */
|
|
65
|
+
kind?: "normal" | "summary";
|
|
66
|
+
/** 默认展示范围。 */
|
|
67
|
+
visibility?: "visible" | "internal";
|
|
68
|
+
/** 可选指定 Message ID。 */
|
|
69
|
+
message_id?: string;
|
|
70
|
+
/** Summary 已覆盖到的来源 Message。 */
|
|
71
|
+
summary_through_message_id?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 已完成 Assistant Message 直接写入参数。 */
|
|
75
|
+
export interface AppendCompletedAssistantMessageInput {
|
|
76
|
+
/** Assistant 所属 turn。 */
|
|
77
|
+
turn_id?: string;
|
|
78
|
+
/** Assistant 完整 parts。 */
|
|
79
|
+
parts: SessionAssistantMessagePart[];
|
|
80
|
+
/** 普通 assistant 或 compact summary。 */
|
|
81
|
+
kind?: "normal" | "summary";
|
|
82
|
+
/** 默认展示范围。 */
|
|
83
|
+
visibility?: "visible" | "internal";
|
|
84
|
+
/** Summary 已覆盖到的来源 Message。 */
|
|
85
|
+
summary_through_message_id?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Action Message 创建参数。 */
|
|
89
|
+
export interface OpenSessionActionMessageInput {
|
|
90
|
+
/** 可选稳定 Message ID;业务 action 生命周期使用该值定位。 */
|
|
91
|
+
message_id?: string;
|
|
92
|
+
/** Action 所属 turn。 */
|
|
93
|
+
turn_id?: string;
|
|
94
|
+
/** Action 业务类型。 */
|
|
95
|
+
action_type: string;
|
|
96
|
+
/** Action 标题。 */
|
|
97
|
+
title: string;
|
|
98
|
+
/** Action 描述。 */
|
|
99
|
+
description?: string;
|
|
100
|
+
/** Action 附加数据。 */
|
|
101
|
+
data?: JsonObject;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Error Message 创建参数。 */
|
|
105
|
+
export interface AppendSessionErrorMessageInput {
|
|
106
|
+
/** 错误影响范围。 */
|
|
107
|
+
scope: "session" | "turn";
|
|
108
|
+
/** 错误所属 turn。 */
|
|
109
|
+
turn_id?: string;
|
|
110
|
+
/** 稳定错误码。 */
|
|
111
|
+
code: string;
|
|
112
|
+
/** 用户可见错误文本。 */
|
|
113
|
+
message: string;
|
|
114
|
+
/** 是否允许恢复。 */
|
|
115
|
+
recoverable: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 唯一 Session Message 写入服务。 */
|
|
119
|
+
export class SessionRecorder {
|
|
120
|
+
readonly session_id: string;
|
|
121
|
+
private readonly store: JsonlSessionMessageStore;
|
|
122
|
+
private readonly publish: SessionRecorderOptions["publish"];
|
|
123
|
+
private readonly subscribers = new Set<SessionMessageMutationSubscriber>();
|
|
124
|
+
private readonly messages_by_id = new Map<string, SessionMessage>();
|
|
125
|
+
private initialized = false;
|
|
126
|
+
|
|
127
|
+
constructor(options: SessionRecorderOptions) {
|
|
128
|
+
this.session_id = String(options.session_id || "").trim();
|
|
129
|
+
this.store = options.store;
|
|
130
|
+
this.publish = options.publish;
|
|
131
|
+
if (!this.session_id) throw new Error("SessionRecorder requires session_id");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 重放已有 Mutation,并收口进程中断遗留的运行状态。 */
|
|
135
|
+
async initialize(): Promise<void> {
|
|
136
|
+
if (this.initialized) return;
|
|
137
|
+
await this.store.initialize();
|
|
138
|
+
for (const message of await this.store.list_messages()) {
|
|
139
|
+
this.messages_by_id.set(message.message_id, message);
|
|
140
|
+
}
|
|
141
|
+
this.initialized = true;
|
|
142
|
+
const unfinished = [...this.messages_by_id.values()];
|
|
143
|
+
for (const message of unfinished) {
|
|
144
|
+
if (message.type === "assistant" && message.status === "streaming") {
|
|
145
|
+
await this.complete_assistant_message(message.message_id, "stopped");
|
|
146
|
+
}
|
|
147
|
+
if (message.type === "action" && message.status === "running") {
|
|
148
|
+
await this.update_action_message(message.message_id, "failed", {
|
|
149
|
+
description: message.description || "Action interrupted before completion.",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 同步读取当前内存 Message。 */
|
|
156
|
+
get_message(message_id: string): SessionMessage | undefined {
|
|
157
|
+
return this.messages_by_id.get(message_id);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 追加普通 prompt 或 steering User Message。 */
|
|
161
|
+
async append_user_message(
|
|
162
|
+
input: AppendSessionUserMessageInput,
|
|
163
|
+
): Promise<SessionUserMessage> {
|
|
164
|
+
const message = await this.create_message((sequence, created_at) => ({
|
|
165
|
+
message_id:
|
|
166
|
+
String(input.message_id || "").trim() ||
|
|
167
|
+
`user:${this.session_id}:${generateId()}`,
|
|
168
|
+
session_id: this.session_id,
|
|
169
|
+
turn_id: input.turn_id,
|
|
170
|
+
sequence,
|
|
171
|
+
revision: 1,
|
|
172
|
+
visibility: input.visibility || "visible",
|
|
173
|
+
created_at,
|
|
174
|
+
updated_at: created_at,
|
|
175
|
+
type: "user",
|
|
176
|
+
input_type: input.input_type,
|
|
177
|
+
parts: structuredClone(input.parts),
|
|
178
|
+
}));
|
|
179
|
+
return message as SessionUserMessage;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 创建可持续接收 chunk 的 Assistant segment。 */
|
|
183
|
+
async open_assistant_message(
|
|
184
|
+
input: OpenSessionAssistantMessageInput,
|
|
185
|
+
): Promise<SessionAssistantMessageWriter> {
|
|
186
|
+
const message = (await this.create_message((sequence, created_at) => ({
|
|
187
|
+
message_id:
|
|
188
|
+
String(input.message_id || "").trim() ||
|
|
189
|
+
`assistant:${this.session_id}:${generateId()}`,
|
|
190
|
+
session_id: this.session_id,
|
|
191
|
+
turn_id: input.turn_id,
|
|
192
|
+
sequence,
|
|
193
|
+
revision: 1,
|
|
194
|
+
visibility: input.visibility || "visible",
|
|
195
|
+
created_at,
|
|
196
|
+
updated_at: created_at,
|
|
197
|
+
type: "assistant",
|
|
198
|
+
kind: input.kind || "normal",
|
|
199
|
+
segment_index: input.segment_index,
|
|
200
|
+
status: "streaming",
|
|
201
|
+
parts: [],
|
|
202
|
+
...(input.summary_through_message_id
|
|
203
|
+
? { summary_through_message_id: input.summary_through_message_id }
|
|
204
|
+
: {}),
|
|
205
|
+
}))) as SessionAssistantMessage;
|
|
206
|
+
return new SessionAssistantMessageWriter(this, message.message_id);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 直接写入一条已完成 Assistant Message。 */
|
|
210
|
+
async append_completed_assistant_message(
|
|
211
|
+
input: AppendCompletedAssistantMessageInput,
|
|
212
|
+
): Promise<SessionAssistantMessage> {
|
|
213
|
+
const turn_id = input.turn_id || `external:${this.session_id}:${generateId()}`;
|
|
214
|
+
const writer = await this.open_assistant_message({
|
|
215
|
+
turn_id,
|
|
216
|
+
segment_index: 1,
|
|
217
|
+
kind: input.kind || "normal",
|
|
218
|
+
visibility: input.visibility || "visible",
|
|
219
|
+
...(input.summary_through_message_id
|
|
220
|
+
? { summary_through_message_id: input.summary_through_message_id }
|
|
221
|
+
: {}),
|
|
222
|
+
});
|
|
223
|
+
for (const part of input.parts) await writer.upsert_part(part);
|
|
224
|
+
await writer.complete();
|
|
225
|
+
return this.get_message(writer.message_id) as SessionAssistantMessage;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 创建 running Action Message。 */
|
|
229
|
+
async open_action_message(
|
|
230
|
+
input: OpenSessionActionMessageInput,
|
|
231
|
+
): Promise<SessionActionMessageWriter> {
|
|
232
|
+
const message = (await this.create_message((sequence, created_at) => ({
|
|
233
|
+
message_id:
|
|
234
|
+
String(input.message_id || "").trim() ||
|
|
235
|
+
`action:${this.session_id}:${generateId()}`,
|
|
236
|
+
session_id: this.session_id,
|
|
237
|
+
...(input.turn_id ? { turn_id: input.turn_id } : {}),
|
|
238
|
+
sequence,
|
|
239
|
+
revision: 1,
|
|
240
|
+
visibility: "visible",
|
|
241
|
+
created_at,
|
|
242
|
+
updated_at: created_at,
|
|
243
|
+
type: "action",
|
|
244
|
+
action_type: input.action_type,
|
|
245
|
+
status: "running",
|
|
246
|
+
title: input.title,
|
|
247
|
+
...(input.description ? { description: input.description } : {}),
|
|
248
|
+
...(input.data ? { data: structuredClone(input.data) } : {}),
|
|
249
|
+
}))) as SessionActionMessage;
|
|
250
|
+
return new SessionActionMessageWriter(this, message.message_id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 更新 Action 状态,同时保持 message_id 与 sequence 不变。 */
|
|
254
|
+
async update_action_message(
|
|
255
|
+
message_id: string,
|
|
256
|
+
status: "running" | "completed" | "failed",
|
|
257
|
+
changes?: { title?: string; description?: string; data?: JsonObject },
|
|
258
|
+
): Promise<SessionActionMessage> {
|
|
259
|
+
const mutation = await this.store.commit((state) => {
|
|
260
|
+
const current = require_message(state.messages, message_id, "action");
|
|
261
|
+
const created_at = Date.now();
|
|
262
|
+
const message: SessionActionMessage = {
|
|
263
|
+
...current,
|
|
264
|
+
status,
|
|
265
|
+
...(changes?.title ? { title: changes.title } : {}),
|
|
266
|
+
...(changes?.description !== undefined
|
|
267
|
+
? { description: changes.description }
|
|
268
|
+
: {}),
|
|
269
|
+
...(changes?.data ? { data: structuredClone(changes.data) } : {}),
|
|
270
|
+
revision: current.revision + 1,
|
|
271
|
+
updated_at: created_at,
|
|
272
|
+
};
|
|
273
|
+
return this.build_message_mutation(state.commit_sequence, message);
|
|
274
|
+
});
|
|
275
|
+
this.accept_mutation(mutation);
|
|
276
|
+
return this.get_message(message_id) as SessionActionMessage;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 创建用户可见 Error Message。 */
|
|
280
|
+
async append_error_message(
|
|
281
|
+
input: AppendSessionErrorMessageInput,
|
|
282
|
+
): Promise<SessionErrorMessage> {
|
|
283
|
+
return (await this.create_message((sequence, created_at) => ({
|
|
284
|
+
message_id: `error:${this.session_id}:${generateId()}`,
|
|
285
|
+
session_id: this.session_id,
|
|
286
|
+
...(input.turn_id ? { turn_id: input.turn_id } : {}),
|
|
287
|
+
sequence,
|
|
288
|
+
revision: 1,
|
|
289
|
+
visibility: "visible",
|
|
290
|
+
created_at,
|
|
291
|
+
updated_at: created_at,
|
|
292
|
+
type: "error",
|
|
293
|
+
scope: input.scope,
|
|
294
|
+
code: input.code,
|
|
295
|
+
message: input.message,
|
|
296
|
+
recoverable: input.recoverable,
|
|
297
|
+
}))) as SessionErrorMessage;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 读取折叠后的 Message snapshot。 */
|
|
301
|
+
async list_messages(
|
|
302
|
+
input?: ListSessionMessagesInput,
|
|
303
|
+
): Promise<SessionMessagePage> {
|
|
304
|
+
await this.ensure_initialized();
|
|
305
|
+
const mutations = await this.store.list_mutations();
|
|
306
|
+
const latest_commit_sequence =
|
|
307
|
+
mutations[mutations.length - 1]?.commit_sequence || 0;
|
|
308
|
+
const include_internal = input?.include_internal === true;
|
|
309
|
+
const through_sequence = Number.isFinite(input?.through_sequence)
|
|
310
|
+
? Number(input?.through_sequence)
|
|
311
|
+
: Number.POSITIVE_INFINITY;
|
|
312
|
+
const before_sequence = Number.isFinite(input?.before_sequence)
|
|
313
|
+
? Number(input?.before_sequence)
|
|
314
|
+
: Number.POSITIVE_INFINITY;
|
|
315
|
+
const all = [...this.messages_by_id.values()]
|
|
316
|
+
.filter(
|
|
317
|
+
(message) =>
|
|
318
|
+
(include_internal || message.visibility === "visible") &&
|
|
319
|
+
message.sequence <= through_sequence &&
|
|
320
|
+
message.sequence < before_sequence,
|
|
321
|
+
)
|
|
322
|
+
.sort((left, right) => left.sequence - right.sequence);
|
|
323
|
+
const offset = decode_cursor(input?.cursor);
|
|
324
|
+
const limit = normalize_limit(input?.limit, 100, 500);
|
|
325
|
+
const items = all.slice(offset, offset + limit).map((item) => structuredClone(item));
|
|
326
|
+
const next_offset = offset + items.length;
|
|
327
|
+
return {
|
|
328
|
+
items,
|
|
329
|
+
total: all.length,
|
|
330
|
+
...(next_offset < all.length ? { next_cursor: encode_cursor(next_offset) } : {}),
|
|
331
|
+
has_more: next_offset < all.length,
|
|
332
|
+
latest_commit_sequence,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** 从指定 commit_sequence 增量读取 Mutation。 */
|
|
337
|
+
async list_message_changes(
|
|
338
|
+
input: ListSessionMessageChangesInput,
|
|
339
|
+
): Promise<SessionMessageMutationPage> {
|
|
340
|
+
await this.ensure_initialized();
|
|
341
|
+
const after = Math.max(0, Math.floor(input.after_commit_sequence || 0));
|
|
342
|
+
const limit = normalize_limit(input.limit, 200, 1000);
|
|
343
|
+
const all = await this.store.list_mutations();
|
|
344
|
+
const matching = all.filter((item) => item.commit_sequence > after);
|
|
345
|
+
const items = matching.slice(0, limit);
|
|
346
|
+
const latest_commit_sequence = all[all.length - 1]?.commit_sequence || 0;
|
|
347
|
+
return {
|
|
348
|
+
items,
|
|
349
|
+
has_more: matching.length > items.length,
|
|
350
|
+
next_commit_sequence:
|
|
351
|
+
items[items.length - 1]?.commit_sequence || after,
|
|
352
|
+
latest_commit_sequence,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 订阅已成功持久化的未来 Mutation。 */
|
|
357
|
+
subscribe(
|
|
358
|
+
subscriber: SessionMessageMutationSubscriber,
|
|
359
|
+
): SessionMessageMutationUnsubscribe {
|
|
360
|
+
this.subscribers.add(subscriber);
|
|
361
|
+
return () => this.subscribers.delete(subscriber);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** 向当前 Session 导入 fork 来源 Message,并重新分配全部身份和顺序。 */
|
|
365
|
+
async import_messages(messages: SessionMessage[]): Promise<void> {
|
|
366
|
+
const turn_ids = new Map<string, string>();
|
|
367
|
+
const message_ids = new Map<string, string>();
|
|
368
|
+
for (const source of [...messages].sort((a, b) => a.sequence - b.sequence)) {
|
|
369
|
+
const turn_id = source.turn_id
|
|
370
|
+
? resolve_import_id(turn_ids, source.turn_id, "turn")
|
|
371
|
+
: undefined;
|
|
372
|
+
const message_id = resolve_import_id(
|
|
373
|
+
message_ids,
|
|
374
|
+
source.message_id,
|
|
375
|
+
source.type,
|
|
376
|
+
);
|
|
377
|
+
await this.create_message((sequence, created_at) => ({
|
|
378
|
+
...structuredClone(source),
|
|
379
|
+
message_id,
|
|
380
|
+
session_id: this.session_id,
|
|
381
|
+
...(turn_id ? { turn_id } : {}),
|
|
382
|
+
sequence,
|
|
383
|
+
revision: 1,
|
|
384
|
+
created_at,
|
|
385
|
+
updated_at: created_at,
|
|
386
|
+
origin: {
|
|
387
|
+
session_id: source.session_id,
|
|
388
|
+
message_id: source.message_id,
|
|
389
|
+
...(source.turn_id ? { turn_id: source.turn_id } : {}),
|
|
390
|
+
},
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @internal 写入 Assistant 原始文本 delta。 */
|
|
396
|
+
async append_assistant_delta(
|
|
397
|
+
message_id: string,
|
|
398
|
+
part_id: string,
|
|
399
|
+
type: "text" | "reasoning",
|
|
400
|
+
delta: string,
|
|
401
|
+
): Promise<void> {
|
|
402
|
+
if (!delta) return;
|
|
403
|
+
const mutation = await this.store.commit((state) => {
|
|
404
|
+
const current = require_message(state.messages, message_id, "assistant");
|
|
405
|
+
return {
|
|
406
|
+
mutation_id: generateId(),
|
|
407
|
+
variant: "delta",
|
|
408
|
+
type,
|
|
409
|
+
commit_sequence: state.commit_sequence,
|
|
410
|
+
message_id,
|
|
411
|
+
revision: current.revision + 1,
|
|
412
|
+
session_id: this.session_id,
|
|
413
|
+
turn_id: current.turn_id,
|
|
414
|
+
created_at: Date.now(),
|
|
415
|
+
part_id,
|
|
416
|
+
delta,
|
|
417
|
+
} satisfies SessionMessageMutation;
|
|
418
|
+
});
|
|
419
|
+
this.accept_mutation(mutation);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** @internal 写入 Assistant 完整 part。 */
|
|
423
|
+
async update_assistant_part(
|
|
424
|
+
message_id: string,
|
|
425
|
+
part: SessionAssistantMessagePart,
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const mutation = await this.store.commit((state) => {
|
|
428
|
+
const current = require_message(state.messages, message_id, "assistant");
|
|
429
|
+
return {
|
|
430
|
+
mutation_id: generateId(),
|
|
431
|
+
variant: "part",
|
|
432
|
+
type: part.type,
|
|
433
|
+
commit_sequence: state.commit_sequence,
|
|
434
|
+
message_id,
|
|
435
|
+
revision: current.revision + 1,
|
|
436
|
+
session_id: this.session_id,
|
|
437
|
+
turn_id: current.turn_id,
|
|
438
|
+
created_at: Date.now(),
|
|
439
|
+
part_id: part.part_id,
|
|
440
|
+
part: structuredClone(part),
|
|
441
|
+
} as SessionPartSnapshotMutation;
|
|
442
|
+
});
|
|
443
|
+
this.accept_mutation(mutation);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** @internal 收口 Assistant Message。 */
|
|
447
|
+
async complete_assistant_message(
|
|
448
|
+
message_id: string,
|
|
449
|
+
status: "completed" | "stopped" | "failed",
|
|
450
|
+
): Promise<void> {
|
|
451
|
+
const mutation = await this.store.commit((state) => {
|
|
452
|
+
const current = require_message(state.messages, message_id, "assistant");
|
|
453
|
+
const created_at = Date.now();
|
|
454
|
+
const message: SessionAssistantMessage = {
|
|
455
|
+
...current,
|
|
456
|
+
revision: current.revision + 1,
|
|
457
|
+
status,
|
|
458
|
+
updated_at: created_at,
|
|
459
|
+
parts: current.parts.map((part) =>
|
|
460
|
+
part.type === "text" || part.type === "reasoning"
|
|
461
|
+
? { ...part, state: "done" as const }
|
|
462
|
+
: part,
|
|
463
|
+
),
|
|
464
|
+
};
|
|
465
|
+
return this.build_message_mutation(state.commit_sequence, message);
|
|
466
|
+
});
|
|
467
|
+
this.accept_mutation(mutation);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private async create_message(
|
|
471
|
+
factory: (sequence: number, created_at: number) => SessionMessage,
|
|
472
|
+
): Promise<SessionMessage> {
|
|
473
|
+
const mutation = await this.store.commit((state) => {
|
|
474
|
+
const message = factory(state.message_sequence, Date.now());
|
|
475
|
+
return this.build_message_mutation(state.commit_sequence, message);
|
|
476
|
+
});
|
|
477
|
+
this.accept_mutation(mutation);
|
|
478
|
+
return (mutation as SessionMessageSnapshotMutation).message;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private build_message_mutation(
|
|
482
|
+
commit_sequence: number,
|
|
483
|
+
message: SessionMessage,
|
|
484
|
+
): SessionMessageSnapshotMutation {
|
|
485
|
+
return {
|
|
486
|
+
mutation_id: generateId(),
|
|
487
|
+
variant: "message",
|
|
488
|
+
type: message.type,
|
|
489
|
+
commit_sequence,
|
|
490
|
+
message_id: message.message_id,
|
|
491
|
+
sequence: message.sequence,
|
|
492
|
+
revision: message.revision,
|
|
493
|
+
session_id: this.session_id,
|
|
494
|
+
...(message.turn_id ? { turn_id: message.turn_id } : {}),
|
|
495
|
+
created_at: message.updated_at,
|
|
496
|
+
message,
|
|
497
|
+
} as SessionMessageSnapshotMutation;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private accept_mutation(mutation: SessionMessageMutation): void {
|
|
501
|
+
const current = this.messages_by_id.get(mutation.message_id);
|
|
502
|
+
const next = reduce_session_message(current, mutation);
|
|
503
|
+
this.messages_by_id.set(next.message_id, next);
|
|
504
|
+
this.publish(mutation);
|
|
505
|
+
for (const subscriber of this.subscribers) {
|
|
506
|
+
try {
|
|
507
|
+
subscriber(mutation);
|
|
508
|
+
} catch {
|
|
509
|
+
// 单个订阅者异常不能影响已经完成的提交。
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private async ensure_initialized(): Promise<void> {
|
|
515
|
+
if (!this.initialized) await this.initialize();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** 单个 Assistant segment 的流式 writer。 */
|
|
520
|
+
export class SessionAssistantMessageWriter {
|
|
521
|
+
readonly message_id: string;
|
|
522
|
+
private readonly recorder: SessionRecorder;
|
|
523
|
+
private closed = false;
|
|
524
|
+
|
|
525
|
+
constructor(recorder: SessionRecorder, message_id: string) {
|
|
526
|
+
this.recorder = recorder;
|
|
527
|
+
this.message_id = message_id;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** 应用一个原始 AI SDK UI chunk。 */
|
|
531
|
+
async apply_chunk(chunk: UIMessageChunk): Promise<void> {
|
|
532
|
+
if (this.closed) throw new Error("Assistant Message writer is closed");
|
|
533
|
+
const current = this.current_message();
|
|
534
|
+
switch (chunk.type) {
|
|
535
|
+
case "text-start":
|
|
536
|
+
case "reasoning-start":
|
|
537
|
+
await this.upsert_part({
|
|
538
|
+
part_id: `${chunk.type === "text-start" ? "text" : "reasoning"}:${chunk.id}`,
|
|
539
|
+
type: chunk.type === "text-start" ? "text" : "reasoning",
|
|
540
|
+
text: "",
|
|
541
|
+
state: "streaming",
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
case "text-delta":
|
|
545
|
+
case "reasoning-delta":
|
|
546
|
+
await this.recorder.append_assistant_delta(
|
|
547
|
+
this.message_id,
|
|
548
|
+
`${chunk.type === "text-delta" ? "text" : "reasoning"}:${chunk.id}`,
|
|
549
|
+
chunk.type === "text-delta" ? "text" : "reasoning",
|
|
550
|
+
chunk.delta,
|
|
551
|
+
);
|
|
552
|
+
return;
|
|
553
|
+
case "text-end":
|
|
554
|
+
case "reasoning-end": {
|
|
555
|
+
const part_id = `${chunk.type === "text-end" ? "text" : "reasoning"}:${chunk.id}`;
|
|
556
|
+
const part = current.parts.find((item) => item.part_id === part_id);
|
|
557
|
+
if (part?.type === "text" || part?.type === "reasoning") {
|
|
558
|
+
await this.upsert_part({ ...part, state: "done" });
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
case "tool-input-start":
|
|
563
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
564
|
+
tool_name: chunk.toolName,
|
|
565
|
+
state: "input-streaming",
|
|
566
|
+
input_text: "",
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
case "tool-input-delta": {
|
|
570
|
+
const tool = this.find_tool(chunk.toolCallId);
|
|
571
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
572
|
+
tool_name: tool?.tool_name || "unknown",
|
|
573
|
+
state: "input-streaming",
|
|
574
|
+
input_text: `${tool?.input_text || ""}${chunk.inputTextDelta}`,
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
case "tool-input-available":
|
|
579
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
580
|
+
tool_name: chunk.toolName,
|
|
581
|
+
state: "running",
|
|
582
|
+
input: to_json_value(chunk.input),
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
case "tool-input-error":
|
|
586
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
587
|
+
tool_name: chunk.toolName,
|
|
588
|
+
state: "failed",
|
|
589
|
+
input: to_json_value(chunk.input),
|
|
590
|
+
error: chunk.errorText,
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
case "tool-approval-request": {
|
|
594
|
+
const tool = this.find_tool(chunk.toolCallId);
|
|
595
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
596
|
+
tool_name: tool?.tool_name || "unknown",
|
|
597
|
+
state: "approval-required",
|
|
598
|
+
approval_id: chunk.approvalId,
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
case "tool-output-available": {
|
|
603
|
+
const tool = this.find_tool(chunk.toolCallId);
|
|
604
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
605
|
+
tool_name: tool?.tool_name || "unknown",
|
|
606
|
+
state: "completed",
|
|
607
|
+
output: to_json_value(chunk.output),
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
case "tool-output-error":
|
|
612
|
+
case "tool-output-denied": {
|
|
613
|
+
const tool = this.find_tool(chunk.toolCallId);
|
|
614
|
+
await this.upsert_tool(chunk.toolCallId, {
|
|
615
|
+
tool_name: tool?.tool_name || "unknown",
|
|
616
|
+
state: "failed",
|
|
617
|
+
error:
|
|
618
|
+
chunk.type === "tool-output-error"
|
|
619
|
+
? chunk.errorText
|
|
620
|
+
: "Tool output denied",
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
case "file":
|
|
625
|
+
await this.upsert_part({
|
|
626
|
+
part_id: `file:${generateId()}`,
|
|
627
|
+
type: "file",
|
|
628
|
+
media_type: chunk.mediaType,
|
|
629
|
+
url: chunk.url,
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
default:
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** 写入一个完整 Assistant part。 */
|
|
638
|
+
async upsert_part(part: SessionAssistantMessagePart): Promise<void> {
|
|
639
|
+
await this.recorder.update_assistant_part(this.message_id, part);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** 当前实现逐 chunk 等待落盘,因此 flush 在返回时天然完成。 */
|
|
643
|
+
async flush(): Promise<void> {}
|
|
644
|
+
|
|
645
|
+
/** 正常完成当前 assistant segment。 */
|
|
646
|
+
async complete(): Promise<void> {
|
|
647
|
+
await this.close("completed");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** 停止当前 assistant segment,并保留已有 parts。 */
|
|
651
|
+
async stop(): Promise<void> {
|
|
652
|
+
await this.close("stopped");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** 以失败状态关闭当前 assistant segment。 */
|
|
656
|
+
async fail(_error: unknown): Promise<void> {
|
|
657
|
+
await this.close("failed");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private current_message(): SessionAssistantMessage {
|
|
661
|
+
const message = this.recorder.get_message(this.message_id);
|
|
662
|
+
if (!message || message.type !== "assistant") {
|
|
663
|
+
throw new Error(`Assistant Message not found: ${this.message_id}`);
|
|
664
|
+
}
|
|
665
|
+
return message;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private find_tool(tool_call_id: string): SessionAssistantToolPart | undefined {
|
|
669
|
+
return this.current_message().parts.find(
|
|
670
|
+
(part): part is SessionAssistantToolPart =>
|
|
671
|
+
part.type === "tool" && part.tool_call_id === tool_call_id,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private async upsert_tool(
|
|
676
|
+
tool_call_id: string,
|
|
677
|
+
changes: Pick<SessionAssistantToolPart, "tool_name" | "state"> &
|
|
678
|
+
Partial<Omit<SessionAssistantToolPart, "part_id" | "type" | "tool_call_id" | "tool_name" | "state">>,
|
|
679
|
+
): Promise<void> {
|
|
680
|
+
const current = this.find_tool(tool_call_id);
|
|
681
|
+
await this.upsert_part({
|
|
682
|
+
...(current || {}),
|
|
683
|
+
part_id: `tool:${tool_call_id}`,
|
|
684
|
+
type: "tool",
|
|
685
|
+
tool_call_id,
|
|
686
|
+
...changes,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private async close(status: "completed" | "stopped" | "failed"): Promise<void> {
|
|
691
|
+
if (this.closed) return;
|
|
692
|
+
await this.recorder.complete_assistant_message(this.message_id, status);
|
|
693
|
+
this.closed = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** 单个 Action Message 的生命周期 writer。 */
|
|
698
|
+
export class SessionActionMessageWriter {
|
|
699
|
+
readonly message_id: string;
|
|
700
|
+
private readonly recorder: SessionRecorder;
|
|
701
|
+
private closed = false;
|
|
702
|
+
|
|
703
|
+
constructor(recorder: SessionRecorder, message_id: string) {
|
|
704
|
+
this.recorder = recorder;
|
|
705
|
+
this.message_id = message_id;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** 把 Action 更新为 completed。 */
|
|
709
|
+
async complete(input?: { title?: string; description?: string; data?: JsonObject }): Promise<void> {
|
|
710
|
+
if (this.closed) return;
|
|
711
|
+
await this.recorder.update_action_message(this.message_id, "completed", input);
|
|
712
|
+
this.closed = true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** 把 Action 更新为 failed。 */
|
|
716
|
+
async fail(error: unknown): Promise<void> {
|
|
717
|
+
if (this.closed) return;
|
|
718
|
+
await this.recorder.update_action_message(this.message_id, "failed", {
|
|
719
|
+
description: error instanceof Error ? error.message : String(error),
|
|
720
|
+
});
|
|
721
|
+
this.closed = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** 把 AI SDK User parts 归一为 canonical User parts。 */
|
|
726
|
+
export function normalize_session_user_parts(
|
|
727
|
+
parts: UIMessage["parts"] | null | undefined,
|
|
728
|
+
): SessionUserMessagePart[] {
|
|
729
|
+
if (!Array.isArray(parts)) return [];
|
|
730
|
+
return parts.flatMap<SessionUserMessagePart>((part, index) => {
|
|
731
|
+
if (!part || typeof part !== "object") return [];
|
|
732
|
+
const candidate = part as Record<string, unknown>;
|
|
733
|
+
if (candidate.type === "text") {
|
|
734
|
+
return [{
|
|
735
|
+
part_id: `user-text:${index + 1}`,
|
|
736
|
+
type: "text",
|
|
737
|
+
text: String(candidate.text || ""),
|
|
738
|
+
state: "done",
|
|
739
|
+
}];
|
|
740
|
+
}
|
|
741
|
+
if (candidate.type === "file") {
|
|
742
|
+
return [{
|
|
743
|
+
part_id: `user-file:${index + 1}`,
|
|
744
|
+
type: "file",
|
|
745
|
+
url: String(candidate.url || ""),
|
|
746
|
+
media_type: String(candidate.mediaType || "application/octet-stream"),
|
|
747
|
+
...(candidate.filename ? { filename: String(candidate.filename) } : {}),
|
|
748
|
+
}];
|
|
749
|
+
}
|
|
750
|
+
if (String(candidate.type || "").startsWith("data-")) {
|
|
751
|
+
return [{
|
|
752
|
+
part_id: `user-data:${index + 1}`,
|
|
753
|
+
type: "data",
|
|
754
|
+
data_type: String(candidate.type),
|
|
755
|
+
data: to_json_value(candidate.data),
|
|
756
|
+
}];
|
|
757
|
+
}
|
|
758
|
+
return [];
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function require_message<TType extends SessionMessage["type"]>(
|
|
763
|
+
messages: SessionMessage[],
|
|
764
|
+
message_id: string,
|
|
765
|
+
type: TType,
|
|
766
|
+
): Extract<SessionMessage, { type: TType }> {
|
|
767
|
+
const message = messages.find((item) => item.message_id === message_id);
|
|
768
|
+
if (!message || message.type !== type) {
|
|
769
|
+
throw new Error(`Session ${type} Message not found: ${message_id}`);
|
|
770
|
+
}
|
|
771
|
+
return message as Extract<SessionMessage, { type: TType }>;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function to_json_value(input: unknown): JsonValue {
|
|
775
|
+
if (input === undefined || input === null) return null;
|
|
776
|
+
if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") return input;
|
|
777
|
+
try {
|
|
778
|
+
return JSON.parse(JSON.stringify(input)) as JsonValue;
|
|
779
|
+
} catch {
|
|
780
|
+
return String(input);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function normalize_limit(value: number | undefined, fallback: number, maximum: number): number {
|
|
785
|
+
if (!Number.isFinite(value)) return fallback;
|
|
786
|
+
return Math.min(maximum, Math.max(1, Math.floor(Number(value))));
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function encode_cursor(offset: number): string {
|
|
790
|
+
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function decode_cursor(cursor: string | undefined): number {
|
|
794
|
+
const value = String(cursor || "").trim();
|
|
795
|
+
if (!value) return 0;
|
|
796
|
+
try {
|
|
797
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as { offset?: unknown };
|
|
798
|
+
return typeof parsed.offset === "number" && parsed.offset >= 0
|
|
799
|
+
? Math.floor(parsed.offset)
|
|
800
|
+
: 0;
|
|
801
|
+
} catch {
|
|
802
|
+
throw new Error("Invalid Session Message cursor");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function resolve_import_id(map: Map<string, string>, source_id: string, prefix: string): string {
|
|
807
|
+
const existing = map.get(source_id);
|
|
808
|
+
if (existing) return existing;
|
|
809
|
+
const created = `${prefix}:${generateId()}`;
|
|
810
|
+
map.set(source_id, created);
|
|
811
|
+
return created;
|
|
812
|
+
}
|