@morlay/ui-conversation-message-actions 0.0.17 → 0.0.19-alpha.0
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/README.md +29 -98
- package/dist/client.cjs +1380 -0
- package/dist/client.d.cts +6 -0
- package/dist/client.d.mts +6 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.mts +3 -10
- package/dist/index.mjs +460 -2
- package/dist/invariant.d.cts +7 -0
- package/dist/{plan-BSa7k9HO.d.mts → plan-CT_6Y1fD.d.cts} +11 -9
- package/dist/plan-Cm9hCord.d.mts +101 -0
- package/dist/plan.d.cts +2 -0
- package/dist/plan.d.mts +2 -2
- package/dist/plan.mjs +145 -11
- package/dist/shared-LK3nImWW.mjs +55 -0
- package/dist/testing.d.cts +3242 -0
- package/dist/testing.d.mts +25 -19
- package/dist/testing.mjs +2140 -3939
- package/package.json +35 -32
- package/src/client/chat-node/MessageIconActions.styles.ts +76 -0
- package/src/client/chat-node/MessageIconActions.tsx +30 -11
- package/src/client/chat-node/MessageItem.styles.ts +247 -0
- package/src/client/chat-node/MessageItem.tsx +32 -39
- package/src/client/chat-node/message-chrome.ts +0 -2
- package/src/client/chat-node/register.ts +3 -4
- package/src/client/chat-node/use-calendar-day.ts +0 -4
- package/src/client/composer-stats/StatsPills.styles.ts +57 -0
- package/src/client/composer-stats/StatsPills.tsx +384 -0
- package/src/client/composer-stats/register.ts +17 -0
- package/src/client/composer-stats/stat-dialog.styles.ts +70 -0
- package/src/client/composer-stats/stat-dialog.ts +52 -0
- package/src/client/composer-stats/token-format.ts +85 -0
- package/src/client/composer-stats/turn-reading.ts +35 -0
- package/src/client/controller.ts +32 -114
- package/src/client/index.ts +8 -24
- package/src/index.ts +102 -111
- package/src/plan.ts +189 -24
- package/src/shared.ts +1 -1
- package/src/testing.ts +1 -5
- package/src/types.ts +1 -1
- package/dist/client.js +0 -806
- package/dist/src-B4HwXsob.mjs +0 -499
- package/src/client/chat-node/MessageIconActions.module.css +0 -86
- package/src/client/chat-node/MessageItem.module.css +0 -290
- package/src/client/css-modules.d.ts +0 -4
- package/src/client/import-action.tsx +0 -74
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { C as SessionEditorResult, S as SessionEditorOperation, _ as RecallOperation, b as RetryableTurn, c as editableMessages, g as EditableMessageBlock, h as EditOperation, i as closedTurns, m as retryableTurns, v as RerollOperation, x as RewindOperation, y as RetryOperation } from "./plan-CT_6Y1fD.cjs";
|
|
2
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { UserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { AgentCancelCause, Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { BranchBoundary, BranchTimeline, BranchTimeline as BranchTimeline$1, CascadePolicy, EditableBlockKind, SESSION_BRANCH_VERSION_SCHEMA, SessionBranchVersionEvent } from "@morlay/session-branch";
|
|
6
|
+
//#region src/index.d.ts
|
|
7
|
+
declare module "@deepseek-ai/cordis" {
|
|
8
|
+
interface Context {
|
|
9
|
+
sessionEditor: SessionEditor;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
interface EditorAgent {
|
|
13
|
+
readonly session: Session;
|
|
14
|
+
followup(message: UserMessage): void;
|
|
15
|
+
whenIdle(): Promise<void>;
|
|
16
|
+
cancel?(cause: AgentCancelCause, options?: {
|
|
17
|
+
keepInbox?: boolean;
|
|
18
|
+
}): void;
|
|
19
|
+
}
|
|
20
|
+
interface EditorAgentHandle {
|
|
21
|
+
readonly agent: EditorAgent;
|
|
22
|
+
dispose(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
interface EditorAgentRegistry {
|
|
25
|
+
get(sessionId: SessionId): EditorAgent | undefined;
|
|
26
|
+
create(options: {
|
|
27
|
+
sessionId?: SessionId;
|
|
28
|
+
seed?: readonly SessionEvent[];
|
|
29
|
+
meta?: {
|
|
30
|
+
cwd?: string;
|
|
31
|
+
parentSession?: SessionId;
|
|
32
|
+
seedLength?: number;
|
|
33
|
+
agentPreset?: string;
|
|
34
|
+
};
|
|
35
|
+
agentOptions?: {
|
|
36
|
+
provider: string;
|
|
37
|
+
model: string;
|
|
38
|
+
maxTokens?: number;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<EditorAgentHandle>;
|
|
41
|
+
resume(options: {
|
|
42
|
+
resumeSessionId: SessionId;
|
|
43
|
+
agentOptions?: {
|
|
44
|
+
provider: string;
|
|
45
|
+
model: string;
|
|
46
|
+
maxTokens?: number;
|
|
47
|
+
};
|
|
48
|
+
}): Promise<EditorAgentHandle>;
|
|
49
|
+
}
|
|
50
|
+
declare class SessionEditor extends Service {
|
|
51
|
+
static inject: string[];
|
|
52
|
+
constructor(ctx: Context);
|
|
53
|
+
readBranchPrefix(id: SessionId, atSeq?: number, mode?: "after" | "before", signal?: AbortSignal): Promise<BranchBoundary>;
|
|
54
|
+
fork(sourceId: SessionId, atSeq?: number, childSessionId?: SessionId, meta?: {
|
|
55
|
+
cwd?: string;
|
|
56
|
+
agentPreset?: string;
|
|
57
|
+
}, signal?: AbortSignal): Promise<SessionId>;
|
|
58
|
+
rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceSnapshot>;
|
|
59
|
+
timeline(sessionId: SessionId, signal?: AbortSignal): Promise<BranchTimeline$1>;
|
|
60
|
+
edit(operation: EditOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
61
|
+
reroll(operation: RerollOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
62
|
+
retry(operation: RetryOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
63
|
+
recall(operation: RecallOperation, signal?: AbortSignal): Promise<SessionEditorResult>;
|
|
64
|
+
editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
|
|
65
|
+
retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
|
|
66
|
+
private branchOperation;
|
|
67
|
+
private recallOperation;
|
|
68
|
+
private seedStart;
|
|
69
|
+
private appendSeedSuffix;
|
|
70
|
+
private stopLoop;
|
|
71
|
+
private readEvents;
|
|
72
|
+
private prepareReplay;
|
|
73
|
+
}
|
|
74
|
+
interface HttpRequestLike {
|
|
75
|
+
method?: string;
|
|
76
|
+
url?: string;
|
|
77
|
+
on(event: "data", listener: (chunk: Uint8Array | string) => void): this;
|
|
78
|
+
on(event: "end", listener: () => void): this;
|
|
79
|
+
on(event: "error", listener: (error: unknown) => void): this;
|
|
80
|
+
}
|
|
81
|
+
interface HttpResponseLike {
|
|
82
|
+
writeHead(status: number, headers?: Record<string, string>): unknown;
|
|
83
|
+
end(body?: string): void;
|
|
84
|
+
}
|
|
85
|
+
interface HttpServerLike {
|
|
86
|
+
register(route: {
|
|
87
|
+
kind: "exact";
|
|
88
|
+
path: string;
|
|
89
|
+
handler: (request: HttpRequestLike, response: HttpResponseLike) => void | Promise<void>;
|
|
90
|
+
}): () => void;
|
|
91
|
+
}
|
|
92
|
+
declare module "@deepseek-ai/cordis" {
|
|
93
|
+
interface Context {
|
|
94
|
+
webServer: HttpServerLike;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { type BranchTimeline, type CascadePolicy, type EditOperation, type EditableBlockKind, type EditableMessageBlock, EditorAgent, EditorAgentHandle, EditorAgentRegistry, type RecallOperation, type RerollOperation, type RetryOperation, type RetryableTurn, type RewindOperation, SESSION_BRANCH_VERSION_SCHEMA, type SessionBranchVersionEvent, SessionEditor, SessionEditor as default, type SessionEditorOperation, type SessionEditorResult, closedTurns, editableMessages, retryableTurns };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as SessionEditorResult, S as SessionEditorOperation, _ as RecallOperation, b as RetryableTurn, c as editableMessages, g as EditableMessageBlock, h as EditOperation, i as closedTurns, m as retryableTurns, v as RerollOperation, x as RewindOperation, y as RetryOperation } from "./plan-Cm9hCord.mjs";
|
|
2
2
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { AgentCancelCause, Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { BranchBoundary, BranchTimeline, BranchTimeline as BranchTimeline$1, CascadePolicy, EditableBlockKind, SESSION_BRANCH_VERSION_SCHEMA, SessionBranchVersionEvent } from "@morlay/session-branch";
|
|
@@ -12,7 +12,6 @@ declare module "@deepseek-ai/cordis" {
|
|
|
12
12
|
interface EditorAgent {
|
|
13
13
|
readonly session: Session;
|
|
14
14
|
followup(message: UserMessage): void;
|
|
15
|
-
/** 等待 agent 到达 quiescence(当前 turn/任务结束后 resolve)。 */
|
|
16
15
|
whenIdle(): Promise<void>;
|
|
17
16
|
cancel?(cause: AgentCancelCause, options?: {
|
|
18
17
|
keepInbox?: boolean;
|
|
@@ -65,15 +64,9 @@ declare class SessionEditor extends Service {
|
|
|
65
64
|
editableMessages(sessionId: SessionId, signal?: AbortSignal): Promise<EditableMessageBlock[]>;
|
|
66
65
|
retryableTurns(sessionId: SessionId, signal?: AbortSignal): Promise<RetryableTurn[]>;
|
|
67
66
|
private branchOperation;
|
|
68
|
-
/**
|
|
69
|
-
* 撤回(recall):只 rewind 截断,不重写、不重放——被撤回的 user 消息文本
|
|
70
|
-
* 由客户端回填到输入框,交给用户修改后重新发送。
|
|
71
|
-
*
|
|
72
|
-
* 边界见 {@link recallBoundary}:轮首消息整轮截断到前一轮 `turn/end`
|
|
73
|
-
* (无前轮则 -1),轮内 followup 与轮外 user 消息只截断到该消息本身
|
|
74
|
-
* (exclusive drop),避免留下悬空 `turn/start` 让下一次发送开到错误轮号。
|
|
75
|
-
*/
|
|
76
67
|
private recallOperation;
|
|
68
|
+
private seedStart;
|
|
69
|
+
private appendSeedSuffix;
|
|
77
70
|
private stopLoop;
|
|
78
71
|
private readEvents;
|
|
79
72
|
private prepareReplay;
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,461 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { closedTurns, editableMessages, retryableTurns } from "./plan.mjs";
|
|
1
|
+
import { n as toTimelinePayload, t as SESSION_EDITOR_PATH } from "./shared-LK3nImWW.mjs";
|
|
2
|
+
import { closedTurns, droppedCompactions, editPlan, editableMessages, precedingContentIndex, recallBoundary, rerollPlan, restoreCompactions, retryPlan, retryableTurns } from "./plan.mjs";
|
|
3
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
4
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import { SESSION_BRANCH_VERSION_SCHEMA, SessionBranchError, balanceRewindPrefix } from "@morlay/session-branch";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
function appendLogSeedEvent(events, type, data, ignorable = false) {
|
|
8
|
+
events.push({
|
|
9
|
+
type,
|
|
10
|
+
seq: events.length,
|
|
11
|
+
time: Date.now(),
|
|
12
|
+
data,
|
|
13
|
+
...ignorable ? { ignorable: true } : {}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function appendSurfaceSeedEvent(events, type, data, intent) {
|
|
17
|
+
events.push({
|
|
18
|
+
type,
|
|
19
|
+
seq: SessionSeq(events.length),
|
|
20
|
+
time: Date.now(),
|
|
21
|
+
data,
|
|
22
|
+
surfaceOp: intent.surfaceOp,
|
|
23
|
+
...intent.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: intent.sourceEventSeqs }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function appendManualTurn(events, manual) {
|
|
27
|
+
const { turn, user, assistant } = manual;
|
|
28
|
+
appendLogSeedEvent(events, "turn/start", { turn });
|
|
29
|
+
appendSurfaceSeedEvent(events, "user/message", user, { surfaceOp: "append" });
|
|
30
|
+
appendLogSeedEvent(events, "step/start", {
|
|
31
|
+
turn,
|
|
32
|
+
step: 1
|
|
33
|
+
});
|
|
34
|
+
appendSurfaceSeedEvent(events, "assistant/message", {
|
|
35
|
+
turn,
|
|
36
|
+
step: 1,
|
|
37
|
+
message: assistant,
|
|
38
|
+
stream: []
|
|
39
|
+
}, { surfaceOp: "append" });
|
|
40
|
+
appendLogSeedEvent(events, "step/end", {
|
|
41
|
+
turn,
|
|
42
|
+
step: 1
|
|
43
|
+
});
|
|
44
|
+
appendLogSeedEvent(events, "turn/end", {
|
|
45
|
+
turn,
|
|
46
|
+
reason: { kind: "completed" }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function keepFromOf(events, boundary) {
|
|
50
|
+
if (boundary < 0) return 0;
|
|
51
|
+
return events[boundary]?.type === "turn/end" ? boundary + 1 : boundary;
|
|
52
|
+
}
|
|
53
|
+
async function appendSeedSuffixLive(session, seedSuffix, appendDirect) {
|
|
54
|
+
for (const event of seedSuffix) {
|
|
55
|
+
if (event.ignorable === true) {
|
|
56
|
+
const s = session;
|
|
57
|
+
const seq = s.log.length;
|
|
58
|
+
await appendDirect([{
|
|
59
|
+
...event,
|
|
60
|
+
seq
|
|
61
|
+
}]);
|
|
62
|
+
s.log.push({
|
|
63
|
+
...event,
|
|
64
|
+
seq
|
|
65
|
+
});
|
|
66
|
+
s.eventsSnapshot = void 0;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const s = session;
|
|
70
|
+
const raw = event;
|
|
71
|
+
if (raw.surfaceOp !== void 0) s.append(event.type, event.data, {
|
|
72
|
+
surfaceOp: raw.surfaceOp,
|
|
73
|
+
...raw.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: raw.sourceEventSeqs }
|
|
74
|
+
});
|
|
75
|
+
else s.append(event.type, event.data);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var SessionEditor = class extends Service {
|
|
79
|
+
static inject = [
|
|
80
|
+
"sessionBranch",
|
|
81
|
+
"sessionPersistence",
|
|
82
|
+
"sessions"
|
|
83
|
+
];
|
|
84
|
+
constructor(ctx) {
|
|
85
|
+
super(ctx, "sessionEditor");
|
|
86
|
+
registerHttpRoutes(ctx);
|
|
87
|
+
}
|
|
88
|
+
readBranchPrefix(id, atSeq, mode, signal) {
|
|
89
|
+
return this.ctx.sessionBranch.readBranchPrefix(id, atSeq, mode, signal);
|
|
90
|
+
}
|
|
91
|
+
fork(sourceId, atSeq, childSessionId, meta, signal) {
|
|
92
|
+
return this.ctx.sessionBranch.forkFrom(sourceId, {
|
|
93
|
+
...atSeq === void 0 ? {} : { atSeq },
|
|
94
|
+
...childSessionId === void 0 ? {} : { childSessionId },
|
|
95
|
+
...meta === void 0 ? {} : { meta }
|
|
96
|
+
}, signal);
|
|
97
|
+
}
|
|
98
|
+
async rewind(id, toBoundary, signal) {
|
|
99
|
+
await this.stopLoop(id, signal);
|
|
100
|
+
return this.ctx.sessionBranch.rewind(id, toBoundary, signal);
|
|
101
|
+
}
|
|
102
|
+
timeline(sessionId, signal) {
|
|
103
|
+
return this.ctx.sessionBranch.timeline(sessionId, signal);
|
|
104
|
+
}
|
|
105
|
+
edit(operation, signal) {
|
|
106
|
+
return this.branchOperation(operation, signal);
|
|
107
|
+
}
|
|
108
|
+
reroll(operation, signal) {
|
|
109
|
+
return this.branchOperation(operation, signal);
|
|
110
|
+
}
|
|
111
|
+
retry(operation, signal) {
|
|
112
|
+
return this.branchOperation(operation, signal);
|
|
113
|
+
}
|
|
114
|
+
recall(operation, signal) {
|
|
115
|
+
return this.recallOperation(operation, signal);
|
|
116
|
+
}
|
|
117
|
+
async editableMessages(sessionId, signal) {
|
|
118
|
+
const events = await this.readEvents(sessionId, signal);
|
|
119
|
+
return editableMessages(closedTurns(events));
|
|
120
|
+
}
|
|
121
|
+
async retryableTurns(sessionId, signal) {
|
|
122
|
+
const events = await this.readEvents(sessionId, signal);
|
|
123
|
+
return retryableTurns(closedTurns(events));
|
|
124
|
+
}
|
|
125
|
+
async branchOperation(operation, signal) {
|
|
126
|
+
signal?.throwIfAborted();
|
|
127
|
+
const events = await this.readEvents(operation.sessionId, signal);
|
|
128
|
+
const turns = closedTurns(events);
|
|
129
|
+
const plan = operation.action === "edit" ? editPlan(operation, turns) : operation.action === "retry" ? retryPlan(operation, turns) : rerollPlan(operation, turns);
|
|
130
|
+
const headerConfig = events.findLast((event) => event.type === "request/header")?.data.header.config;
|
|
131
|
+
const turnIndex = turns.findIndex((turn) => turn.startSeq === plan.anchorSeq);
|
|
132
|
+
const preceding = precedingContentIndex(turns, turnIndex);
|
|
133
|
+
const boundary = plan.rewindBoundary !== void 0 ? plan.rewindBoundary : preceding < 0 ? -1 : turns[preceding].endSeq;
|
|
134
|
+
const keepFrom = keepFromOf(events, boundary);
|
|
135
|
+
const compactions = droppedCompactions(events, keepFrom, plan.targetSeq);
|
|
136
|
+
const replayTurn = preceding < 0 ? 1 : turns[preceding].turn + 1;
|
|
137
|
+
const versionSeed = [];
|
|
138
|
+
appendLogSeedEvent(versionSeed, "session-branch/version", plan.version, true);
|
|
139
|
+
const manualSeed = [];
|
|
140
|
+
if (plan.manualTurn !== void 0) appendManualTurn(manualSeed, {
|
|
141
|
+
...plan.manualTurn,
|
|
142
|
+
turn: replayTurn
|
|
143
|
+
});
|
|
144
|
+
const replay = await this.prepareReplay(operation.sessionId, plan.queuedUsers, signal, headerConfig);
|
|
145
|
+
await this.stopLoop(operation.sessionId, signal);
|
|
146
|
+
const live = this.ctx.sessions.get(operation.sessionId);
|
|
147
|
+
await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
|
|
148
|
+
const seedStart = this.seedStart(live, events, keepFrom);
|
|
149
|
+
const seedSuffix = [
|
|
150
|
+
...versionSeed,
|
|
151
|
+
...restoreCompactions(compactions, keepFrom, seedStart + versionSeed.length),
|
|
152
|
+
...manualSeed
|
|
153
|
+
];
|
|
154
|
+
await this.appendSeedSuffix(operation.sessionId, live, seedSuffix, events, keepFrom);
|
|
155
|
+
let queuedTurns = 0;
|
|
156
|
+
if (replay.agent !== void 0 && plan.queuedUsers.length > 0) {
|
|
157
|
+
const lastSeedTurn = seedSuffix.findLast((event) => event.type === "turn/start")?.data.turn;
|
|
158
|
+
if (lastSeedTurn !== void 0) {
|
|
159
|
+
const phase = replay.agent.phase;
|
|
160
|
+
if (phase !== void 0) phase.lastTurn = lastSeedTurn;
|
|
161
|
+
}
|
|
162
|
+
for (const message of plan.queuedUsers) replay.agent.followup(message);
|
|
163
|
+
await this.ctx.sessions.flush(replay.agent.session);
|
|
164
|
+
queuedTurns = plan.queuedUsers.length;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
sessionId: operation.sessionId,
|
|
168
|
+
queuedTurns,
|
|
169
|
+
live: this.ctx.sessions.get(operation.sessionId) !== void 0
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async recallOperation(operation, signal) {
|
|
173
|
+
signal?.throwIfAborted();
|
|
174
|
+
const events = await this.readEvents(operation.sessionId, signal);
|
|
175
|
+
const boundary = recallBoundary(events, closedTurns(events), operation.eventSeq);
|
|
176
|
+
const keepFrom = keepFromOf(events, boundary);
|
|
177
|
+
const compactions = droppedCompactions(events, keepFrom, operation.eventSeq);
|
|
178
|
+
await this.stopLoop(operation.sessionId, signal);
|
|
179
|
+
const live = this.ctx.sessions.get(operation.sessionId);
|
|
180
|
+
await this.ctx.sessionBranch.rewind(operation.sessionId, boundary, signal);
|
|
181
|
+
await this.appendSeedSuffix(operation.sessionId, live, restoreCompactions(compactions, keepFrom, this.seedStart(live, events, keepFrom)), events, keepFrom);
|
|
182
|
+
return {
|
|
183
|
+
sessionId: operation.sessionId,
|
|
184
|
+
queuedTurns: 0,
|
|
185
|
+
live: this.ctx.sessions.get(operation.sessionId) !== void 0
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
seedStart(live, events, keepFrom) {
|
|
189
|
+
return live === void 0 ? balanceRewindPrefix(events.slice(0, keepFrom)).length : Number(live.seq);
|
|
190
|
+
}
|
|
191
|
+
async appendSeedSuffix(sessionId, live, seedSuffix, events, keepFrom) {
|
|
192
|
+
if (seedSuffix.length === 0) return;
|
|
193
|
+
if (live !== void 0) {
|
|
194
|
+
const handle = this.ctx.sessionPersistence.tracker.writerOf(sessionId);
|
|
195
|
+
if (handle === void 0) throw new Error(`session "${sessionId}" has no live write handle for the version effect`);
|
|
196
|
+
await this.ctx.sessions.flush(live);
|
|
197
|
+
await appendSeedSuffixLive(live, seedSuffix, (batch) => handle.append(batch));
|
|
198
|
+
await this.ctx.sessions.flush(live);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const keepLength = balanceRewindPrefix(events.slice(0, keepFrom)).length;
|
|
202
|
+
const renumbered = seedSuffix.map((event, index) => ({
|
|
203
|
+
...event,
|
|
204
|
+
seq: keepLength + index
|
|
205
|
+
}));
|
|
206
|
+
const handle = await this.ctx.sessionPersistence.open(sessionId, "write");
|
|
207
|
+
try {
|
|
208
|
+
await handle.append(renumbered);
|
|
209
|
+
} finally {
|
|
210
|
+
await handle.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async stopLoop(sessionId, signal) {
|
|
214
|
+
signal?.throwIfAborted();
|
|
215
|
+
const existing = this.ctx.get("agents")?.get(sessionId);
|
|
216
|
+
if (existing === void 0) return;
|
|
217
|
+
existing.cancel?.({ kind: "user" }, { keepInbox: true });
|
|
218
|
+
await existing.whenIdle();
|
|
219
|
+
signal?.throwIfAborted();
|
|
220
|
+
}
|
|
221
|
+
async readEvents(sessionId, signal) {
|
|
222
|
+
const live = this.ctx.sessions.get(sessionId);
|
|
223
|
+
if (live !== void 0) return live.snapshotEvents();
|
|
224
|
+
return (await this.ctx.sessionBranch.readRawEvents(sessionId, signal)).events;
|
|
225
|
+
}
|
|
226
|
+
async prepareReplay(sessionId, queuedUsers, signal, headerConfig) {
|
|
227
|
+
if (queuedUsers.length === 0) return { agent: void 0 };
|
|
228
|
+
signal?.throwIfAborted();
|
|
229
|
+
const agents = this.ctx.get("agents");
|
|
230
|
+
if (agents === void 0) return { agent: void 0 };
|
|
231
|
+
const existing = agents.get(sessionId);
|
|
232
|
+
if (existing !== void 0) return { agent: existing };
|
|
233
|
+
const provider = headerConfig?.provider ?? "";
|
|
234
|
+
const model = headerConfig?.model ?? "";
|
|
235
|
+
if (provider.length === 0 || model.length === 0) {
|
|
236
|
+
const config = (await this.readEvents(sessionId, signal)).findLast((event) => event.type === "request/header")?.data.header.config;
|
|
237
|
+
const fallbackProvider = config?.provider ?? "";
|
|
238
|
+
const fallbackModel = config?.model ?? "";
|
|
239
|
+
if (fallbackProvider.length === 0 || fallbackModel.length === 0) throw new SessionBranchError("无法重放:会话没有可解析的模型配置。", "INVALID_BOUNDARY");
|
|
240
|
+
const handle = await agents.resume({
|
|
241
|
+
resumeSessionId: sessionId,
|
|
242
|
+
agentOptions: {
|
|
243
|
+
provider: fallbackProvider,
|
|
244
|
+
model: fallbackModel
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
signal?.throwIfAborted();
|
|
248
|
+
return { agent: handle.agent };
|
|
249
|
+
}
|
|
250
|
+
const handle = await agents.resume({
|
|
251
|
+
resumeSessionId: sessionId,
|
|
252
|
+
agentOptions: {
|
|
253
|
+
provider,
|
|
254
|
+
model
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
return { agent: handle.agent };
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
function objectValue(value) {
|
|
262
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("请求体必须是 JSON 对象。");
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
function sessionIdOf(value) {
|
|
266
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError("sessionId 必须是非空字符串。");
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
function integerOf(value, name) {
|
|
270
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} 必须是非负安全整数。`);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function cascadeOf(value) {
|
|
274
|
+
if (value !== "truncate" && value !== "preserve") throw new TypeError("cascade 必须是 truncate 或 preserve。");
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
function decodeOperation(value) {
|
|
278
|
+
const record = objectValue(value);
|
|
279
|
+
const sessionId = sessionIdOf(record["sessionId"]);
|
|
280
|
+
switch (record["action"]) {
|
|
281
|
+
case "edit":
|
|
282
|
+
if (typeof record["text"] !== "string") throw new TypeError("text 必须是字符串。");
|
|
283
|
+
return {
|
|
284
|
+
action: "edit",
|
|
285
|
+
sessionId,
|
|
286
|
+
eventSeq: integerOf(record["eventSeq"], "eventSeq"),
|
|
287
|
+
blockIndex: integerOf(record["blockIndex"], "blockIndex"),
|
|
288
|
+
text: record["text"],
|
|
289
|
+
cascade: cascadeOf(record["cascade"])
|
|
290
|
+
};
|
|
291
|
+
case "reroll": return {
|
|
292
|
+
action: "reroll",
|
|
293
|
+
sessionId
|
|
294
|
+
};
|
|
295
|
+
case "retry": return {
|
|
296
|
+
action: "retry",
|
|
297
|
+
sessionId,
|
|
298
|
+
turn: integerOf(record["turn"], "turn"),
|
|
299
|
+
cascade: cascadeOf(record["cascade"])
|
|
300
|
+
};
|
|
301
|
+
case "rewind": return {
|
|
302
|
+
action: "rewind",
|
|
303
|
+
sessionId,
|
|
304
|
+
toBoundary: integerOf(record["toBoundary"], "toBoundary")
|
|
305
|
+
};
|
|
306
|
+
case "recall": return {
|
|
307
|
+
action: "recall",
|
|
308
|
+
sessionId,
|
|
309
|
+
eventSeq: integerOf(record["eventSeq"], "eventSeq")
|
|
310
|
+
};
|
|
311
|
+
case "fork": return {
|
|
312
|
+
action: "fork",
|
|
313
|
+
sessionId,
|
|
314
|
+
...record["atSeq"] === void 0 ? {} : { atSeq: integerOf(record["atSeq"], "atSeq") },
|
|
315
|
+
...record["childSessionId"] === void 0 ? {} : { childSessionId: sessionIdOf(record["childSessionId"]) }
|
|
316
|
+
};
|
|
317
|
+
default: throw new TypeError("action 必须是 edit、reroll、retry、rewind、recall 或 fork。");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function requestJson(request) {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const decoder = new TextDecoder();
|
|
323
|
+
let text = "";
|
|
324
|
+
request.on("data", (chunk) => {
|
|
325
|
+
text += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
326
|
+
});
|
|
327
|
+
request.on("end", () => {
|
|
328
|
+
try {
|
|
329
|
+
text += decoder.decode();
|
|
330
|
+
resolve(JSON.parse(text));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
reject(error);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
request.on("error", reject);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function respondJson(response, status, value) {
|
|
339
|
+
response.writeHead(status, {
|
|
340
|
+
"content-type": "application/json; charset=utf-8",
|
|
341
|
+
"cache-control": "no-store"
|
|
342
|
+
});
|
|
343
|
+
response.end(JSON.stringify(value));
|
|
344
|
+
}
|
|
345
|
+
async function readTimeline(editor, sessionId) {
|
|
346
|
+
const timeline = await editor.timeline(sessionId);
|
|
347
|
+
const messages = await editor.editableMessages(sessionId);
|
|
348
|
+
const retryable = await editor.retryableTurns(sessionId);
|
|
349
|
+
return toTimelinePayload(sessionId, timeline, messages, retryable);
|
|
350
|
+
}
|
|
351
|
+
async function runOperation(editor, operation) {
|
|
352
|
+
switch (operation.action) {
|
|
353
|
+
case "edit": {
|
|
354
|
+
const result = await editor.edit(operation);
|
|
355
|
+
return {
|
|
356
|
+
sessionId: result.sessionId,
|
|
357
|
+
queuedTurns: result.queuedTurns,
|
|
358
|
+
...result.live === void 0 ? {} : { live: result.live }
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
case "reroll": {
|
|
362
|
+
const result = await editor.reroll(operation);
|
|
363
|
+
return {
|
|
364
|
+
sessionId: result.sessionId,
|
|
365
|
+
queuedTurns: result.queuedTurns,
|
|
366
|
+
...result.live === void 0 ? {} : { live: result.live }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
case "retry": {
|
|
370
|
+
const result = await editor.retry(operation);
|
|
371
|
+
return {
|
|
372
|
+
sessionId: result.sessionId,
|
|
373
|
+
queuedTurns: result.queuedTurns,
|
|
374
|
+
...result.live === void 0 ? {} : { live: result.live }
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
case "rewind":
|
|
378
|
+
await editor.rewind(operation.sessionId, operation.toBoundary);
|
|
379
|
+
return {
|
|
380
|
+
sessionId: operation.sessionId,
|
|
381
|
+
queuedTurns: 0
|
|
382
|
+
};
|
|
383
|
+
case "recall": {
|
|
384
|
+
const result = await editor.recall(operation);
|
|
385
|
+
return {
|
|
386
|
+
sessionId: result.sessionId,
|
|
387
|
+
queuedTurns: result.queuedTurns,
|
|
388
|
+
...result.live === void 0 ? {} : { live: result.live }
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
case "fork": return {
|
|
392
|
+
sessionId: await editor.fork(operation.sessionId, operation.atSeq, operation.childSessionId),
|
|
393
|
+
queuedTurns: 0
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function handleRoute(editor, request, response) {
|
|
398
|
+
try {
|
|
399
|
+
if (request.method === "GET") {
|
|
400
|
+
respondJson(response, 200, await readTimeline(editor, sessionIdOf(new URL(request.url ?? "/session-editor", "http://session-editor.local").searchParams.get("sessionId"))));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (request.method === "POST") {
|
|
404
|
+
respondJson(response, 200, await runOperation(editor, decodeOperation(await requestJson(request))));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
response.writeHead(405);
|
|
408
|
+
response.end();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
411
|
+
respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function registerHttpRoutes(ctx) {
|
|
415
|
+
ctx.effect(() => {
|
|
416
|
+
const webServer = ctx.get("webServer");
|
|
417
|
+
if (webServer === void 0) return () => {};
|
|
418
|
+
const editor = ctx.sessionEditor;
|
|
419
|
+
return webServer.register({
|
|
420
|
+
kind: "exact",
|
|
421
|
+
path: SESSION_EDITOR_PATH,
|
|
422
|
+
handler: (request, response) => handleRoute(editor, request, response)
|
|
423
|
+
});
|
|
424
|
+
}, "session-editor: HTTP route");
|
|
425
|
+
const registerConnectionRoute = () => {
|
|
426
|
+
const connection = ctx.get("connection", false);
|
|
427
|
+
if (connection === void 0) return;
|
|
428
|
+
const editor = ctx.sessionEditor;
|
|
429
|
+
ctx.effect(() => connection.fetch.register({
|
|
430
|
+
path: `/api${SESSION_EDITOR_PATH}`,
|
|
431
|
+
methods: ["GET", "POST"],
|
|
432
|
+
requestBody: "buffered",
|
|
433
|
+
fetch: (request) => handleFetchRoute(editor, request)
|
|
434
|
+
}), "session-editor: connection fetch route");
|
|
435
|
+
};
|
|
436
|
+
ctx.on("internal/service", (name) => {
|
|
437
|
+
if (name === "connection") registerConnectionRoute();
|
|
438
|
+
});
|
|
439
|
+
registerConnectionRoute();
|
|
440
|
+
}
|
|
441
|
+
async function handleFetchRoute(editor, request) {
|
|
442
|
+
try {
|
|
443
|
+
if (request.method === "GET") return jsonResponse(200, await readTimeline(editor, sessionIdOf(new URL(request.url).searchParams.get("sessionId"))));
|
|
444
|
+
if (request.method === "POST") return jsonResponse(200, await runOperation(editor, decodeOperation(await request.json())));
|
|
445
|
+
return new Response(null, { status: 405 });
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
448
|
+
return jsonResponse(error instanceof TypeError ? 400 : 409, { error: message });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function jsonResponse(status, value) {
|
|
452
|
+
return new Response(JSON.stringify(value), {
|
|
453
|
+
status,
|
|
454
|
+
headers: {
|
|
455
|
+
"content-type": "application/json; charset=utf-8",
|
|
456
|
+
"cache-control": "no-store"
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
3
461
|
export { SESSION_BRANCH_VERSION_SCHEMA, SessionEditor, SessionEditor as default, closedTurns, editableMessages, retryableTurns };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/invariant.d.ts
|
|
3
|
+
declare const name = "ui-conversation-message-actions-invariant";
|
|
4
|
+
declare const inject: string[];
|
|
5
|
+
declare const apply: (ctx: Context) => Promise<() => void>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { apply, inject, name };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
|
|
1
2
|
import { SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
2
3
|
import { BranchTimeline, CascadePolicy, SessionBranchVersionEvent } from "@morlay/session-branch";
|
|
3
|
-
import { AssistantMessage, UserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
interface EditOperation {
|
|
6
6
|
action: "edit";
|
|
@@ -65,7 +65,6 @@ interface ClosedTurn {
|
|
|
65
65
|
endSeq?: number;
|
|
66
66
|
closed: boolean;
|
|
67
67
|
user?: SessionEvent<"user/message">;
|
|
68
|
-
/** 轮内全部 user/message(agent 运行中 followup 追加的输入也计入)。 */
|
|
69
68
|
users: SessionEvent<"user/message">[];
|
|
70
69
|
assistants: SessionEvent<"assistant/message">[];
|
|
71
70
|
}
|
|
@@ -79,21 +78,24 @@ interface OperationPlan {
|
|
|
79
78
|
assistant: AssistantMessage;
|
|
80
79
|
};
|
|
81
80
|
queuedUsers: UserMessage[];
|
|
81
|
+
targetSeq: number;
|
|
82
82
|
}
|
|
83
83
|
declare function closedTurns(events: readonly SessionEvent[]): ClosedTurn[];
|
|
84
84
|
declare function editableMessages(turns: readonly ClosedTurn[]): EditableMessageBlock[];
|
|
85
85
|
declare function retryableTurns(turns: readonly ClosedTurn[]): RetryableTurn[];
|
|
86
|
-
/**
|
|
87
|
-
* 目标轮之前最后一个「有内容」轮次的下标:跳过既无用户输入也无助手回复的
|
|
88
|
-
* 空轮。空轮是早期重放缺陷的遗留(turn/start 后直接 turn/end),在轨迹 /
|
|
89
|
-
* 轮次导航里表现为点不开的空项,并让后续轮号出现空洞。rewind 把它们随
|
|
90
|
-
* 目标轮一起截断,重放按保留前缀续号即可自愈。
|
|
91
|
-
*/
|
|
92
86
|
declare function precedingContentIndex(turns: readonly ClosedTurn[], turnIndex: number): number;
|
|
93
87
|
declare function downstreamUsers(turns: readonly ClosedTurn[], start: number): UserMessage[];
|
|
94
88
|
declare function recallBoundary(events: readonly SessionEvent[], turns: readonly ClosedTurn[], eventSeq: number): number;
|
|
95
89
|
declare function editPlan(operation: EditOperation, turns: readonly ClosedTurn[]): OperationPlan;
|
|
96
90
|
declare function retryPlan(operation: RetryOperation, turns: readonly ClosedTurn[]): OperationPlan;
|
|
97
91
|
declare function rerollPlan(operation: RerollOperation, turns: readonly ClosedTurn[]): OperationPlan;
|
|
92
|
+
interface CompactionBracket {
|
|
93
|
+
start: SessionEvent;
|
|
94
|
+
summary?: SessionEvent;
|
|
95
|
+
checkpoint: SessionEvent;
|
|
96
|
+
end: SessionEvent;
|
|
97
|
+
}
|
|
98
|
+
declare function droppedCompactions(events: readonly SessionEvent[], keepFrom: number, targetSeq: number): CompactionBracket[];
|
|
99
|
+
declare function restoreCompactions(brackets: readonly CompactionBracket[], keepFrom: number, base: number): SessionEvent[];
|
|
98
100
|
//#endregion
|
|
99
|
-
export {
|
|
101
|
+
export { SessionEditorResult as C, SessionEditorOperation as S, RecallOperation as _, downstreamUsers as a, RetryableTurn as b, editableMessages as c, rerollPlan as d, restoreCompactions as f, EditableMessageBlock as g, EditOperation as h, closedTurns as i, precedingContentIndex as l, retryableTurns as m, CompactionBracket as n, droppedCompactions as o, retryPlan as p, OperationPlan as r, editPlan as s, ClosedTurn as t, recallBoundary as u, RerollOperation as v, RewindOperation as x, RetryOperation as y };
|