@morlay/ui-conversation-message-actions 0.0.12 → 0.0.13

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.
@@ -0,0 +1,86 @@
1
+ import { BranchTimeline, CascadePolicy, SessionBranchVersionEvent } from "@morlay/session-branch";
2
+ import { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
3
+ import { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
4
+ //#region src/types.d.ts
5
+ interface EditOperation {
6
+ action: "edit";
7
+ sessionId: SessionId;
8
+ eventSeq: number;
9
+ blockIndex: number;
10
+ text: string;
11
+ cascade: CascadePolicy;
12
+ }
13
+ interface RerollOperation {
14
+ action: "reroll";
15
+ sessionId: SessionId;
16
+ }
17
+ interface RetryOperation {
18
+ action: "retry";
19
+ sessionId: SessionId;
20
+ turn: number;
21
+ cascade: CascadePolicy;
22
+ }
23
+ interface RewindOperation {
24
+ action: "rewind";
25
+ sessionId: SessionId;
26
+ toBoundary: number;
27
+ }
28
+ interface ForkOperation {
29
+ action: "fork";
30
+ sessionId: SessionId;
31
+ atSeq?: number;
32
+ childSessionId?: SessionId;
33
+ }
34
+ type SessionEditorOperation = EditOperation | RerollOperation | RetryOperation | RewindOperation | ForkOperation;
35
+ interface SessionEditorResult {
36
+ sessionId: SessionId;
37
+ queuedTurns: number;
38
+ live?: boolean;
39
+ }
40
+ interface EditableMessageBlock {
41
+ key: string;
42
+ turn: number;
43
+ eventSeq: number;
44
+ blockIndex: number;
45
+ kind: "user" | "assistant.reasoning" | "assistant.response";
46
+ text: string;
47
+ time: number;
48
+ }
49
+ interface RetryableTurn {
50
+ turn: number;
51
+ userEventSeq: number;
52
+ preview: string;
53
+ time: number;
54
+ }
55
+ //#endregion
56
+ //#region src/plan.d.ts
57
+ interface ClosedTurn {
58
+ turn: number;
59
+ startSeq: number;
60
+ endSeq?: number;
61
+ closed: boolean;
62
+ user?: SessionEvent<"user/message">;
63
+ /** 轮内全部 user/message(agent 运行中 followup 追加的输入也计入)。 */
64
+ users: SessionEvent<"user/message">[];
65
+ assistants: SessionEvent<"assistant/message">[];
66
+ }
67
+ interface OperationPlan {
68
+ anchorSeq: number;
69
+ rewindBoundary?: number;
70
+ version: SessionBranchVersionEvent;
71
+ manualTurn?: {
72
+ turn: number;
73
+ user: UserMessage;
74
+ assistant: AssistantMessage;
75
+ };
76
+ queuedUsers: UserMessage[];
77
+ }
78
+ declare function closedTurns(events: readonly SessionEvent[]): ClosedTurn[];
79
+ declare function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[];
80
+ declare function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[];
81
+ declare function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[];
82
+ declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan;
83
+ declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
84
+ declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
85
+ //#endregion
86
+ export { SessionEditorResult as _, editPlan as a, retryPlan as c, EditableMessageBlock as d, RerollOperation as f, SessionEditorOperation as g, RewindOperation as h, downstreamUsers as i, retryableTurns as l, RetryableTurn as m, OperationPlan as n, editableMessages as o, RetryOperation as p, closedTurns as r, rerollPlan as s, ClosedTurn as t, EditOperation as u };
@@ -0,0 +1,2 @@
1
+ import { a as editPlan, c as retryPlan, i as downstreamUsers, l as retryableTurns, n as OperationPlan, o as editableMessages, r as closedTurns, s as rerollPlan, t as ClosedTurn } from "./plan-Bw4zoI4N.mjs";
2
+ export { ClosedTurn, OperationPlan, closedTurns, downstreamUsers, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns };
package/dist/plan.mjs ADDED
@@ -0,0 +1,225 @@
1
+ import { SESSION_BRANCH_VERSION_SCHEMA, SessionBranchError } from "@morlay/session-branch";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/plan.ts
4
+ function isTextualBlock(block) {
5
+ return block?.type === "text" || block?.type === "reasoning";
6
+ }
7
+ function userText(message) {
8
+ return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
9
+ }
10
+ function cloneUser(message, content = structuredClone(message.content)) {
11
+ return Object.freeze({
12
+ id: randomUUID(),
13
+ role: "user",
14
+ content: Object.freeze(content),
15
+ source: Object.freeze({ kind: "user" })
16
+ });
17
+ }
18
+ function replaceTextBlock(content, blockIndex, text) {
19
+ const block = content[blockIndex];
20
+ if (!isTextualBlock(block)) throw new SessionBranchError("所选内容块不是可编辑文本。", "INVALID_BOUNDARY");
21
+ return content.map((candidate, index) => index === blockIndex ? {
22
+ ...candidate,
23
+ text
24
+ } : structuredClone(candidate));
25
+ }
26
+ function closedTurns(events) {
27
+ const result = [];
28
+ let current;
29
+ for (const event of events) {
30
+ if (event.type === "turn/start") {
31
+ current = {
32
+ turn: event.data.turn,
33
+ startSeq: event.seq,
34
+ users: [],
35
+ assistants: []
36
+ };
37
+ continue;
38
+ }
39
+ if (current === void 0) continue;
40
+ if (event.type === "user/message" && event.data.source.kind === "user") {
41
+ if (current.user === void 0) current.user = event;
42
+ current.users.push(event);
43
+ continue;
44
+ }
45
+ if (event.type === "assistant/message" && event.data.turn === current.turn) {
46
+ current.assistants.push(event);
47
+ continue;
48
+ }
49
+ if (event.type === "turn/end" && event.data.turn === current.turn) {
50
+ result.push({
51
+ ...current,
52
+ endSeq: event.seq,
53
+ closed: true
54
+ });
55
+ current = void 0;
56
+ }
57
+ }
58
+ if (current !== void 0) result.push({
59
+ ...current,
60
+ closed: false
61
+ });
62
+ return result;
63
+ }
64
+ function editableMessages(turns) {
65
+ const result = [];
66
+ for (const turn of turns) {
67
+ for (const user of turn.users) for (const [blockIndex, block] of user.data.content.entries()) {
68
+ if (block.type !== "text") continue;
69
+ result.push({
70
+ key: `${String(user.seq)}:${String(blockIndex)}`,
71
+ turn: turn.turn,
72
+ eventSeq: user.seq,
73
+ blockIndex,
74
+ kind: "user",
75
+ text: block.text,
76
+ time: user.time
77
+ });
78
+ }
79
+ if (!turn.closed) continue;
80
+ for (const event of turn.assistants) for (const [blockIndex, block] of event.data.message.content.entries()) {
81
+ if (!isTextualBlock(block)) continue;
82
+ result.push({
83
+ key: `${String(event.seq)}:${String(blockIndex)}`,
84
+ turn: turn.turn,
85
+ eventSeq: event.seq,
86
+ blockIndex,
87
+ kind: block.type === "reasoning" ? "assistant.reasoning" : "assistant.response",
88
+ text: block.text,
89
+ time: event.time
90
+ });
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ function retryableTurns(turns) {
96
+ return turns.flatMap((turn) => turn.user === void 0 || !turn.closed ? [] : [{
97
+ turn: turn.turn,
98
+ userEventSeq: turn.user.seq,
99
+ preview: userText(turn.user.data),
100
+ time: turn.user.time
101
+ }]);
102
+ }
103
+ function downstreamUsers(turns, start) {
104
+ return turns.slice(start).flatMap((turn) => turn.users.map((user) => cloneUser(user.data)));
105
+ }
106
+ function assistantReplacement(event, blockIndex, text) {
107
+ const replaced = replaceTextBlock(event.data.message.content, blockIndex, text).filter((block) => block.type === "text" || block.type === "reasoning");
108
+ return Object.freeze({
109
+ id: randomUUID(),
110
+ role: "assistant",
111
+ content: Object.freeze(replaced),
112
+ source: Object.freeze({
113
+ kind: "model",
114
+ provider: event.data.message.source.provider,
115
+ model: event.data.message.source.model
116
+ })
117
+ });
118
+ }
119
+ function pairVersionEffect(sourceSessionId, effect) {
120
+ return {
121
+ schemaVersion: SESSION_BRANCH_VERSION_SCHEMA,
122
+ effect: {
123
+ ...effect,
124
+ id: randomUUID()
125
+ },
126
+ inverse: {
127
+ kind: "restore-version",
128
+ sessionId: sourceSessionId
129
+ }
130
+ };
131
+ }
132
+ function editPlan(operation, turns) {
133
+ const turnIndex = turns.findIndex((turn) => operation.eventSeq > turn.startSeq && (turn.endSeq === void 0 || operation.eventSeq < turn.endSeq));
134
+ const turn = turns[turnIndex];
135
+ if (turn === void 0) throw new SessionBranchError("所选消息不属于已落定回合。", "INVALID_BOUNDARY");
136
+ const event = turn.users.find((candidate) => candidate.seq === operation.eventSeq) ?? turn.assistants.find((candidate) => candidate.seq === operation.eventSeq);
137
+ if (event === void 0) throw new SessionBranchError("所选消息不存在或不可编辑。", "INVALID_BOUNDARY");
138
+ if (event.type === "user/message") {
139
+ const before = event.data.content[operation.blockIndex];
140
+ if (before?.type !== "text") throw new SessionBranchError("所选用户消息块不是文本。", "INVALID_BOUNDARY");
141
+ const edited = cloneUser(event.data, replaceTextBlock(event.data.content, operation.blockIndex, operation.text));
142
+ const userIndex = turn.users.findIndex((candidate) => candidate.seq === event.seq);
143
+ const sameTurnFollowups = turn.users.slice(userIndex + 1).map((user) => cloneUser(user.data));
144
+ const later = operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : [];
145
+ return {
146
+ anchorSeq: turn.startSeq,
147
+ ...turn.closed && userIndex === 0 ? {} : { rewindBoundary: event.seq },
148
+ version: pairVersionEffect(operation.sessionId, {
149
+ operation: "edit",
150
+ cascade: operation.cascade,
151
+ targetTurn: turn.turn,
152
+ targetEventSeq: event.seq,
153
+ targetBlockIndex: operation.blockIndex,
154
+ blockKind: "user",
155
+ before: before.text,
156
+ after: operation.text
157
+ }),
158
+ queuedUsers: [
159
+ edited,
160
+ ...sameTurnFollowups,
161
+ ...later
162
+ ]
163
+ };
164
+ }
165
+ if (!turn.closed) throw new SessionBranchError("未闭合轮次的助手消息不可编辑。", "INVALID_BOUNDARY");
166
+ const before = event.data.message.content[operation.blockIndex];
167
+ if (!isTextualBlock(before)) throw new SessionBranchError("所选助手消息块不是文本或思考。", "INVALID_BOUNDARY");
168
+ const blockKind = before.type === "reasoning" ? "assistant.reasoning" : "assistant.response";
169
+ if (turn.user === void 0) throw new SessionBranchError("所选助手消息没有可重建的用户输入。", "INVALID_BOUNDARY");
170
+ return {
171
+ anchorSeq: turn.startSeq,
172
+ version: pairVersionEffect(operation.sessionId, {
173
+ operation: "edit",
174
+ cascade: operation.cascade,
175
+ targetTurn: turn.turn,
176
+ targetEventSeq: event.seq,
177
+ targetBlockIndex: operation.blockIndex,
178
+ blockKind,
179
+ before: before.text,
180
+ after: operation.text
181
+ }),
182
+ manualTurn: {
183
+ turn: turn.turn,
184
+ user: cloneUser(turn.user.data),
185
+ assistant: assistantReplacement(event, operation.blockIndex, operation.text)
186
+ },
187
+ queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex + 1) : []
188
+ };
189
+ }
190
+ function retryPlan(operation, turns) {
191
+ const turnIndex = turns.findIndex((turn) => turn.turn === operation.turn);
192
+ const turn = turns[turnIndex];
193
+ if (turn?.user === void 0) throw new SessionBranchError("所选回合没有可重放的用户输入。", "INVALID_BOUNDARY");
194
+ return {
195
+ anchorSeq: turn.startSeq,
196
+ version: pairVersionEffect(operation.sessionId, {
197
+ operation: "retry",
198
+ cascade: operation.cascade,
199
+ targetTurn: turn.turn,
200
+ targetEventSeq: turn.user.seq
201
+ }),
202
+ queuedUsers: operation.cascade === "preserve" ? downstreamUsers(turns, turnIndex) : turn.users.map((user) => cloneUser(user.data))
203
+ };
204
+ }
205
+ function rerollPlan(operation, turns) {
206
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
207
+ const turn = turns[index];
208
+ if (turn?.user === void 0 || !turn.closed) continue;
209
+ const target = turn.assistants.findLast((event) => event.data.message.content.some(isTextualBlock));
210
+ if (target === void 0) continue;
211
+ return {
212
+ anchorSeq: turn.startSeq,
213
+ version: pairVersionEffect(operation.sessionId, {
214
+ operation: "reroll",
215
+ cascade: "truncate",
216
+ targetTurn: turn.turn,
217
+ targetEventSeq: target.seq
218
+ }),
219
+ queuedUsers: turn.users.map((user) => cloneUser(user.data))
220
+ };
221
+ }
222
+ throw new SessionBranchError("当前会话没有可重生成的已落定助手回复。", "INVALID_BOUNDARY");
223
+ }
224
+ //#endregion
225
+ export { closedTurns, downstreamUsers, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns };