@khorsheed/dsh-client-message-tools 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.en.md +82 -0
- package/README.i18n.yaml +6 -0
- package/README.md +82 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +5824 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +644 -0
- package/lib/invariant.js +27 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.host.tsbuildinfo +1 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +250 -0
- package/lib/typert.remote-client.d.ts +26 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +154 -0
- package/lib/types/client/ModelChip.d.ts +29 -0
- package/lib/types/client/ModelChip.d.ts.map +1 -0
- package/lib/types/client/ModelChip.js +86 -0
- package/lib/types/client/RestoredMessageView.d.ts +17 -0
- package/lib/types/client/RestoredMessageView.d.ts.map +1 -0
- package/lib/types/client/RestoredMessageView.js +9 -0
- package/lib/types/client/UserMessageView.d.ts +16 -0
- package/lib/types/client/UserMessageView.d.ts.map +1 -0
- package/lib/types/client/UserMessageView.js +172 -0
- package/lib/types/client/WithdrawnDividerView.d.ts +17 -0
- package/lib/types/client/WithdrawnDividerView.d.ts.map +1 -0
- package/lib/types/client/WithdrawnDividerView.js +59 -0
- package/lib/types/client/backfill.d.ts +19 -0
- package/lib/types/client/backfill.d.ts.map +1 -0
- package/lib/types/client/backfill.js +20 -0
- package/lib/types/client/dom-hider.d.ts +61 -0
- package/lib/types/client/dom-hider.d.ts.map +1 -0
- package/lib/types/client/dom-hider.js +195 -0
- package/lib/types/client/edit-in-place.d.ts +107 -0
- package/lib/types/client/edit-in-place.d.ts.map +1 -0
- package/lib/types/client/edit-in-place.js +97 -0
- package/lib/types/client/icons.d.ts +23 -0
- package/lib/types/client/icons.d.ts.map +1 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +187 -0
- package/lib/types/client/locales.d.ts +74 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/locales.js +65 -0
- package/lib/types/client/slots.d.ts +44 -0
- package/lib/types/client/slots.d.ts.map +1 -0
- package/lib/types/client/slots.js +1 -0
- package/lib/types/client/withdraw-backfill.d.ts +25 -0
- package/lib/types/client/withdraw-backfill.d.ts.map +1 -0
- package/lib/types/client/withdraw-backfill.js +21 -0
- package/lib/types/client/withdrawn-node.d.ts +163 -0
- package/lib/types/client/withdrawn-node.d.ts.map +1 -0
- package/lib/types/client/withdrawn-node.js +297 -0
- package/lib/types/index.d.ts +69 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +175 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +25 -0
- package/lib/types/marker.d.ts +94 -0
- package/lib/types/marker.d.ts.map +1 -0
- package/lib/types/marker.js +131 -0
- package/lib/types/types.d.ts +116 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +1 -0
- package/lib/types/withdraw.d.ts +112 -0
- package/lib/types/withdraw.d.ts.map +1 -0
- package/lib/types/withdraw.js +279 -0
- package/package.json +108 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
|
+
import { isAppendSurfaceEvent, isReplacementSurfaceEvent } from "@deepseek-ai/dsh-session/surface";
|
|
4
|
+
//#region lib/types/marker.js
|
|
5
|
+
/**
|
|
6
|
+
* Shared message-tools markers: the plugin source tag stamped on withdrawal
|
|
7
|
+
* replacement events, the `op` discriminator telling edit replacements, edit
|
|
8
|
+
* triggers, and assistant-text restore replays apart from withdrawals and
|
|
9
|
+
* user-message restores, the model-facing placeholder and trigger texts, and
|
|
10
|
+
* the pure predicates both the Host service and the browser projection
|
|
11
|
+
* consume. No new session event type is ever written — an
|
|
12
|
+
* out-of-harness event type cannot carry `ignorable: true` (Session.append
|
|
13
|
+
* assigns the envelope), so a persisted unknown type would make
|
|
14
|
+
* session-persistence refuse the log on reload. The events themselves are the
|
|
15
|
+
* durable audit trail.
|
|
16
|
+
* @module @khorsheed/dsh-client-message-tools/marker
|
|
17
|
+
*/
|
|
18
|
+
/** Plugin source tag identifying every message-tools event. */
|
|
19
|
+
const MESSAGE_TOOLS_PLUGIN = "message-tools";
|
|
20
|
+
/**
|
|
21
|
+
* Model-facing placeholder left on the surface in place of the withdrawn
|
|
22
|
+
* span. The span itself is hidden from the model; this minimal user message
|
|
23
|
+
* keeps the surface non-empty and tells the model why the transcript jumps.
|
|
24
|
+
*/
|
|
25
|
+
const WITHDRAWN_NOTICE = "(用户撤回了这条消息及其后的所有内容)";
|
|
26
|
+
/**
|
|
27
|
+
* Model-facing trigger message that starts the regeneration turn after an
|
|
28
|
+
* edit replacement lands (no wake-without-append path exists in the harness;
|
|
29
|
+
* `agent.followup` is the sanctioned seam). It is NOT the edited text — that
|
|
30
|
+
* appears exactly once, in the replacement event.
|
|
31
|
+
*/
|
|
32
|
+
const EDIT_TRIGGER_NOTICE = "(用户编辑了上一条消息,请按编辑后的内容重新回答)";
|
|
33
|
+
/**
|
|
34
|
+
* Model-facing frame preceding a replayed assistant text in a restore: the
|
|
35
|
+
* replay lands as a user-role message (assistant/message cannot carry a
|
|
36
|
+
* plugin source, and the session turn/step trace forbids ad-hoc assistant
|
|
37
|
+
* appends), so the frame keeps the role honest for the model.
|
|
38
|
+
*/
|
|
39
|
+
const RESTORED_ASSISTANT_NOTICE = "(以下是先前被撤回、现随恢复放回的助手回复)";
|
|
40
|
+
/** The plugin-tagged source of an edit replacement (op: 'edit'). */
|
|
41
|
+
function editReplacementSource() {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
kind: "plugin",
|
|
44
|
+
plugin: MESSAGE_TOOLS_PLUGIN,
|
|
45
|
+
op: "edit"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The plugin-tagged source of an edit trigger message (op: 'edit-trigger'). */
|
|
49
|
+
function editTriggerSource() {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
kind: "plugin",
|
|
52
|
+
plugin: MESSAGE_TOOLS_PLUGIN,
|
|
53
|
+
op: "edit-trigger"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** The plugin-tagged source of a restored assistant-text replay (op: 'restore-assistant'). */
|
|
57
|
+
function restoreAssistantSource() {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
kind: "plugin",
|
|
60
|
+
plugin: MESSAGE_TOOLS_PLUGIN,
|
|
61
|
+
op: "restore-assistant"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Read the op discriminator off a message source (absent on legacy events). */
|
|
65
|
+
function messageToolsOp(source) {
|
|
66
|
+
const op = source?.op;
|
|
67
|
+
return op === "edit" || op === "edit-trigger" || op === "restore-assistant" ? op : void 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether the event is a message-tools WITHDRAWAL replacement: a
|
|
71
|
+
* `user/message` surface replacement whose source carries this plugin's tag
|
|
72
|
+
* and no `op` discriminator (legacy and current withdrawals both).
|
|
73
|
+
* @param event - the session event to test.
|
|
74
|
+
* @returns the event narrowed to a withdrawal replacement when matched.
|
|
75
|
+
*/
|
|
76
|
+
function isMessageToolsReplacement(event) {
|
|
77
|
+
return event.type === "user/message" && isReplacementSurfaceEvent(event) && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === void 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether the event is a message-tools EDIT replacement: a `user/message`
|
|
81
|
+
* surface replacement whose content IS the edited text (`op: 'edit'`).
|
|
82
|
+
* @param event - the session event to test.
|
|
83
|
+
* @returns the event narrowed to an edit replacement when matched.
|
|
84
|
+
*/
|
|
85
|
+
function isMessageToolsEdit(event) {
|
|
86
|
+
return event.type === "user/message" && isReplacementSurfaceEvent(event) && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === "edit";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether the event is a message-tools restore: an append-surface
|
|
90
|
+
* `user/message` replaying a withdrawn message at the conversation tail.
|
|
91
|
+
* @param event - the session event to test.
|
|
92
|
+
* @returns the event narrowed to a message-tools restore when matched.
|
|
93
|
+
*/
|
|
94
|
+
function isMessageToolsRestore(event) {
|
|
95
|
+
return event.type === "user/message" && event.surfaceOp === "append" && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === void 0;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether the event is a message-tools ASSISTANT-text restore replay: an
|
|
99
|
+
* append-surface `user/message` carrying a withdrawn assistant reply's text
|
|
100
|
+
* (framed, user-role — see {@link RESTORED_ASSISTANT_NOTICE}).
|
|
101
|
+
* @param event - the session event to test.
|
|
102
|
+
* @returns the event narrowed to a restore-assistant replay when matched.
|
|
103
|
+
*/
|
|
104
|
+
function isMessageToolsRestoreAssistant(event) {
|
|
105
|
+
return event.type === "user/message" && event.surfaceOp === "append" && event.data.source.kind === "plugin" && event.data.source.plugin === "message-tools" && messageToolsOp(event.data.source) === "restore-assistant";
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region lib/types/withdraw.js
|
|
109
|
+
/**
|
|
110
|
+
* Withdrawal planning: a pure fold from the session log and the live surface
|
|
111
|
+
* to the replacement intent. Kept free of cordis and Session internals so the
|
|
112
|
+
* unit tests drive it directly; the service only validates and appends.
|
|
113
|
+
* @module @khorsheed/dsh-client-message-tools/withdraw
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Whether a target seq names an editable user message: an append-surface
|
|
117
|
+
* user-source message (an original), a message-tools restore replay (a
|
|
118
|
+
* restored row withdraws and edits like the message it replays), or a
|
|
119
|
+
* message-tools edit replacement (edit chains — the previous edit's
|
|
120
|
+
* replacement is the surface node). Assistant-text restore replays carry no
|
|
121
|
+
* actions and are excluded.
|
|
122
|
+
*/
|
|
123
|
+
function isEditableTarget(event) {
|
|
124
|
+
if (event === void 0 || event.type !== "user/message") return false;
|
|
125
|
+
if (isAppendSurfaceEvent(event)) return event.data.source.kind === "user" || isMessageToolsRestore(event);
|
|
126
|
+
return isMessageToolsEdit(event);
|
|
127
|
+
}
|
|
128
|
+
/** The span [target, surface tail] with full provenance, or undefined when off-surface. */
|
|
129
|
+
function surfaceSpan(surfaceNodes, targetSeq) {
|
|
130
|
+
const startIndex = surfaceNodes.indexOf(targetSeq);
|
|
131
|
+
if (startIndex === -1) return void 0;
|
|
132
|
+
const sourceEventSeqs = surfaceNodes.slice(startIndex);
|
|
133
|
+
const end = sourceEventSeqs[sourceEventSeqs.length - 1];
|
|
134
|
+
/* v8 ignore next -- the slice is non-empty by construction (startIndex names a member) */
|
|
135
|
+
if (end === void 0) return void 0;
|
|
136
|
+
return {
|
|
137
|
+
start: targetSeq,
|
|
138
|
+
end,
|
|
139
|
+
sourceEventSeqs
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Plan a withdrawal: the target must be an editable user message (original,
|
|
144
|
+
* restore replay, or edit replacement — see `isEditableTarget`) still present
|
|
145
|
+
* on the live surface; the span covers it and every surface node after it.
|
|
146
|
+
* @param events - the full session log (seq === index).
|
|
147
|
+
* @param surfaceNodes - the current model-visible surface seqs, in order.
|
|
148
|
+
* @param targetSeq - seq of the user message to withdraw.
|
|
149
|
+
* @returns the replacement intent, or the failure code.
|
|
150
|
+
*/
|
|
151
|
+
function planWithdrawal(events, surfaceNodes, targetSeq) {
|
|
152
|
+
const target = events[targetSeq];
|
|
153
|
+
if (target === void 0) return {
|
|
154
|
+
ok: false,
|
|
155
|
+
code: "target-not-found"
|
|
156
|
+
};
|
|
157
|
+
if (!isEditableTarget(target)) return {
|
|
158
|
+
ok: false,
|
|
159
|
+
code: "not-a-user-message"
|
|
160
|
+
};
|
|
161
|
+
const span = surfaceSpan(surfaceNodes, targetSeq);
|
|
162
|
+
if (span === void 0) return {
|
|
163
|
+
ok: false,
|
|
164
|
+
code: "already-withdrawn"
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
plan: span
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Plan an edit: the target must be an editable user message still present on
|
|
173
|
+
* the live surface, and the new text must be non-blank. The span covers the
|
|
174
|
+
* target and every surface node after it — editing an old message discards
|
|
175
|
+
* what followed it. The replacement's content is the new text (no
|
|
176
|
+
* placeholder), so the model context reads the edited message in place.
|
|
177
|
+
* @param events - the full session log (seq === index).
|
|
178
|
+
* @param surfaceNodes - the current model-visible surface seqs, in order.
|
|
179
|
+
* @param targetSeq - seq of the user message to edit.
|
|
180
|
+
* @param text - the edited text.
|
|
181
|
+
* @returns the replacement intent, or the failure code.
|
|
182
|
+
*/
|
|
183
|
+
function planEdit(events, surfaceNodes, targetSeq, text) {
|
|
184
|
+
if (text.trim() === "") return {
|
|
185
|
+
ok: false,
|
|
186
|
+
code: "empty-text"
|
|
187
|
+
};
|
|
188
|
+
const target = events[targetSeq];
|
|
189
|
+
if (target === void 0) return {
|
|
190
|
+
ok: false,
|
|
191
|
+
code: "target-not-found"
|
|
192
|
+
};
|
|
193
|
+
if (!isEditableTarget(target)) return {
|
|
194
|
+
ok: false,
|
|
195
|
+
code: "not-a-user-message"
|
|
196
|
+
};
|
|
197
|
+
const span = surfaceSpan(surfaceNodes, targetSeq);
|
|
198
|
+
if (span === void 0) return {
|
|
199
|
+
ok: false,
|
|
200
|
+
code: "already-withdrawn"
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
plan: span
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Join the text blocks of one message's content into one plain string. */
|
|
208
|
+
function joinText(blocks) {
|
|
209
|
+
return blocks.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Join an assistant message's visible text. Normal replies use text blocks;
|
|
213
|
+
* interrupted replies may contain only reasoning. Prefer text when present,
|
|
214
|
+
* otherwise preserve the reasoning so a restore does not drop the only
|
|
215
|
+
* assistant content the user saw.
|
|
216
|
+
*/
|
|
217
|
+
function joinAssistantMessageText(blocks) {
|
|
218
|
+
const text = joinText(blocks);
|
|
219
|
+
if (text !== "") return text;
|
|
220
|
+
return blocks.filter((block) => block.type === "reasoning" && typeof block.text === "string").map((block) => block.text).join("");
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The full log seqs shadowed by the latest message-tools withdrawal
|
|
224
|
+
* replacement citing `targetSeq`, or undefined when no such replacement exists
|
|
225
|
+
* (the message left the surface through another producer, e.g. compaction).
|
|
226
|
+
*
|
|
227
|
+
* `sourceEventSeqs` is authoritative for the shadowed *surface* nodes, but an
|
|
228
|
+
* interrupted assistant step may exist only as `assistant/chunk` events (no
|
|
229
|
+
* `assistant/message` ever landed). Those chunks are not surface nodes, so
|
|
230
|
+
* they are absent from `sourceEventSeqs`. The replacement's own `start` and
|
|
231
|
+
* `seq` delimit the whole withdrawn log interval — `[start, seq)` — and are
|
|
232
|
+
* still read from the replacement itself, never guessed from another producer.
|
|
233
|
+
*/
|
|
234
|
+
function findWithdrawnSpan(events, targetSeq) {
|
|
235
|
+
for (let index = events.length - 1; index > targetSeq; index--) {
|
|
236
|
+
const event = events[index];
|
|
237
|
+
if (event !== void 0 && isMessageToolsReplacement(event) && event.sourceEventSeqs?.includes(targetSeq)) {
|
|
238
|
+
const span = [];
|
|
239
|
+
for (let seq = event.surfaceOp.start; seq < event.seq; seq++) span.push(seq);
|
|
240
|
+
return span;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Fold one withdrawn span into its replay entries, in span (original) order.
|
|
246
|
+
* Replayed: user-source messages, edit replacements (their content is the
|
|
247
|
+
* last edit's new text), earlier restore replays, and assistant text (framed;
|
|
248
|
+
* restore-assistant replays already carry the frame). Skipped: withdrawal
|
|
249
|
+
* placeholders, edit triggers, other plugin context, and tool calls/results.
|
|
250
|
+
*
|
|
251
|
+
* Assistant text normally comes from `assistant/message` surface events. A
|
|
252
|
+
* turn that was interrupted before finalization may never emit
|
|
253
|
+
* `assistant/message`; its visible content survives only as `assistant/chunk`
|
|
254
|
+
* rows. Those chunks are folded into one assistant replay per step when no
|
|
255
|
+
* final `assistant/message` exists for that step, so withdrawing/restoring an
|
|
256
|
+
* interrupted reply does not lose the partial assistant content.
|
|
257
|
+
*/
|
|
258
|
+
function replayEntries(events, spanSeqs) {
|
|
259
|
+
const entries = [];
|
|
260
|
+
let pending;
|
|
261
|
+
const finalizedSteps = /* @__PURE__ */ new Set();
|
|
262
|
+
const flushPending = () => {
|
|
263
|
+
if (pending === void 0) return;
|
|
264
|
+
if (!pending.finalized) {
|
|
265
|
+
const text = [...pending.text.values()].join("");
|
|
266
|
+
const reasoning = [...pending.reasoning.values()].join("");
|
|
267
|
+
const replay = text !== "" ? text : reasoning;
|
|
268
|
+
if (replay !== "") entries.push({
|
|
269
|
+
role: "assistant",
|
|
270
|
+
text: `${RESTORED_ASSISTANT_NOTICE}\n${replay}`,
|
|
271
|
+
sourceSeq: pending.sourceSeq
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
pending = void 0;
|
|
275
|
+
};
|
|
276
|
+
const pushChunk = (event) => {
|
|
277
|
+
if (finalizedSteps.has(`${event.data.turn}:${event.data.step}`)) return;
|
|
278
|
+
if (pending === void 0 || pending.turn !== event.data.turn || pending.step !== event.data.step) {
|
|
279
|
+
flushPending();
|
|
280
|
+
pending = {
|
|
281
|
+
turn: event.data.turn,
|
|
282
|
+
step: event.data.step,
|
|
283
|
+
sourceSeq: event.seq,
|
|
284
|
+
text: /* @__PURE__ */ new Map(),
|
|
285
|
+
reasoning: /* @__PURE__ */ new Map(),
|
|
286
|
+
finalized: false
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const chunk = event.data.chunk;
|
|
290
|
+
if (chunk.type === "text-delta") pending.text.set(chunk.index, (pending.text.get(chunk.index) ?? "") + chunk.text);
|
|
291
|
+
else if (chunk.type === "block-end" && chunk.block.type === "text") pending.text.set(chunk.index, chunk.block.text);
|
|
292
|
+
else if (chunk.type === "reasoning-delta") pending.reasoning.set(chunk.index, (pending.reasoning.get(chunk.index) ?? "") + chunk.text);
|
|
293
|
+
else if (chunk.type === "block-end" && chunk.block.type === "reasoning") pending.reasoning.set(chunk.index, chunk.block.text);
|
|
294
|
+
};
|
|
295
|
+
const pushAssistantText = (text, sourceSeq) => {
|
|
296
|
+
if (text !== "") entries.push({
|
|
297
|
+
role: "assistant",
|
|
298
|
+
text: `${RESTORED_ASSISTANT_NOTICE}\n${text}`,
|
|
299
|
+
sourceSeq
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
for (const seq of spanSeqs) {
|
|
303
|
+
const event = events[seq];
|
|
304
|
+
if (event === void 0) continue;
|
|
305
|
+
if (isMessageToolsEdit(event)) {
|
|
306
|
+
flushPending();
|
|
307
|
+
entries.push({
|
|
308
|
+
role: "user",
|
|
309
|
+
content: event.data.content,
|
|
310
|
+
sourceSeq: seq
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (event.type === "assistant/chunk") {
|
|
315
|
+
pushChunk(event);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (event.type === "user/message") {
|
|
319
|
+
flushPending();
|
|
320
|
+
if (event.surfaceOp !== "append") continue;
|
|
321
|
+
if (event.data.source.kind === "user" || isMessageToolsRestore(event)) entries.push({
|
|
322
|
+
role: "user",
|
|
323
|
+
content: event.data.content,
|
|
324
|
+
sourceSeq: seq
|
|
325
|
+
});
|
|
326
|
+
else if (isMessageToolsRestoreAssistant(event)) entries.push({
|
|
327
|
+
role: "assistant",
|
|
328
|
+
text: joinText(event.data.content),
|
|
329
|
+
sourceSeq: seq
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (event.type === "assistant/message") {
|
|
334
|
+
finalizedSteps.add(`${event.data.turn}:${event.data.step}`);
|
|
335
|
+
if (pending !== void 0 && pending.turn === event.data.turn && pending.step === event.data.step) pending.finalized = true;
|
|
336
|
+
flushPending();
|
|
337
|
+
pushAssistantText(joinAssistantMessageText(event.data.message.content), seq);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
flushPending();
|
|
342
|
+
return entries;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Plan a restore: the target must be an editable user message (the same
|
|
346
|
+
* vocabulary as withdrawal — original, restore replay, or edit replacement)
|
|
347
|
+
* that is no longer on the live surface (i.e. withdrawn). Restoring an edit
|
|
348
|
+
* replacement replays the span's last edit text — the replacement's content
|
|
349
|
+
* IS the new text. The surface model is positional — a replaced span folds
|
|
350
|
+
* into exactly one node — so the withdrawn span can never be put back in
|
|
351
|
+
* place; restore replays the span's replayable content (user messages, the
|
|
352
|
+
* last edit's new text, assistant text) as fresh tail messages in original
|
|
353
|
+
* order. When no message-tools replacement cites the target (a foreign
|
|
354
|
+
* shadowing, e.g. compaction), only the target message replays — the span
|
|
355
|
+
* boundary is never re-guessed.
|
|
356
|
+
* @param events - the full session log (seq === index).
|
|
357
|
+
* @param surfaceNodes - the current model-visible surface seqs.
|
|
358
|
+
* @param targetSeq - seq of the withdrawn user message to restore.
|
|
359
|
+
* @returns the restore intent, or the failure code.
|
|
360
|
+
*/
|
|
361
|
+
function planRestore(events, surfaceNodes, targetSeq) {
|
|
362
|
+
const target = events[targetSeq];
|
|
363
|
+
if (target === void 0) return {
|
|
364
|
+
ok: false,
|
|
365
|
+
code: "target-not-found"
|
|
366
|
+
};
|
|
367
|
+
if (!isEditableTarget(target)) return {
|
|
368
|
+
ok: false,
|
|
369
|
+
code: "not-a-user-message"
|
|
370
|
+
};
|
|
371
|
+
if (surfaceNodes.includes(targetSeq)) return {
|
|
372
|
+
ok: false,
|
|
373
|
+
code: "not-withdrawn"
|
|
374
|
+
};
|
|
375
|
+
const span = findWithdrawnSpan(events, targetSeq);
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
plan: { entries: span === void 0 ? [{
|
|
379
|
+
role: "user",
|
|
380
|
+
content: target.data.content,
|
|
381
|
+
sourceSeq: targetSeq
|
|
382
|
+
}] : replayEntries(events, span) }
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region lib/types/index.js
|
|
387
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
388
|
+
var useValue = arguments.length > 2;
|
|
389
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
390
|
+
return useValue ? value : void 0;
|
|
391
|
+
};
|
|
392
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
393
|
+
function accept(f) {
|
|
394
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
395
|
+
return f;
|
|
396
|
+
}
|
|
397
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
398
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
399
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
400
|
+
var _, done = false;
|
|
401
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
402
|
+
var context = {};
|
|
403
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
404
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
405
|
+
context.addInitializer = function(f) {
|
|
406
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
407
|
+
extraInitializers.push(accept(f || null));
|
|
408
|
+
};
|
|
409
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
410
|
+
get: descriptor.get,
|
|
411
|
+
set: descriptor.set
|
|
412
|
+
} : descriptor[key], context);
|
|
413
|
+
if (kind === "accessor") {
|
|
414
|
+
if (result === void 0) continue;
|
|
415
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
416
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
417
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
418
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
419
|
+
} else if (_ = accept(result)) {
|
|
420
|
+
if (kind === "field") initializers.unshift(_);
|
|
421
|
+
else descriptor[key] = _;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
425
|
+
done = true;
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* messageTools Remote service: appends the withdrawal replacement to the
|
|
429
|
+
* owning Session and flushes it durable. The replacement event's
|
|
430
|
+
* plugin-tagged source is the audit trail; no new session event type is
|
|
431
|
+
* introduced, so session reload stays safe for harnesses without this plugin.
|
|
432
|
+
*/
|
|
433
|
+
let MessageToolsService = (() => {
|
|
434
|
+
let _classSuper = TypertRemoteService;
|
|
435
|
+
let _instanceExtraInitializers = [];
|
|
436
|
+
let _withdraw_decorators;
|
|
437
|
+
let _restore_decorators;
|
|
438
|
+
let _edit_decorators;
|
|
439
|
+
return class MessageToolsService extends _classSuper {
|
|
440
|
+
static {
|
|
441
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
442
|
+
_withdraw_decorators = [Remote("withdraw")];
|
|
443
|
+
_restore_decorators = [Remote("restore")];
|
|
444
|
+
_edit_decorators = [Remote("edit")];
|
|
445
|
+
__esDecorate(this, null, _withdraw_decorators, {
|
|
446
|
+
kind: "method",
|
|
447
|
+
name: "withdraw",
|
|
448
|
+
static: false,
|
|
449
|
+
private: false,
|
|
450
|
+
access: {
|
|
451
|
+
has: (obj) => "withdraw" in obj,
|
|
452
|
+
get: (obj) => obj.withdraw
|
|
453
|
+
},
|
|
454
|
+
metadata: _metadata
|
|
455
|
+
}, null, _instanceExtraInitializers);
|
|
456
|
+
__esDecorate(this, null, _restore_decorators, {
|
|
457
|
+
kind: "method",
|
|
458
|
+
name: "restore",
|
|
459
|
+
static: false,
|
|
460
|
+
private: false,
|
|
461
|
+
access: {
|
|
462
|
+
has: (obj) => "restore" in obj,
|
|
463
|
+
get: (obj) => obj.restore
|
|
464
|
+
},
|
|
465
|
+
metadata: _metadata
|
|
466
|
+
}, null, _instanceExtraInitializers);
|
|
467
|
+
__esDecorate(this, null, _edit_decorators, {
|
|
468
|
+
kind: "method",
|
|
469
|
+
name: "edit",
|
|
470
|
+
static: false,
|
|
471
|
+
private: false,
|
|
472
|
+
access: {
|
|
473
|
+
has: (obj) => "edit" in obj,
|
|
474
|
+
get: (obj) => obj.edit
|
|
475
|
+
},
|
|
476
|
+
metadata: _metadata
|
|
477
|
+
}, null, _instanceExtraInitializers);
|
|
478
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
479
|
+
enumerable: true,
|
|
480
|
+
configurable: true,
|
|
481
|
+
writable: true,
|
|
482
|
+
value: _metadata
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
static inject = ["sessions", "agents"];
|
|
486
|
+
/**
|
|
487
|
+
* @param ctx - host context carrying the session store.
|
|
488
|
+
*/
|
|
489
|
+
constructor(ctx) {
|
|
490
|
+
super(ctx, "messageTools");
|
|
491
|
+
__runInitializers(this, _instanceExtraInitializers);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Withdraw a user message: replace it and every surface node after it with
|
|
495
|
+
* a minimal plugin-sourced placeholder, hiding the span from the model.
|
|
496
|
+
* @param request - session identity and target message seq.
|
|
497
|
+
* @returns the replacement receipt, or a rejection from the closed failure union.
|
|
498
|
+
*/
|
|
499
|
+
async withdraw(request) {
|
|
500
|
+
const session = this.ctx.sessions.get(request.sessionId);
|
|
501
|
+
if (session === void 0) return {
|
|
502
|
+
ok: false,
|
|
503
|
+
error: { code: "session-not-found" }
|
|
504
|
+
};
|
|
505
|
+
const planned = planWithdrawal(session.events, session.surface.nodes, request.targetSeq);
|
|
506
|
+
if (!planned.ok) return {
|
|
507
|
+
ok: false,
|
|
508
|
+
error: { code: planned.code }
|
|
509
|
+
};
|
|
510
|
+
const replacement = session.append("user/message", createUserMessage({
|
|
511
|
+
content: [{
|
|
512
|
+
type: "text",
|
|
513
|
+
text: WITHDRAWN_NOTICE
|
|
514
|
+
}],
|
|
515
|
+
source: {
|
|
516
|
+
kind: "plugin",
|
|
517
|
+
plugin: MESSAGE_TOOLS_PLUGIN
|
|
518
|
+
}
|
|
519
|
+
}), {
|
|
520
|
+
surfaceOp: {
|
|
521
|
+
op: "replace",
|
|
522
|
+
start: planned.plan.start,
|
|
523
|
+
end: planned.plan.end
|
|
524
|
+
},
|
|
525
|
+
sourceEventSeqs: [...planned.plan.sourceEventSeqs]
|
|
526
|
+
});
|
|
527
|
+
await this.ctx.sessions.flush(session);
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
value: {
|
|
531
|
+
replacementSeq: replacement.seq,
|
|
532
|
+
shadowedCount: planned.plan.sourceEventSeqs.length
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Restore a withdrawn span: replay every replayable entry of it (user
|
|
538
|
+
* messages verbatim — including the last edit's new text — and framed
|
|
539
|
+
* assistant text; tool calls/results never replay) as fresh plugin-tagged
|
|
540
|
+
* `user/message` appends at the conversation tail, in original order, each
|
|
541
|
+
* citing its original event in `sourceEventSeqs`. The surface replace model
|
|
542
|
+
* is positional — a replaced span folds into one node — so the span cannot
|
|
543
|
+
* be restored in place; the replayed entries are simply the newest messages
|
|
544
|
+
* the model sees. Assistant text replays as framed user-role messages:
|
|
545
|
+
* `assistant/message` cannot carry a plugin source and the turn/step trace
|
|
546
|
+
* forbids assistant appends outside a step.
|
|
547
|
+
* @param request - session identity and withdrawn message seq.
|
|
548
|
+
* @returns the restore receipt, or a rejection from the closed failure union.
|
|
549
|
+
*/
|
|
550
|
+
async restore(request) {
|
|
551
|
+
const session = this.ctx.sessions.get(request.sessionId);
|
|
552
|
+
if (session === void 0) return {
|
|
553
|
+
ok: false,
|
|
554
|
+
error: { code: "session-not-found" }
|
|
555
|
+
};
|
|
556
|
+
const planned = planRestore(session.events, session.surface.nodes, request.targetSeq);
|
|
557
|
+
if (!planned.ok) return {
|
|
558
|
+
ok: false,
|
|
559
|
+
error: { code: planned.code }
|
|
560
|
+
};
|
|
561
|
+
const appendedSeqs = [];
|
|
562
|
+
for (const entry of planned.plan.entries) {
|
|
563
|
+
const message = entry.role === "user" ? createUserMessage({
|
|
564
|
+
content: [...entry.content],
|
|
565
|
+
source: {
|
|
566
|
+
kind: "plugin",
|
|
567
|
+
plugin: MESSAGE_TOOLS_PLUGIN
|
|
568
|
+
}
|
|
569
|
+
}) : createUserMessage({
|
|
570
|
+
content: [{
|
|
571
|
+
type: "text",
|
|
572
|
+
text: entry.text
|
|
573
|
+
}],
|
|
574
|
+
source: restoreAssistantSource()
|
|
575
|
+
});
|
|
576
|
+
appendedSeqs.push(session.append("user/message", message, {
|
|
577
|
+
surfaceOp: "append",
|
|
578
|
+
sourceEventSeqs: [entry.sourceSeq]
|
|
579
|
+
}).seq);
|
|
580
|
+
}
|
|
581
|
+
await this.ctx.sessions.flush(session);
|
|
582
|
+
/* v8 ignore next -- entries is never empty (the target itself always replays), so [0] exists */
|
|
583
|
+
return {
|
|
584
|
+
ok: true,
|
|
585
|
+
value: { restoredSeq: appendedSeqs[0] ?? request.targetSeq }
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Edit a user message in place: replace it and every surface node after it
|
|
590
|
+
* with the edited text itself (no placeholder, no tail duplicate — the
|
|
591
|
+
* edited text appears exactly once, in the replacement event), then start a
|
|
592
|
+
* regeneration turn. The harness has no wake-without-append path, so the
|
|
593
|
+
* turn is started by `agent.followup` carrying a minimal plugin-sourced
|
|
594
|
+
* trigger message (the ankh-guard/schedule pattern); the trigger is not the
|
|
595
|
+
* edited text and lands once, as itself.
|
|
596
|
+
* @param request - session identity, target seq, edited text.
|
|
597
|
+
* @returns the replacement receipt and whether the turn was triggered, or a rejection.
|
|
598
|
+
*/
|
|
599
|
+
async edit(request) {
|
|
600
|
+
const session = this.ctx.sessions.get(request.sessionId);
|
|
601
|
+
if (session === void 0) return {
|
|
602
|
+
ok: false,
|
|
603
|
+
error: { code: "session-not-found" }
|
|
604
|
+
};
|
|
605
|
+
const planned = planEdit(session.events, session.surface.nodes, request.targetSeq, request.text);
|
|
606
|
+
if (!planned.ok) return {
|
|
607
|
+
ok: false,
|
|
608
|
+
error: { code: planned.code }
|
|
609
|
+
};
|
|
610
|
+
const replacement = session.append("user/message", createUserMessage({
|
|
611
|
+
content: [{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: request.text
|
|
614
|
+
}],
|
|
615
|
+
source: editReplacementSource()
|
|
616
|
+
}), {
|
|
617
|
+
surfaceOp: {
|
|
618
|
+
op: "replace",
|
|
619
|
+
start: planned.plan.start,
|
|
620
|
+
end: planned.plan.end
|
|
621
|
+
},
|
|
622
|
+
sourceEventSeqs: [...planned.plan.sourceEventSeqs]
|
|
623
|
+
});
|
|
624
|
+
await this.ctx.sessions.flush(session);
|
|
625
|
+
const agent = this.ctx.agents.get(request.sessionId);
|
|
626
|
+
if (agent !== void 0) agent.followup(createUserMessage({
|
|
627
|
+
content: [{
|
|
628
|
+
type: "text",
|
|
629
|
+
text: EDIT_TRIGGER_NOTICE
|
|
630
|
+
}],
|
|
631
|
+
source: editTriggerSource()
|
|
632
|
+
}));
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
value: {
|
|
636
|
+
replacementSeq: replacement.seq,
|
|
637
|
+
triggered: agent !== void 0
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
})();
|
|
643
|
+
//#endregion
|
|
644
|
+
export { MessageToolsService, MessageToolsService as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@khorsheed/dsh-client-message-tools`.
|
|
4
|
+
* @module @khorsheed/dsh-client-message-tools/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@khorsheed/dsh-client-message-tools";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "client-message-tools-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: the withdrawal write passes through Session.append,
|
|
13
|
+
* whose surface validation rejects any replacement that misses a shadowed
|
|
14
|
+
* surface node, and the client contributions (one Definition, two slot
|
|
15
|
+
* entries) ride the slot/declaration lifecycles disposed with the plugin
|
|
16
|
+
* fiber. The package's unit tests exercise the plan fold and the projection
|
|
17
|
+
* helpers, so no second authority exists to check at runtime.
|
|
18
|
+
*/
|
|
19
|
+
const install = () => {};
|
|
20
|
+
/**
|
|
21
|
+
* Register this package's invariant companion.
|
|
22
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
23
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
24
|
+
*/
|
|
25
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
26
|
+
//#endregion
|
|
27
|
+
export { apply, inject, name };
|