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