@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.
- package/dist/client.js +23 -23
- package/dist/index.d.mts +21 -69
- package/dist/index.mjs +2 -606
- package/dist/invariant.d.mts +1 -2
- package/dist/invariant.mjs +0 -2
- package/dist/plan-Bw4zoI4N.d.mts +86 -0
- package/dist/plan.d.mts +2 -0
- package/dist/plan.mjs +225 -0
- package/dist/src-CXYW0Bc0.mjs +408 -0
- package/dist/testing.d.mts +707 -0
- package/dist/testing.mjs +23690 -0
- package/package.json +32 -12
- package/src/index.ts +79 -365
- package/src/plan.ts +322 -0
- package/src/testing.ts +151 -0
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/invariant.d.mts.map +0 -1
- package/dist/invariant.mjs.map +0 -1
|
@@ -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 };
|
package/dist/plan.d.mts
ADDED
|
@@ -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 };
|