@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,408 @@
1
+ import { closedTurns, editPlan, editableMessages, rerollPlan, retryPlan, retryableTurns } from "./plan.mjs";
2
+ import { Service } from "@deepseek-ai/cordis";
3
+ import { SESSION_BRANCH_VERSION_SCHEMA as SESSION_BRANCH_VERSION_SCHEMA$1, SessionBranchError, balanceRewindPrefix } from "@morlay/session-branch";
4
+ //#region src/shared.ts
5
+ const SESSION_EDITOR_PATH = "/session-editor";
6
+ function toTimelinePayload(sessionId, timeline, messages, retryableTurns) {
7
+ const currentPath = /* @__PURE__ */ new Set();
8
+ for (const node of timeline.nodes) currentPath.add(String(node.sessionId));
9
+ const versions = timeline.nodes.map((node) => ({
10
+ sessionId: String(node.sessionId),
11
+ ...node.parentSessionId === void 0 ? {} : { parentSessionId: String(node.parentSessionId) },
12
+ ...node.effect === void 0 ? {} : {
13
+ effectId: node.effect.id,
14
+ inverseSessionId: String(node.inverseSessionId),
15
+ operation: node.effect.operation,
16
+ cascade: node.effect.cascade,
17
+ targetTurn: node.effect.targetTurn,
18
+ ...node.effect.blockKind === void 0 ? {} : { blockKind: node.effect.blockKind },
19
+ ...node.effect.before === void 0 ? {} : { before: node.effect.before },
20
+ ...node.effect.after === void 0 ? {} : { after: node.effect.after }
21
+ },
22
+ createdAt: node.createdAt,
23
+ depth: depthOf(timeline, node.sessionId),
24
+ current: String(node.sessionId) === String(sessionId),
25
+ onCurrentEffectPath: currentPath.has(String(node.sessionId))
26
+ }));
27
+ const versionsById = new Map(versions.map((version) => [version.sessionId, version]));
28
+ const undoStack = [];
29
+ let cursor = versionsById.get(String(sessionId));
30
+ while (cursor?.inverseSessionId !== void 0) {
31
+ if (undoStack.includes(cursor.inverseSessionId)) break;
32
+ undoStack.push(cursor.inverseSessionId);
33
+ cursor = versionsById.get(cursor.inverseSessionId);
34
+ }
35
+ const redoSessionIds = versions.filter((version) => version.inverseSessionId === String(sessionId)).map((version) => version.sessionId);
36
+ return {
37
+ sessionId: String(sessionId),
38
+ messages,
39
+ retryableTurns,
40
+ versions,
41
+ undoStack,
42
+ redoSessionIds
43
+ };
44
+ }
45
+ function depthOf(timeline, sessionId) {
46
+ const byId = new Map(timeline.nodes.map((node) => [String(node.sessionId), node]));
47
+ let depth = 0;
48
+ let cursor = byId.get(String(sessionId));
49
+ const seen = /* @__PURE__ */ new Set();
50
+ while (cursor?.parentSessionId !== void 0 && !seen.has(String(cursor.sessionId))) {
51
+ seen.add(String(cursor.sessionId));
52
+ depth += 1;
53
+ cursor = byId.get(String(cursor.parentSessionId));
54
+ }
55
+ return depth;
56
+ }
57
+ //#endregion
58
+ //#region src/index.ts
59
+ function appendLogSeedEvent(events, type, data, ignorable = false) {
60
+ events.push({
61
+ type,
62
+ seq: events.length,
63
+ time: Date.now(),
64
+ data,
65
+ ...ignorable ? { ignorable: true } : {}
66
+ });
67
+ }
68
+ function appendSurfaceSeedEvent(events, type, data, intent) {
69
+ events.push({
70
+ type,
71
+ seq: events.length,
72
+ time: Date.now(),
73
+ data,
74
+ surfaceOp: intent.surfaceOp,
75
+ ...intent.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: intent.sourceEventSeqs }
76
+ });
77
+ }
78
+ function appendManualTurn(events, manual) {
79
+ const { turn, user, assistant } = manual;
80
+ appendLogSeedEvent(events, "turn/start", { turn });
81
+ appendSurfaceSeedEvent(events, "user/message", user, { surfaceOp: "append" });
82
+ appendLogSeedEvent(events, "step/start", {
83
+ turn,
84
+ step: 1
85
+ });
86
+ appendSurfaceSeedEvent(events, "assistant/message", {
87
+ turn,
88
+ step: 1,
89
+ message: assistant
90
+ }, {
91
+ surfaceOp: "append",
92
+ sourceEventSeqs: []
93
+ });
94
+ appendLogSeedEvent(events, "step/end", {
95
+ turn,
96
+ step: 1
97
+ });
98
+ appendLogSeedEvent(events, "turn/end", {
99
+ turn,
100
+ reason: { kind: "completed" }
101
+ });
102
+ }
103
+ function appendSeedSuffixLive(session, seedSuffix) {
104
+ for (const event of seedSuffix) {
105
+ if (event.ignorable === true) {
106
+ const s = session;
107
+ s.log.push({
108
+ ...event,
109
+ seq: s.log.length
110
+ });
111
+ s.eventsSnapshot = void 0;
112
+ continue;
113
+ }
114
+ const s = session;
115
+ const raw = event;
116
+ if (raw.surfaceOp !== void 0) s.append(event.type, event.data, {
117
+ surfaceOp: raw.surfaceOp,
118
+ ...raw.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: raw.sourceEventSeqs }
119
+ });
120
+ else s.append(event.type, event.data);
121
+ }
122
+ }
123
+ var SessionEditor = class extends Service {
124
+ static inject = [
125
+ "sessionBranch",
126
+ "sessionPersistence",
127
+ "sessions"
128
+ ];
129
+ constructor(ctx) {
130
+ super(ctx, "sessionEditor");
131
+ registerHttpRoutes(ctx);
132
+ }
133
+ readBranchPrefix(id, atSeq, mode, signal) {
134
+ return this.ctx.sessionBranch.readBranchPrefix(id, atSeq, mode, signal);
135
+ }
136
+ fork(sourceId, atSeq, childSessionId, meta, signal) {
137
+ return this.ctx.sessionBranch.forkFrom(sourceId, {
138
+ ...atSeq === void 0 ? {} : { atSeq },
139
+ ...childSessionId === void 0 ? {} : { childSessionId },
140
+ ...meta === void 0 ? {} : { meta }
141
+ }, signal);
142
+ }
143
+ rewind(id, toBoundary, signal) {
144
+ return this.ctx.sessionBranch.rewind(id, toBoundary, signal);
145
+ }
146
+ timeline(sessionId, signal) {
147
+ return this.ctx.sessionBranch.timeline(sessionId, signal);
148
+ }
149
+ edit(operation, signal) {
150
+ return this.branchOperation(operation, signal);
151
+ }
152
+ reroll(operation, signal) {
153
+ return this.branchOperation(operation, signal);
154
+ }
155
+ retry(operation, signal) {
156
+ return this.branchOperation(operation, signal);
157
+ }
158
+ async editableMessages(sessionId, signal) {
159
+ const events = await this.readEvents(sessionId, signal);
160
+ return editableMessages(closedTurns(events));
161
+ }
162
+ async retryableTurns(sessionId, signal) {
163
+ const events = await this.readEvents(sessionId, signal);
164
+ return retryableTurns(closedTurns(events));
165
+ }
166
+ async branchOperation(operation, signal) {
167
+ signal?.throwIfAborted();
168
+ const events = await this.readEvents(operation.sessionId, signal);
169
+ const turns = closedTurns(events);
170
+ const plan = operation.action === "edit" ? editPlan(operation, turns) : operation.action === "retry" ? retryPlan(operation, turns) : rerollPlan(operation, turns);
171
+ const headerConfig = events.findLast((event) => event.type === "request/header")?.data.header.config;
172
+ const seedSuffix = [];
173
+ appendLogSeedEvent(seedSuffix, "session-branch/version", plan.version, true);
174
+ if (plan.manualTurn !== void 0) appendManualTurn(seedSuffix, plan.manualTurn);
175
+ const replay = await this.prepareReplay(operation.sessionId, plan.queuedUsers, signal, headerConfig);
176
+ const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
177
+ const boundary = plan.rewindBoundary !== void 0 ? plan.rewindBoundary : turnIndex <= 0 ? -1 : turns[turnIndex - 1].endSeq;
178
+ const live = this.ctx.sessions.get(operation.sessionId);
179
+ await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
180
+ if (seedSuffix.length > 0) {
181
+ if (live !== void 0) {
182
+ appendSeedSuffixLive(live, seedSuffix);
183
+ this.ctx.sessionBranch.syncLiveCursor(operation.sessionId);
184
+ await this.ctx.sessions.flush(live);
185
+ } else {
186
+ const rawKeepLength = boundary + (plan.rewindBoundary === void 0 ? 1 : 0);
187
+ const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;
188
+ const renumbered = seedSuffix.map((event, index) => ({
189
+ ...event,
190
+ seq: keepLength + index
191
+ }));
192
+ await this.ctx.sessionPersistence.append(operation.sessionId, renumbered);
193
+ }
194
+ }
195
+ let queuedTurns = 0;
196
+ if (replay.agent !== void 0 && plan.queuedUsers.length > 0) {
197
+ for (const message of plan.queuedUsers) replay.agent.followup(message);
198
+ await this.ctx.sessions.flush(replay.agent.session);
199
+ queuedTurns = plan.queuedUsers.length;
200
+ }
201
+ return {
202
+ sessionId: operation.sessionId,
203
+ queuedTurns,
204
+ live: this.ctx.sessions.get(operation.sessionId) !== void 0
205
+ };
206
+ }
207
+ async readEvents(sessionId, signal) {
208
+ const live = this.ctx.sessions.get(sessionId);
209
+ if (live !== void 0) return live.snapshotEvents();
210
+ return (await this.ctx.sessionBranch.readRawEvents(sessionId, signal)).events;
211
+ }
212
+ /**
213
+ * rewind 前确保 agent 可驱动重放:live agent 先等待其停下(rewind 会截断其
214
+ * session 内存 log,须在 quiescence 后执行);cold 会话先 resume 出驻留
215
+ * agent(此时会话完整,resume 的 prepare 不与截断冲突)。agents 服务缺失或
216
+ * 无需重放时返回空——调用方退化为就地截断版本。
217
+ */
218
+ async prepareReplay(sessionId, queuedUsers, signal, headerConfig) {
219
+ if (queuedUsers.length === 0) return { agent: void 0 };
220
+ signal?.throwIfAborted();
221
+ const agents = this.ctx.get("agents");
222
+ if (agents === void 0) return { agent: void 0 };
223
+ const existing = agents.get(sessionId);
224
+ if (existing !== void 0) {
225
+ await existing.whenIdle();
226
+ signal?.throwIfAborted();
227
+ if (existing.inboxPending) existing.clearInbox();
228
+ return { agent: existing };
229
+ }
230
+ const provider = headerConfig?.provider ?? "";
231
+ const model = headerConfig?.model ?? "";
232
+ if (provider.length === 0 || model.length === 0) {
233
+ const config = (await this.readEvents(sessionId, signal)).findLast((event) => event.type === "request/header")?.data.header.config;
234
+ const fallbackProvider = config?.provider ?? "";
235
+ const fallbackModel = config?.model ?? "";
236
+ if (fallbackProvider.length === 0 || fallbackModel.length === 0) throw new SessionBranchError("无法重放:会话没有可解析的模型配置。", "INVALID_BOUNDARY");
237
+ const handle = await agents.resume({
238
+ resumeSessionId: sessionId,
239
+ agentOptions: {
240
+ provider: fallbackProvider,
241
+ model: fallbackModel
242
+ }
243
+ });
244
+ signal?.throwIfAborted();
245
+ return { agent: handle.agent };
246
+ }
247
+ const handle = await agents.resume({
248
+ resumeSessionId: sessionId,
249
+ agentOptions: {
250
+ provider,
251
+ model
252
+ }
253
+ });
254
+ signal?.throwIfAborted();
255
+ return { agent: handle.agent };
256
+ }
257
+ };
258
+ function objectValue(value) {
259
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("请求体必须是 JSON 对象。");
260
+ return value;
261
+ }
262
+ function sessionIdOf(value) {
263
+ if (typeof value !== "string" || value.length === 0) throw new TypeError("sessionId 必须是非空字符串。");
264
+ return value;
265
+ }
266
+ function integerOf(value, name) {
267
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} 必须是非负安全整数。`);
268
+ return value;
269
+ }
270
+ function cascadeOf(value) {
271
+ if (value !== "truncate" && value !== "preserve") throw new TypeError("cascade 必须是 truncate 或 preserve。");
272
+ return value;
273
+ }
274
+ function decodeOperation(value) {
275
+ const record = objectValue(value);
276
+ const sessionId = sessionIdOf(record["sessionId"]);
277
+ switch (record["action"]) {
278
+ case "edit":
279
+ if (typeof record["text"] !== "string") throw new TypeError("text 必须是字符串。");
280
+ return {
281
+ action: "edit",
282
+ sessionId,
283
+ eventSeq: integerOf(record["eventSeq"], "eventSeq"),
284
+ blockIndex: integerOf(record["blockIndex"], "blockIndex"),
285
+ text: record["text"],
286
+ cascade: cascadeOf(record["cascade"])
287
+ };
288
+ case "reroll": return {
289
+ action: "reroll",
290
+ sessionId
291
+ };
292
+ case "retry": return {
293
+ action: "retry",
294
+ sessionId,
295
+ turn: integerOf(record["turn"], "turn"),
296
+ cascade: cascadeOf(record["cascade"])
297
+ };
298
+ case "rewind": return {
299
+ action: "rewind",
300
+ sessionId,
301
+ toBoundary: integerOf(record["toBoundary"], "toBoundary")
302
+ };
303
+ case "fork": return {
304
+ action: "fork",
305
+ sessionId,
306
+ ...record["atSeq"] === void 0 ? {} : { atSeq: integerOf(record["atSeq"], "atSeq") },
307
+ ...record["childSessionId"] === void 0 ? {} : { childSessionId: sessionIdOf(record["childSessionId"]) }
308
+ };
309
+ default: throw new TypeError("action 必须是 edit、reroll、retry、rewind 或 fork。");
310
+ }
311
+ }
312
+ function requestJson(request) {
313
+ return new Promise((resolve, reject) => {
314
+ const decoder = new TextDecoder();
315
+ let text = "";
316
+ request.on("data", (chunk) => {
317
+ text += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
318
+ });
319
+ request.on("end", () => {
320
+ try {
321
+ text += decoder.decode();
322
+ resolve(JSON.parse(text));
323
+ } catch (error) {
324
+ reject(error);
325
+ }
326
+ });
327
+ request.on("error", reject);
328
+ });
329
+ }
330
+ function respondJson(response, status, value) {
331
+ response.writeHead(status, {
332
+ "content-type": "application/json; charset=utf-8",
333
+ "cache-control": "no-store"
334
+ });
335
+ response.end(JSON.stringify(value));
336
+ }
337
+ async function readTimeline(editor, sessionId) {
338
+ return toTimelinePayload(sessionId, await editor.timeline(sessionId), await editor.editableMessages(sessionId), await editor.retryableTurns(sessionId));
339
+ }
340
+ async function runOperation(editor, operation) {
341
+ switch (operation.action) {
342
+ case "edit": {
343
+ const result = await editor.edit(operation);
344
+ return {
345
+ sessionId: result.sessionId,
346
+ queuedTurns: result.queuedTurns,
347
+ ...result.live === void 0 ? {} : { live: result.live }
348
+ };
349
+ }
350
+ case "reroll": {
351
+ const result = await editor.reroll(operation);
352
+ return {
353
+ sessionId: result.sessionId,
354
+ queuedTurns: result.queuedTurns,
355
+ ...result.live === void 0 ? {} : { live: result.live }
356
+ };
357
+ }
358
+ case "retry": {
359
+ const result = await editor.retry(operation);
360
+ return {
361
+ sessionId: result.sessionId,
362
+ queuedTurns: result.queuedTurns,
363
+ ...result.live === void 0 ? {} : { live: result.live }
364
+ };
365
+ }
366
+ case "rewind":
367
+ await editor.rewind(operation.sessionId, operation.toBoundary);
368
+ return {
369
+ sessionId: operation.sessionId,
370
+ queuedTurns: 0
371
+ };
372
+ case "fork": return {
373
+ sessionId: await editor.fork(operation.sessionId, operation.atSeq, operation.childSessionId),
374
+ queuedTurns: 0
375
+ };
376
+ }
377
+ }
378
+ async function handleRoute(editor, request, response) {
379
+ try {
380
+ if (request.method === "GET") {
381
+ respondJson(response, 200, await readTimeline(editor, sessionIdOf(new URL(request.url ?? "/session-editor", "http://session-editor.local").searchParams.get("sessionId"))));
382
+ return;
383
+ }
384
+ if (request.method === "POST") {
385
+ respondJson(response, 200, await runOperation(editor, decodeOperation(await requestJson(request))));
386
+ return;
387
+ }
388
+ response.writeHead(405);
389
+ response.end();
390
+ } catch (error) {
391
+ const message = error instanceof Error ? error.message : String(error);
392
+ respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
393
+ }
394
+ }
395
+ function registerHttpRoutes(ctx) {
396
+ const webServer = ctx.get("webServer");
397
+ if (webServer === void 0) return;
398
+ ctx.effect(() => {
399
+ const editor = ctx.sessionEditor;
400
+ return webServer.register({
401
+ kind: "exact",
402
+ path: SESSION_EDITOR_PATH,
403
+ handler: (request, response) => handleRoute(editor, request, response)
404
+ });
405
+ }, "session-editor: HTTP route");
406
+ }
407
+ //#endregion
408
+ export { SessionEditor as n, SESSION_BRANCH_VERSION_SCHEMA$1 as t };