@mengine/medeo-tool 1.0.1-alpha.2 → 1.2.1-alpha.7
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 +36 -6
- package/dist/entity-contract-B3txrzTt.d.mts +174 -0
- package/dist/index.d.mts +104 -17
- package/dist/index.mjs +1544 -123
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +250 -4
- package/dist/script-session-BF44uKv_.mjs +1501 -0
- package/dist/script-session-BF44uKv_.mjs.map +1 -0
- package/dist/worker-entry.d.mts +2 -0
- package/dist/worker-entry.mjs +23 -4
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/script-session-B8fc9Ccb.mjs +0 -439
- package/dist/script-session-B8fc9Ccb.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as renderCompactProjection, n as collectAffectedPartIds, r as renderPreview, t as EditSandboxSession } from "./script-session-
|
|
2
|
-
import {
|
|
1
|
+
import { i as renderCompactProjection, n as collectAffectedPartIds, r as renderPreview, t as EditSandboxSession } from "./script-session-BF44uKv_.mjs";
|
|
2
|
+
import { ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, replayJournal, toVideoDocument } from "@mengine/medeo-client";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
//#region src/sandbox/node-host.ts
|
|
@@ -23,6 +23,7 @@ function runEditScript(options) {
|
|
|
23
23
|
const workerEntryUrl = options.workerEntryUrl ?? sourceSibling("worker-entry");
|
|
24
24
|
const resolveRegisterUrl = sourceSibling("node-esm-resolve-register");
|
|
25
25
|
const ops = [];
|
|
26
|
+
const entityCommands = [];
|
|
26
27
|
const logs = [];
|
|
27
28
|
return new Promise((resolve) => {
|
|
28
29
|
let settled = false;
|
|
@@ -33,6 +34,7 @@ function runEditScript(options) {
|
|
|
33
34
|
document: options.document,
|
|
34
35
|
script: options.script,
|
|
35
36
|
inputs: options.inputs,
|
|
37
|
+
entityState: options.entityState,
|
|
36
38
|
idLabel: options.idLabel
|
|
37
39
|
},
|
|
38
40
|
execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
|
|
@@ -54,6 +56,7 @@ function runEditScript(options) {
|
|
|
54
56
|
error: { message: `edit script exceeded timeout of ${timeoutMs}ms` },
|
|
55
57
|
partial: {
|
|
56
58
|
ops: ops.slice(),
|
|
59
|
+
entityCommands: entityCommands.slice(),
|
|
57
60
|
logs: logs.slice()
|
|
58
61
|
}
|
|
59
62
|
});
|
|
@@ -81,6 +84,10 @@ function runEditScript(options) {
|
|
|
81
84
|
ops.push(message.entry);
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
87
|
+
if (message.t === "entity-entry") {
|
|
88
|
+
entityCommands.push(message.command);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
84
91
|
if (message.t === "log") {
|
|
85
92
|
logs.push(message.line);
|
|
86
93
|
return;
|
|
@@ -89,14 +96,19 @@ function runEditScript(options) {
|
|
|
89
96
|
ops.length = Math.max(0, Math.min(message.index, ops.length));
|
|
90
97
|
return;
|
|
91
98
|
}
|
|
99
|
+
if (message.t === "entity-truncate") {
|
|
100
|
+
entityCommands.length = Math.max(0, Math.min(message.index, entityCommands.length));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
92
103
|
if (message.t === "done") {
|
|
93
|
-
if (message.opsCount !== ops.length) {
|
|
104
|
+
if (message.opsCount !== ops.length || message.entityCommandsCount !== entityCommands.length) {
|
|
94
105
|
finish({
|
|
95
106
|
ok: false,
|
|
96
107
|
phase: "runtime",
|
|
97
|
-
error: { message: `
|
|
108
|
+
error: { message: `journal count mismatch: worker reported timeline=${message.opsCount}, entities=${message.entityCommandsCount}; host collected timeline=${ops.length}, entities=${entityCommands.length}` },
|
|
98
109
|
partial: {
|
|
99
110
|
ops: ops.slice(),
|
|
111
|
+
entityCommands: entityCommands.slice(),
|
|
100
112
|
logs: logs.slice()
|
|
101
113
|
}
|
|
102
114
|
});
|
|
@@ -105,9 +117,13 @@ function runEditScript(options) {
|
|
|
105
117
|
finish({
|
|
106
118
|
ok: true,
|
|
107
119
|
plan: {
|
|
120
|
+
plan_kind: message.planKind,
|
|
108
121
|
doc_id: options.document.meta.draft_id ?? "",
|
|
109
122
|
base_version: options.baseVersion,
|
|
110
123
|
ops: ops.slice(),
|
|
124
|
+
entity_base_revision: message.entityBaseRevision,
|
|
125
|
+
entity_commands: entityCommands.slice(),
|
|
126
|
+
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
111
127
|
preview: message.preview,
|
|
112
128
|
logs: logs.slice()
|
|
113
129
|
},
|
|
@@ -121,6 +137,7 @@ function runEditScript(options) {
|
|
|
121
137
|
error: message.error,
|
|
122
138
|
partial: {
|
|
123
139
|
ops: ops.slice(),
|
|
140
|
+
entityCommands: entityCommands.slice(),
|
|
124
141
|
logs: logs.slice()
|
|
125
142
|
}
|
|
126
143
|
});
|
|
@@ -137,6 +154,7 @@ function runEditScript(options) {
|
|
|
137
154
|
},
|
|
138
155
|
partial: {
|
|
139
156
|
ops: ops.slice(),
|
|
157
|
+
entityCommands: entityCommands.slice(),
|
|
140
158
|
logs: logs.slice()
|
|
141
159
|
}
|
|
142
160
|
});
|
|
@@ -150,6 +168,7 @@ function runEditScript(options) {
|
|
|
150
168
|
error: { message: `worker exited with code ${code ?? "null"} before completion` },
|
|
151
169
|
partial: {
|
|
152
170
|
ops: ops.slice(),
|
|
171
|
+
entityCommands: entityCommands.slice(),
|
|
153
172
|
logs: logs.slice()
|
|
154
173
|
}
|
|
155
174
|
});
|
|
@@ -157,17 +176,1152 @@ function runEditScript(options) {
|
|
|
157
176
|
});
|
|
158
177
|
}
|
|
159
178
|
//#endregion
|
|
179
|
+
//#region src/entity/entity-contract.ts
|
|
180
|
+
const KNOWN_ENTITY_KINDS = [
|
|
181
|
+
"axvideo",
|
|
182
|
+
"timeline",
|
|
183
|
+
"track",
|
|
184
|
+
"clip",
|
|
185
|
+
"asset",
|
|
186
|
+
"video",
|
|
187
|
+
"audio",
|
|
188
|
+
"voice",
|
|
189
|
+
"image",
|
|
190
|
+
"sequence-marker",
|
|
191
|
+
"viewport",
|
|
192
|
+
"audio-script",
|
|
193
|
+
"phonetic-script",
|
|
194
|
+
"caption"
|
|
195
|
+
];
|
|
196
|
+
const KNOWN_RELATION_KINDS = [
|
|
197
|
+
"timeline-track",
|
|
198
|
+
"track-clip",
|
|
199
|
+
"clip-marker",
|
|
200
|
+
"marker-content",
|
|
201
|
+
"axvideo-marker",
|
|
202
|
+
"marker-timeline",
|
|
203
|
+
"physical-asset",
|
|
204
|
+
"generated",
|
|
205
|
+
"phonetic-script-provenance",
|
|
206
|
+
"caption-provenance",
|
|
207
|
+
"caption-alignment"
|
|
208
|
+
];
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/entity/entity-http-client.ts
|
|
211
|
+
const API_PREFIX = "/api/mengine/v1";
|
|
212
|
+
const entityKinds = new Set(KNOWN_ENTITY_KINDS);
|
|
213
|
+
const relationKinds = new Set(KNOWN_RELATION_KINDS);
|
|
214
|
+
var MengineEntityHttpRequestError = class extends Error {
|
|
215
|
+
status;
|
|
216
|
+
payload;
|
|
217
|
+
constructor(status, payload) {
|
|
218
|
+
super(`mengine entity-state request failed: ${status}`);
|
|
219
|
+
this.status = status;
|
|
220
|
+
this.payload = payload;
|
|
221
|
+
this.name = "MengineEntityHttpRequestError";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
/** Narrow authenticated client for the entity-store CAS endpoint. */
|
|
225
|
+
var EntityHttpClient = class {
|
|
226
|
+
options;
|
|
227
|
+
fetchImpl;
|
|
228
|
+
constructor(options) {
|
|
229
|
+
this.options = options;
|
|
230
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
231
|
+
}
|
|
232
|
+
async fetchState() {
|
|
233
|
+
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
234
|
+
}
|
|
235
|
+
async commit(expectedRevision, state) {
|
|
236
|
+
return toSnapshot(await this.requestJson({
|
|
237
|
+
method: "POST",
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
expected_revision: expectedRevision,
|
|
240
|
+
rows: {
|
|
241
|
+
entities: state.entities,
|
|
242
|
+
relations: state.relations
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
}), this.options.docId);
|
|
246
|
+
}
|
|
247
|
+
async requestJson(init) {
|
|
248
|
+
const response = await this.fetchImpl(this.endpoint(), {
|
|
249
|
+
...init,
|
|
250
|
+
headers: this.headers()
|
|
251
|
+
});
|
|
252
|
+
const payload = await safeReadJson(response);
|
|
253
|
+
if (!response.ok) throw new MengineEntityHttpRequestError(response.status, payload);
|
|
254
|
+
return payload;
|
|
255
|
+
}
|
|
256
|
+
headers() {
|
|
257
|
+
const headers = new Headers({
|
|
258
|
+
accept: "application/json",
|
|
259
|
+
"content-type": "application/json"
|
|
260
|
+
});
|
|
261
|
+
const authToken = typeof this.options.authToken === "function" ? this.options.authToken() : this.options.authToken;
|
|
262
|
+
if (authToken != null && authToken !== "") headers.set("authorization", `Bearer ${authToken}`);
|
|
263
|
+
const userId = typeof this.options.userId === "function" ? this.options.userId() : this.options.userId;
|
|
264
|
+
if (userId != null && userId !== "") headers.set("medeo-user-id", userId);
|
|
265
|
+
return headers;
|
|
266
|
+
}
|
|
267
|
+
endpoint() {
|
|
268
|
+
return `${this.options.httpOrigin.replace(/\/$/, "")}${API_PREFIX}/docs/${encodeURIComponent(this.options.docId)}/entity-state`;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function toSnapshot(value, expectedDocId) {
|
|
272
|
+
if (!isRecord$1(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
273
|
+
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
274
|
+
if (!isRecord$1(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
275
|
+
const response = value;
|
|
276
|
+
return {
|
|
277
|
+
revision: response.revision,
|
|
278
|
+
entities: response.rows.entities.map(parseEntity),
|
|
279
|
+
relations: response.rows.relations.map(parseRelation)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function parseEntity(value) {
|
|
283
|
+
if (!isRecord$1(value) || !isTrimmed(value.entity_id) || typeof value.entity_kind !== "string" || !entityKinds.has(value.entity_kind) || !isJsonObject(value.payload)) throw new Error("invalid Entity row in entity-state response");
|
|
284
|
+
return structuredClone(value);
|
|
285
|
+
}
|
|
286
|
+
function parseRelation(value) {
|
|
287
|
+
if (!isRecord$1(value) || !isTrimmed(value.relation_id) || typeof value.relation_kind !== "string" || !relationKinds.has(value.relation_kind) || !isTrimmed(value.endpoint_0_entity_id) || !isTrimmed(value.endpoint_1_entity_id) || !isJsonObject(value.metadata) || !isJsonObject(value.trace)) throw new Error("invalid Relation row in entity-state response");
|
|
288
|
+
return structuredClone(value);
|
|
289
|
+
}
|
|
290
|
+
function isJsonObject(value) {
|
|
291
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$1(value);
|
|
292
|
+
}
|
|
293
|
+
function isJsonValue(value, ancestors) {
|
|
294
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
295
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
296
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
297
|
+
const prototype = Object.getPrototypeOf(value);
|
|
298
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
299
|
+
ancestors.add(value);
|
|
300
|
+
const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
301
|
+
ancestors.delete(value);
|
|
302
|
+
return valid;
|
|
303
|
+
}
|
|
304
|
+
function isRecord$1(value) {
|
|
305
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
306
|
+
}
|
|
307
|
+
function isTrimmed(value) {
|
|
308
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
309
|
+
}
|
|
310
|
+
function isNonNegativeInteger(value) {
|
|
311
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
312
|
+
}
|
|
313
|
+
async function safeReadJson(response) {
|
|
314
|
+
const text = await response.text();
|
|
315
|
+
if (text.length === 0) return null;
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(text);
|
|
318
|
+
} catch {
|
|
319
|
+
return text;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/sandbox/generated/edit-sandbox-model-context.ts
|
|
324
|
+
/**
|
|
325
|
+
* @generated by gen:sandbox-dts — DO NOT EDIT MANUALLY
|
|
326
|
+
*
|
|
327
|
+
* Runtime copy of the sandbox TypeScript disclosure. The model prompt imports
|
|
328
|
+
* this value so its interface and the checked-in declaration cannot drift.
|
|
329
|
+
*/
|
|
330
|
+
const EDIT_SANDBOX_API_DTS = [
|
|
331
|
+
"/**",
|
|
332
|
+
" * @generated by gen:sandbox-dts — DO NOT EDIT MANUALLY",
|
|
333
|
+
" *",
|
|
334
|
+
" * Schema version: video-document/v0",
|
|
335
|
+
" * Semantic ops: 20",
|
|
336
|
+
" *",
|
|
337
|
+
" * Boundary: zod `superRefine` / custom refine rules are NOT introspectable and",
|
|
338
|
+
" * do not appear here. Business mutual-exclusion rules surface via runtime",
|
|
339
|
+
" * validation errors (L3 feedback channel).",
|
|
340
|
+
" *",
|
|
341
|
+
" * @example 读取→计算→批量写",
|
|
342
|
+
" * ```ts",
|
|
343
|
+
" * const clips = timeline.clipsInRange(0, 10_000);",
|
|
344
|
+
" * await edit.setVideoClipSpeedShift({",
|
|
345
|
+
" * clips: clips.map((c) => ({ clip_id: c.id, speed_shift: { category: 'linear', mode: 'constant', config: { linear: { speed: 1.5 } } } })),",
|
|
346
|
+
" * });",
|
|
347
|
+
" * ```",
|
|
348
|
+
" *",
|
|
349
|
+
" * @example anchored 删除",
|
|
350
|
+
" * ```ts",
|
|
351
|
+
" * await edit.deleteVideoClips({ clip_ids: ['clip_a'], on_anchored: 'detach' });",
|
|
352
|
+
" * ```",
|
|
353
|
+
" */",
|
|
354
|
+
"",
|
|
355
|
+
"/**",
|
|
356
|
+
" * A clip's playback-speed fact, the only thing `SetVideoClipSpeedShift` writes.",
|
|
357
|
+
" */",
|
|
358
|
+
"export interface SpeedShift {",
|
|
359
|
+
" category: 'linear' | 'curve';",
|
|
360
|
+
" mode: string;",
|
|
361
|
+
" config:",
|
|
362
|
+
" | {",
|
|
363
|
+
" linear: {",
|
|
364
|
+
" /**",
|
|
365
|
+
" * @constraint positive",
|
|
366
|
+
" */",
|
|
367
|
+
" speed: number;",
|
|
368
|
+
" };",
|
|
369
|
+
" }",
|
|
370
|
+
" | {",
|
|
371
|
+
" curve: {",
|
|
372
|
+
" /**",
|
|
373
|
+
" * @constraint minLength(2)",
|
|
374
|
+
" */",
|
|
375
|
+
" keyframes: {",
|
|
376
|
+
" /**",
|
|
377
|
+
" * @constraint min(0)",
|
|
378
|
+
" * @constraint max(1)",
|
|
379
|
+
" */",
|
|
380
|
+
" position: number;",
|
|
381
|
+
" /**",
|
|
382
|
+
" * @constraint min(0)",
|
|
383
|
+
" */",
|
|
384
|
+
" rate: number;",
|
|
385
|
+
" /**",
|
|
386
|
+
" * Bezier tangent handle (x, y)",
|
|
387
|
+
" */",
|
|
388
|
+
" in_tangent?: { x: number; y: number };",
|
|
389
|
+
" /**",
|
|
390
|
+
" * Bezier tangent handle (x, y)",
|
|
391
|
+
" */",
|
|
392
|
+
" out_tangent?: { x: number; y: number };",
|
|
393
|
+
" }[];",
|
|
394
|
+
" };",
|
|
395
|
+
" };",
|
|
396
|
+
"}",
|
|
397
|
+
"",
|
|
398
|
+
"/**",
|
|
399
|
+
" * TTS voice summary attached to a speech",
|
|
400
|
+
" */",
|
|
401
|
+
"export interface Voice {",
|
|
402
|
+
" /**",
|
|
403
|
+
" * @constraint minLength(1)",
|
|
404
|
+
" */",
|
|
405
|
+
" id: string;",
|
|
406
|
+
" name: string;",
|
|
407
|
+
"}",
|
|
408
|
+
"",
|
|
409
|
+
"/**",
|
|
410
|
+
" * A materialized speech-subtree write (speeches + their captions).",
|
|
411
|
+
" */",
|
|
412
|
+
"export interface SpeechAssets {",
|
|
413
|
+
" /**",
|
|
414
|
+
" * Materialized speech parts to write",
|
|
415
|
+
" * @constraint minLength(1)",
|
|
416
|
+
" */",
|
|
417
|
+
" speeches: {",
|
|
418
|
+
" /**",
|
|
419
|
+
" * The speech part ID (= side-effect speech_parts[].id)",
|
|
420
|
+
" * @constraint minLength(1)",
|
|
421
|
+
" */",
|
|
422
|
+
" speech_id: string;",
|
|
423
|
+
" /**",
|
|
424
|
+
" * Host video clip part ID the speech anchors to (RFC 02 §4)",
|
|
425
|
+
" * @constraint minLength(1)",
|
|
426
|
+
" */",
|
|
427
|
+
" anchor_part_id: string;",
|
|
428
|
+
" /**",
|
|
429
|
+
" * Offset within the host clip (speech.abs = host.abs + offset_ms)",
|
|
430
|
+
" * @constraint int",
|
|
431
|
+
" * @constraint min(0)",
|
|
432
|
+
" */",
|
|
433
|
+
" offset_ms: number;",
|
|
434
|
+
" /**",
|
|
435
|
+
" * @constraint minLength(1)",
|
|
436
|
+
" */",
|
|
437
|
+
" audio_storage_key: string;",
|
|
438
|
+
" /**",
|
|
439
|
+
" * Duration in milliseconds (> 0)",
|
|
440
|
+
" * @constraint int",
|
|
441
|
+
" * @constraint positive",
|
|
442
|
+
" */",
|
|
443
|
+
" duration_ms: number;",
|
|
444
|
+
" audio_script: string;",
|
|
445
|
+
" /**",
|
|
446
|
+
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
447
|
+
" * @constraint min(-60)",
|
|
448
|
+
" * @constraint max(20)",
|
|
449
|
+
" */",
|
|
450
|
+
" volume: number;",
|
|
451
|
+
" /**",
|
|
452
|
+
" * TTS voice summary attached to a speech",
|
|
453
|
+
" */",
|
|
454
|
+
" voice: {",
|
|
455
|
+
" /**",
|
|
456
|
+
" * @constraint minLength(1)",
|
|
457
|
+
" */",
|
|
458
|
+
" id: string;",
|
|
459
|
+
" name: string;",
|
|
460
|
+
" };",
|
|
461
|
+
" /**",
|
|
462
|
+
" * @constraint minLength(1)",
|
|
463
|
+
" */",
|
|
464
|
+
" origin_speech_id: string;",
|
|
465
|
+
" /**",
|
|
466
|
+
" * Caption part IDs owned by this speech",
|
|
467
|
+
" */",
|
|
468
|
+
" caption_ids: string[];",
|
|
469
|
+
" }[];",
|
|
470
|
+
" /**",
|
|
471
|
+
" * Materialized caption parts owned by the speeches",
|
|
472
|
+
" */",
|
|
473
|
+
" captions: {",
|
|
474
|
+
" /**",
|
|
475
|
+
" * The caption part ID (= side-effect created_caption_parts[].id)",
|
|
476
|
+
" * @constraint minLength(1)",
|
|
477
|
+
" */",
|
|
478
|
+
" caption_id: string;",
|
|
479
|
+
" /**",
|
|
480
|
+
" * The owning speech part ID",
|
|
481
|
+
" * @constraint minLength(1)",
|
|
482
|
+
" */",
|
|
483
|
+
" speech_part_id: string;",
|
|
484
|
+
" text: string;",
|
|
485
|
+
" /**",
|
|
486
|
+
" * Offset within the host speech (caption.abs = speech.abs + start_ms)",
|
|
487
|
+
" * @constraint int",
|
|
488
|
+
" * @constraint min(0)",
|
|
489
|
+
" */",
|
|
490
|
+
" start_ms: number;",
|
|
491
|
+
" /**",
|
|
492
|
+
" * Duration in milliseconds (> 0)",
|
|
493
|
+
" * @constraint int",
|
|
494
|
+
" * @constraint positive",
|
|
495
|
+
" */",
|
|
496
|
+
" duration_ms: number;",
|
|
497
|
+
" }[];",
|
|
498
|
+
"}",
|
|
499
|
+
"",
|
|
500
|
+
"export interface MoveVideoClipsInput {",
|
|
501
|
+
" /**",
|
|
502
|
+
" * List of video clips to move to new positions",
|
|
503
|
+
" * @constraint minLength(1)",
|
|
504
|
+
" */",
|
|
505
|
+
" clips: {",
|
|
506
|
+
" /**",
|
|
507
|
+
" * The video clip part ID to move",
|
|
508
|
+
" * @constraint minLength(1)",
|
|
509
|
+
" */",
|
|
510
|
+
" clip_id: string;",
|
|
511
|
+
" /**",
|
|
512
|
+
" * New absolute start time in milliseconds on the timeline",
|
|
513
|
+
" * @constraint int",
|
|
514
|
+
" * @constraint min(0)",
|
|
515
|
+
" */",
|
|
516
|
+
" new_start_ms: number;",
|
|
517
|
+
" /**",
|
|
518
|
+
" * Target track ID to move the clip to (optional)",
|
|
519
|
+
" * @constraint minLength(1)",
|
|
520
|
+
" */",
|
|
521
|
+
" new_track_id?: string;",
|
|
522
|
+
" }[];",
|
|
523
|
+
"}",
|
|
524
|
+
"",
|
|
525
|
+
"/**",
|
|
526
|
+
" * Reorder a set of main-track clips relative to a reference clip.",
|
|
527
|
+
" */",
|
|
528
|
+
"export interface MoveVideoClipsByAnchorInput {",
|
|
529
|
+
" /**",
|
|
530
|
+
" * Clips to move as one block, keeping their relative order. Need not be contiguous on the track.",
|
|
531
|
+
" * @constraint minLength(1)",
|
|
532
|
+
" */",
|
|
533
|
+
" clip_ids: string[];",
|
|
534
|
+
" /**",
|
|
535
|
+
" * Where the moved block lands: before/after a reference clip, or at the head of the track",
|
|
536
|
+
" */",
|
|
537
|
+
" anchor:",
|
|
538
|
+
" | {",
|
|
539
|
+
" position: 'before';",
|
|
540
|
+
" /**",
|
|
541
|
+
" * The moved block lands immediately before this clip",
|
|
542
|
+
" * @constraint minLength(1)",
|
|
543
|
+
" */",
|
|
544
|
+
" clip_id: string;",
|
|
545
|
+
" }",
|
|
546
|
+
" | {",
|
|
547
|
+
" position: 'after';",
|
|
548
|
+
" /**",
|
|
549
|
+
" * The moved block lands immediately after this clip",
|
|
550
|
+
" * @constraint minLength(1)",
|
|
551
|
+
" */",
|
|
552
|
+
" clip_id: string;",
|
|
553
|
+
" }",
|
|
554
|
+
" | { position: 'track_start' };",
|
|
555
|
+
" /**",
|
|
556
|
+
" * What happens to speeches anchored to the moved clips (required — see the policy doc)",
|
|
557
|
+
" */",
|
|
558
|
+
" on_anchored: 'follow' | 'keep_absolute';",
|
|
559
|
+
"}",
|
|
560
|
+
"",
|
|
561
|
+
"export interface DeleteVideoClipsInput {",
|
|
562
|
+
" /**",
|
|
563
|
+
" * List of video clip part IDs to delete from the main track",
|
|
564
|
+
" * @constraint minLength(1)",
|
|
565
|
+
" */",
|
|
566
|
+
" clip_ids: string[];",
|
|
567
|
+
" /**",
|
|
568
|
+
" * How to treat anchored children (default cascade)",
|
|
569
|
+
" */",
|
|
570
|
+
" on_anchored?: 'cascade' | 'detach';",
|
|
571
|
+
"}",
|
|
572
|
+
"",
|
|
573
|
+
"/**",
|
|
574
|
+
" * Add video clips to a track.",
|
|
575
|
+
" */",
|
|
576
|
+
"export interface AddVideoClipsInput {",
|
|
577
|
+
" /**",
|
|
578
|
+
" * List of video clips to create",
|
|
579
|
+
" * @constraint minLength(1)",
|
|
580
|
+
" */",
|
|
581
|
+
" clips: {",
|
|
582
|
+
" /**",
|
|
583
|
+
" * The media asset ID for the video clip",
|
|
584
|
+
" * @constraint minLength(1)",
|
|
585
|
+
" */",
|
|
586
|
+
" media_id: string;",
|
|
587
|
+
" /**",
|
|
588
|
+
" * Absolute start time in milliseconds on the timeline",
|
|
589
|
+
" * @constraint int",
|
|
590
|
+
" * @constraint min(0)",
|
|
591
|
+
" */",
|
|
592
|
+
" start_ms?: number;",
|
|
593
|
+
" /**",
|
|
594
|
+
" * The source media's intrinsic full length in ms",
|
|
595
|
+
" * @constraint int",
|
|
596
|
+
" * @constraint positive",
|
|
597
|
+
" */",
|
|
598
|
+
" media_duration_ms: number;",
|
|
599
|
+
" /**",
|
|
600
|
+
" * Trim window start in the media (default 0)",
|
|
601
|
+
" * @constraint int",
|
|
602
|
+
" * @constraint min(0)",
|
|
603
|
+
" */",
|
|
604
|
+
" play_in?: number;",
|
|
605
|
+
" /**",
|
|
606
|
+
" * Trim window end in the media (default media_duration_ms)",
|
|
607
|
+
" * @constraint int",
|
|
608
|
+
" * @constraint positive",
|
|
609
|
+
" */",
|
|
610
|
+
" play_out?: number;",
|
|
611
|
+
" /**",
|
|
612
|
+
" * Target track ID (optional, defaults to main track)",
|
|
613
|
+
" * @constraint minLength(1)",
|
|
614
|
+
" */",
|
|
615
|
+
" track_id?: string;",
|
|
616
|
+
" }[];",
|
|
617
|
+
" /**",
|
|
618
|
+
" * Insert new clips before this clip ID",
|
|
619
|
+
" * @constraint minLength(1)",
|
|
620
|
+
" */",
|
|
621
|
+
" before_clip_id?: string;",
|
|
622
|
+
" /**",
|
|
623
|
+
" * Insert new clips after this clip ID",
|
|
624
|
+
" * @constraint minLength(1)",
|
|
625
|
+
" */",
|
|
626
|
+
" after_clip_id?: string;",
|
|
627
|
+
"}",
|
|
628
|
+
"",
|
|
629
|
+
"export interface AdjustVideoClipVolumeInput {",
|
|
630
|
+
" /**",
|
|
631
|
+
" * List of video clips with their new volume settings",
|
|
632
|
+
" * @constraint minLength(1)",
|
|
633
|
+
" */",
|
|
634
|
+
" clips: {",
|
|
635
|
+
" /**",
|
|
636
|
+
" * The video clip part ID to adjust volume for",
|
|
637
|
+
" * @constraint minLength(1)",
|
|
638
|
+
" */",
|
|
639
|
+
" clip_id: string;",
|
|
640
|
+
" /**",
|
|
641
|
+
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
642
|
+
" * @constraint min(-60)",
|
|
643
|
+
" * @constraint max(20)",
|
|
644
|
+
" */",
|
|
645
|
+
" volume: number;",
|
|
646
|
+
" }[];",
|
|
647
|
+
"}",
|
|
648
|
+
"",
|
|
649
|
+
"/**",
|
|
650
|
+
" * Set the playback speed of existing video clips.",
|
|
651
|
+
" */",
|
|
652
|
+
"export interface SetVideoClipSpeedShiftInput {",
|
|
653
|
+
" /**",
|
|
654
|
+
" * Video clips with their new speed settings",
|
|
655
|
+
" * @constraint minLength(1)",
|
|
656
|
+
" */",
|
|
657
|
+
" clips: {",
|
|
658
|
+
" /**",
|
|
659
|
+
" * The video clip part ID to set speed for",
|
|
660
|
+
" * @constraint minLength(1)",
|
|
661
|
+
" */",
|
|
662
|
+
" clip_id: string;",
|
|
663
|
+
" /**",
|
|
664
|
+
" * The new speed setting, or null to reset to 1×",
|
|
665
|
+
" */",
|
|
666
|
+
" speed_shift: {",
|
|
667
|
+
" category: 'linear' | 'curve';",
|
|
668
|
+
" mode: string;",
|
|
669
|
+
" config:",
|
|
670
|
+
" | {",
|
|
671
|
+
" linear: {",
|
|
672
|
+
" /**",
|
|
673
|
+
" * @constraint positive",
|
|
674
|
+
" */",
|
|
675
|
+
" speed: number;",
|
|
676
|
+
" };",
|
|
677
|
+
" }",
|
|
678
|
+
" | {",
|
|
679
|
+
" curve: {",
|
|
680
|
+
" /**",
|
|
681
|
+
" * @constraint minLength(2)",
|
|
682
|
+
" */",
|
|
683
|
+
" keyframes: {",
|
|
684
|
+
" /**",
|
|
685
|
+
" * @constraint min(0)",
|
|
686
|
+
" * @constraint max(1)",
|
|
687
|
+
" */",
|
|
688
|
+
" position: number;",
|
|
689
|
+
" /**",
|
|
690
|
+
" * @constraint min(0)",
|
|
691
|
+
" */",
|
|
692
|
+
" rate: number;",
|
|
693
|
+
" /**",
|
|
694
|
+
" * Bezier tangent handle (x, y)",
|
|
695
|
+
" */",
|
|
696
|
+
" in_tangent?: { x: number; y: number };",
|
|
697
|
+
" /**",
|
|
698
|
+
" * Bezier tangent handle (x, y)",
|
|
699
|
+
" */",
|
|
700
|
+
" out_tangent?: { x: number; y: number };",
|
|
701
|
+
" }[];",
|
|
702
|
+
" };",
|
|
703
|
+
" };",
|
|
704
|
+
" } | null;",
|
|
705
|
+
" }[];",
|
|
706
|
+
"}",
|
|
707
|
+
"",
|
|
708
|
+
"/**",
|
|
709
|
+
" * Replace the media backing existing video clips.",
|
|
710
|
+
" */",
|
|
711
|
+
"export interface ReplaceVideoClipContentInput {",
|
|
712
|
+
" /**",
|
|
713
|
+
" * Video clips whose media is being replaced",
|
|
714
|
+
" * @constraint minLength(1)",
|
|
715
|
+
" */",
|
|
716
|
+
" clips: {",
|
|
717
|
+
" /**",
|
|
718
|
+
" * Existing video clip part ID to re-point",
|
|
719
|
+
" * @constraint minLength(1)",
|
|
720
|
+
" */",
|
|
721
|
+
" clip_id: string;",
|
|
722
|
+
" /**",
|
|
723
|
+
" * The new media asset ID",
|
|
724
|
+
" * @constraint minLength(1)",
|
|
725
|
+
" */",
|
|
726
|
+
" origin_media_id: string;",
|
|
727
|
+
" /**",
|
|
728
|
+
" * The new media's intrinsic full length",
|
|
729
|
+
" * @constraint int",
|
|
730
|
+
" * @constraint positive",
|
|
731
|
+
" */",
|
|
732
|
+
" media_duration_ms: number;",
|
|
733
|
+
" /**",
|
|
734
|
+
" * Trim window start in the new media (usually 0)",
|
|
735
|
+
" * @constraint int",
|
|
736
|
+
" * @constraint min(0)",
|
|
737
|
+
" */",
|
|
738
|
+
" play_in: number;",
|
|
739
|
+
" /**",
|
|
740
|
+
" * Trim window end in the new media (usually = media_duration_ms)",
|
|
741
|
+
" * @constraint int",
|
|
742
|
+
" * @constraint positive",
|
|
743
|
+
" */",
|
|
744
|
+
" play_out: number;",
|
|
745
|
+
" /**",
|
|
746
|
+
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
747
|
+
" * @constraint min(-60)",
|
|
748
|
+
" * @constraint max(20)",
|
|
749
|
+
" */",
|
|
750
|
+
" volume: number;",
|
|
751
|
+
" }[];",
|
|
752
|
+
"}",
|
|
753
|
+
"",
|
|
754
|
+
"/**",
|
|
755
|
+
" * Replace a contiguous run of main-track clips with a new run.",
|
|
756
|
+
" */",
|
|
757
|
+
"export interface ReplaceVideoClipSequenceInput {",
|
|
758
|
+
" /**",
|
|
759
|
+
" * The clips being replaced: a contiguous main-track run, listed in timeline order",
|
|
760
|
+
" * @constraint minLength(1)",
|
|
761
|
+
" */",
|
|
762
|
+
" old_clip_ids: string[];",
|
|
763
|
+
" /**",
|
|
764
|
+
" * The replacement clips, in the order they take on the track",
|
|
765
|
+
" * @constraint minLength(1)",
|
|
766
|
+
" */",
|
|
767
|
+
" new_clips: {",
|
|
768
|
+
" /**",
|
|
769
|
+
" * The replacement media asset ID. Omit to create an empty placeholder clip.",
|
|
770
|
+
" * @constraint minLength(1)",
|
|
771
|
+
" */",
|
|
772
|
+
" media_id?: string;",
|
|
773
|
+
" /**",
|
|
774
|
+
" * The source media's intrinsic full length in ms",
|
|
775
|
+
" * @constraint int",
|
|
776
|
+
" * @constraint positive",
|
|
777
|
+
" */",
|
|
778
|
+
" media_duration_ms: number;",
|
|
779
|
+
" /**",
|
|
780
|
+
" * Trim window start in the media (default 0)",
|
|
781
|
+
" * @constraint int",
|
|
782
|
+
" * @constraint min(0)",
|
|
783
|
+
" */",
|
|
784
|
+
" play_in?: number;",
|
|
785
|
+
" /**",
|
|
786
|
+
" * Trim window end in the media (default media_duration_ms)",
|
|
787
|
+
" * @constraint int",
|
|
788
|
+
" * @constraint positive",
|
|
789
|
+
" */",
|
|
790
|
+
" play_out?: number;",
|
|
791
|
+
" }[];",
|
|
792
|
+
" /**",
|
|
793
|
+
" * What happens to speeches anchored to the replaced clips (required — see the policy doc)",
|
|
794
|
+
" */",
|
|
795
|
+
" on_anchored: 'remap' | 'cascade';",
|
|
796
|
+
"}",
|
|
797
|
+
"",
|
|
798
|
+
"/**",
|
|
799
|
+
" * Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window).",
|
|
800
|
+
" */",
|
|
801
|
+
"export interface AdjustVideoClipDurationInput {",
|
|
802
|
+
" /**",
|
|
803
|
+
" * Video clips with their new trim windows",
|
|
804
|
+
" * @constraint minLength(1)",
|
|
805
|
+
" */",
|
|
806
|
+
" clips: {",
|
|
807
|
+
" /**",
|
|
808
|
+
" * The video clip part ID to re-trim",
|
|
809
|
+
" * @constraint minLength(1)",
|
|
810
|
+
" */",
|
|
811
|
+
" clip_id: string;",
|
|
812
|
+
" /**",
|
|
813
|
+
" * New trim window start in the source media",
|
|
814
|
+
" * @constraint int",
|
|
815
|
+
" * @constraint min(0)",
|
|
816
|
+
" */",
|
|
817
|
+
" play_in: number;",
|
|
818
|
+
" /**",
|
|
819
|
+
" * New trim window end in the source media",
|
|
820
|
+
" * @constraint int",
|
|
821
|
+
" * @constraint positive",
|
|
822
|
+
" */",
|
|
823
|
+
" play_out: number;",
|
|
824
|
+
" }[];",
|
|
825
|
+
"}",
|
|
826
|
+
"",
|
|
827
|
+
"/**",
|
|
828
|
+
" * Add speeches (and their captions).",
|
|
829
|
+
" */",
|
|
830
|
+
"export interface AddSpeechesInput extends SpeechAssets {}",
|
|
831
|
+
"",
|
|
832
|
+
"/**",
|
|
833
|
+
" * Delete speeches with their captions.",
|
|
834
|
+
" */",
|
|
835
|
+
"export interface DeleteSpeechesInput {",
|
|
836
|
+
" /**",
|
|
837
|
+
" * Speech part IDs to delete (their captions cascade-delete)",
|
|
838
|
+
" * @constraint minLength(1)",
|
|
839
|
+
" */",
|
|
840
|
+
" speech_ids: string[];",
|
|
841
|
+
"}",
|
|
842
|
+
"",
|
|
843
|
+
"/**",
|
|
844
|
+
" * Move speeches in time.",
|
|
845
|
+
" */",
|
|
846
|
+
"export interface MoveSpeechesInput {",
|
|
847
|
+
" /**",
|
|
848
|
+
" * Speeches to move to new positions",
|
|
849
|
+
" * @constraint minLength(1)",
|
|
850
|
+
" */",
|
|
851
|
+
" speeches: {",
|
|
852
|
+
" /**",
|
|
853
|
+
" * The speech part ID to move",
|
|
854
|
+
" * @constraint minLength(1)",
|
|
855
|
+
" */",
|
|
856
|
+
" speech_id: string;",
|
|
857
|
+
" /**",
|
|
858
|
+
" * New absolute start time on the timeline",
|
|
859
|
+
" * @constraint int",
|
|
860
|
+
" * @constraint min(0)",
|
|
861
|
+
" */",
|
|
862
|
+
" new_start_ms: number;",
|
|
863
|
+
" }[];",
|
|
864
|
+
"}",
|
|
865
|
+
"",
|
|
866
|
+
"/**",
|
|
867
|
+
" * Change a speech's script or voice.",
|
|
868
|
+
" */",
|
|
869
|
+
"export interface ChangeSpeechScriptInput extends SpeechAssets {}",
|
|
870
|
+
"",
|
|
871
|
+
"export interface ChangeSpeechVoiceInput extends SpeechAssets {}",
|
|
872
|
+
"",
|
|
873
|
+
"export interface AdjustSpeechVolumeInput {",
|
|
874
|
+
" /**",
|
|
875
|
+
" * List of speeches with their new volume settings",
|
|
876
|
+
" * @constraint minLength(1)",
|
|
877
|
+
" */",
|
|
878
|
+
" speeches: {",
|
|
879
|
+
" /**",
|
|
880
|
+
" * The speech part ID to adjust volume for",
|
|
881
|
+
" * @constraint minLength(1)",
|
|
882
|
+
" */",
|
|
883
|
+
" speech_id: string;",
|
|
884
|
+
" /**",
|
|
885
|
+
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
886
|
+
" * @constraint min(-60)",
|
|
887
|
+
" * @constraint max(20)",
|
|
888
|
+
" */",
|
|
889
|
+
" volume: number;",
|
|
890
|
+
" }[];",
|
|
891
|
+
"}",
|
|
892
|
+
"",
|
|
893
|
+
"/**",
|
|
894
|
+
" * Toggle caption visibility (the caption track's `is_hidden` flag).",
|
|
895
|
+
" */",
|
|
896
|
+
"export interface SetCaptionVisibilityInput {",
|
|
897
|
+
" /**",
|
|
898
|
+
" * Whether the caption track is hidden",
|
|
899
|
+
" */",
|
|
900
|
+
" is_hidden: boolean;",
|
|
901
|
+
"}",
|
|
902
|
+
"",
|
|
903
|
+
"/**",
|
|
904
|
+
" * Set the caption visual style.",
|
|
905
|
+
" */",
|
|
906
|
+
"export interface SetCaptionStyleInput {",
|
|
907
|
+
" /**",
|
|
908
|
+
" * Font ID referencing a font from the font library",
|
|
909
|
+
" * @constraint minLength(1)",
|
|
910
|
+
" */",
|
|
911
|
+
" font_id?: string;",
|
|
912
|
+
" /**",
|
|
913
|
+
" * Font size in points",
|
|
914
|
+
" * @constraint positive",
|
|
915
|
+
" */",
|
|
916
|
+
" font_size?: number;",
|
|
917
|
+
" /**",
|
|
918
|
+
" * Font color as hex string, e.g. \"#FFFFFF\"",
|
|
919
|
+
" * @constraint minLength(1)",
|
|
920
|
+
" */",
|
|
921
|
+
" font_color?: string;",
|
|
922
|
+
" /**",
|
|
923
|
+
" * Numeric font weight, e.g. 400 or 700",
|
|
924
|
+
" * @constraint int",
|
|
925
|
+
" */",
|
|
926
|
+
" font_weight?: number;",
|
|
927
|
+
" /**",
|
|
928
|
+
" * Entrance animation preset ID, e.g. \"fade\" or \"none\"",
|
|
929
|
+
" */",
|
|
930
|
+
" entrance_animation?: string;",
|
|
931
|
+
" /**",
|
|
932
|
+
" * Entrance animation duration in ms",
|
|
933
|
+
" * @constraint min(0)",
|
|
934
|
+
" */",
|
|
935
|
+
" entrance_animation_duration_ms?: number;",
|
|
936
|
+
" /**",
|
|
937
|
+
" * Outline/stroke color as hex string, e.g. \"#000000\"",
|
|
938
|
+
" * @constraint minLength(1)",
|
|
939
|
+
" */",
|
|
940
|
+
" stroke_color?: string;",
|
|
941
|
+
" /**",
|
|
942
|
+
" * Outline/stroke width in pixels",
|
|
943
|
+
" * @constraint min(0)",
|
|
944
|
+
" */",
|
|
945
|
+
" stroke_width?: number;",
|
|
946
|
+
" /**",
|
|
947
|
+
" * Caption center X as a fraction (0.0 to 1.0)",
|
|
948
|
+
" */",
|
|
949
|
+
" position_x?: number;",
|
|
950
|
+
" /**",
|
|
951
|
+
" * Caption center Y as a fraction (0.0 to 1.0)",
|
|
952
|
+
" */",
|
|
953
|
+
" position_y?: number;",
|
|
954
|
+
"}",
|
|
955
|
+
"",
|
|
956
|
+
"/**",
|
|
957
|
+
" * Set the document BGM.",
|
|
958
|
+
" */",
|
|
959
|
+
"export interface SetBgmInput {",
|
|
960
|
+
" /**",
|
|
961
|
+
" * The bgm part ID to write",
|
|
962
|
+
" * @constraint minLength(1)",
|
|
963
|
+
" */",
|
|
964
|
+
" bgm_id: string;",
|
|
965
|
+
" /**",
|
|
966
|
+
" * @constraint minLength(1)",
|
|
967
|
+
" */",
|
|
968
|
+
" audio_storage_key: string;",
|
|
969
|
+
" /**",
|
|
970
|
+
" * The media asset ID",
|
|
971
|
+
" * @constraint minLength(1)",
|
|
972
|
+
" */",
|
|
973
|
+
" origin_media_id: string;",
|
|
974
|
+
" /**",
|
|
975
|
+
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
976
|
+
" * @constraint min(-60)",
|
|
977
|
+
" * @constraint max(20)",
|
|
978
|
+
" */",
|
|
979
|
+
" volume: number;",
|
|
980
|
+
"}",
|
|
981
|
+
"",
|
|
982
|
+
"/**",
|
|
983
|
+
" * Remove the document BGM.",
|
|
984
|
+
" */",
|
|
985
|
+
"export interface DeleteBgmInput {",
|
|
986
|
+
" [key: string]: never;",
|
|
987
|
+
"}",
|
|
988
|
+
"",
|
|
989
|
+
"export interface AdjustBgmVolumeInput {",
|
|
990
|
+
" /**",
|
|
991
|
+
" * List of bgm parts with their new volume settings",
|
|
992
|
+
" * @constraint minLength(1)",
|
|
993
|
+
" */",
|
|
994
|
+
" bgm: {",
|
|
995
|
+
" /**",
|
|
996
|
+
" * The bgm part ID to adjust volume for",
|
|
997
|
+
" * @constraint minLength(1)",
|
|
998
|
+
" */",
|
|
999
|
+
" bgm_id: string;",
|
|
1000
|
+
" /**",
|
|
1001
|
+
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
1002
|
+
" * @constraint min(-60)",
|
|
1003
|
+
" * @constraint max(20)",
|
|
1004
|
+
" */",
|
|
1005
|
+
" volume: number;",
|
|
1006
|
+
" }[];",
|
|
1007
|
+
"}",
|
|
1008
|
+
"",
|
|
1009
|
+
"/** Agent write surface — one method per SemanticOp kind. */",
|
|
1010
|
+
"export interface EditApi {",
|
|
1011
|
+
" moveVideoClips(input: MoveVideoClipsInput): Promise<void>;",
|
|
1012
|
+
" /** Reorder a set of main-track clips relative to a reference clip. */",
|
|
1013
|
+
" moveVideoClipsByAnchor(input: MoveVideoClipsByAnchorInput): Promise<void>;",
|
|
1014
|
+
" deleteVideoClips(input: DeleteVideoClipsInput): Promise<void>;",
|
|
1015
|
+
" /** Add video clips to a track. */",
|
|
1016
|
+
" addVideoClips(input: AddVideoClipsInput): Promise<void>;",
|
|
1017
|
+
" adjustVideoClipVolume(input: AdjustVideoClipVolumeInput): Promise<void>;",
|
|
1018
|
+
" /** Set the playback speed of existing video clips. */",
|
|
1019
|
+
" setVideoClipSpeedShift(input: SetVideoClipSpeedShiftInput): Promise<void>;",
|
|
1020
|
+
" /** Replace the media backing existing video clips. */",
|
|
1021
|
+
" replaceVideoClipContent(input: ReplaceVideoClipContentInput): Promise<void>;",
|
|
1022
|
+
" /** Replace a contiguous run of main-track clips with a new run. */",
|
|
1023
|
+
" replaceVideoClipSequence(input: ReplaceVideoClipSequenceInput): Promise<void>;",
|
|
1024
|
+
" /** Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window). */",
|
|
1025
|
+
" adjustVideoClipDuration(input: AdjustVideoClipDurationInput): Promise<void>;",
|
|
1026
|
+
" /** Add speeches (and their captions). */",
|
|
1027
|
+
" addSpeeches(input: AddSpeechesInput): Promise<void>;",
|
|
1028
|
+
" /** Delete speeches with their captions. */",
|
|
1029
|
+
" deleteSpeeches(input: DeleteSpeechesInput): Promise<void>;",
|
|
1030
|
+
" /** Move speeches in time. */",
|
|
1031
|
+
" moveSpeeches(input: MoveSpeechesInput): Promise<void>;",
|
|
1032
|
+
" /** Change a speech's script or voice. */",
|
|
1033
|
+
" changeSpeechScript(input: ChangeSpeechScriptInput): Promise<void>;",
|
|
1034
|
+
" changeSpeechVoice(input: ChangeSpeechVoiceInput): Promise<void>;",
|
|
1035
|
+
" adjustSpeechVolume(input: AdjustSpeechVolumeInput): Promise<void>;",
|
|
1036
|
+
" /** Toggle caption visibility (the caption track's `is_hidden` flag). */",
|
|
1037
|
+
" setCaptionVisibility(input: SetCaptionVisibilityInput): Promise<void>;",
|
|
1038
|
+
" /** Set the caption visual style. */",
|
|
1039
|
+
" setCaptionStyle(input: SetCaptionStyleInput): Promise<void>;",
|
|
1040
|
+
" /** Set the document BGM. */",
|
|
1041
|
+
" setBgm(input: SetBgmInput): Promise<void>;",
|
|
1042
|
+
" /** Remove the document BGM. */",
|
|
1043
|
+
" deleteBgm(input: DeleteBgmInput): Promise<void>;",
|
|
1044
|
+
" adjustBgmVolume(input: AdjustBgmVolumeInput): Promise<void>;",
|
|
1045
|
+
"}",
|
|
1046
|
+
"",
|
|
1047
|
+
"export type JsonPrimitive = string | number | boolean | null;",
|
|
1048
|
+
"export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
|
|
1049
|
+
"export interface JsonObject {",
|
|
1050
|
+
" [key: string]: JsonValue;",
|
|
1051
|
+
"}",
|
|
1052
|
+
"",
|
|
1053
|
+
"export type KnownEntityKind =",
|
|
1054
|
+
" | 'axvideo'",
|
|
1055
|
+
" | 'timeline'",
|
|
1056
|
+
" | 'track'",
|
|
1057
|
+
" | 'clip'",
|
|
1058
|
+
" | 'asset'",
|
|
1059
|
+
" | 'video'",
|
|
1060
|
+
" | 'audio'",
|
|
1061
|
+
" | 'voice'",
|
|
1062
|
+
" | 'image'",
|
|
1063
|
+
" | 'sequence-marker'",
|
|
1064
|
+
" | 'viewport'",
|
|
1065
|
+
" | 'audio-script'",
|
|
1066
|
+
" | 'phonetic-script'",
|
|
1067
|
+
" | 'caption';",
|
|
1068
|
+
"",
|
|
1069
|
+
"export type KnownRelationKind =",
|
|
1070
|
+
" | 'timeline-track'",
|
|
1071
|
+
" | 'track-clip'",
|
|
1072
|
+
" | 'clip-marker'",
|
|
1073
|
+
" | 'marker-content'",
|
|
1074
|
+
" | 'axvideo-marker'",
|
|
1075
|
+
" | 'marker-timeline'",
|
|
1076
|
+
" | 'physical-asset'",
|
|
1077
|
+
" | 'generated'",
|
|
1078
|
+
" | 'phonetic-script-provenance'",
|
|
1079
|
+
" | 'caption-provenance'",
|
|
1080
|
+
" | 'caption-alignment';",
|
|
1081
|
+
"",
|
|
1082
|
+
"export interface BoundedNativeSequencePayload extends JsonObject {",
|
|
1083
|
+
" /** Use factual recalled coordinates; never invent an end or duration. */",
|
|
1084
|
+
" extent: { kind: 'bounded'; start: number; end: number };",
|
|
1085
|
+
" sampling: 'native';",
|
|
1086
|
+
" coordinateSpace: JsonValue;",
|
|
1087
|
+
"}",
|
|
1088
|
+
"export interface UnboundedConstantSequencePayload extends JsonObject {",
|
|
1089
|
+
" extent: { kind: 'unbounded'; start: number };",
|
|
1090
|
+
" sampling: 'constant';",
|
|
1091
|
+
" coordinateSpace: JsonValue;",
|
|
1092
|
+
"}",
|
|
1093
|
+
"export interface BoundedDerivedSequencePayload extends JsonObject {",
|
|
1094
|
+
" extent: { kind: 'bounded'; start: number; end: number };",
|
|
1095
|
+
" sampling: 'derived';",
|
|
1096
|
+
" coordinateSpace: JsonValue;",
|
|
1097
|
+
"}",
|
|
1098
|
+
"export type ScriptTextSegment = JsonObject & {",
|
|
1099
|
+
" segmentId: string;",
|
|
1100
|
+
" text: string;",
|
|
1101
|
+
" language?: string;",
|
|
1102
|
+
"};",
|
|
1103
|
+
"",
|
|
1104
|
+
"export interface EntityPayloadByKind {",
|
|
1105
|
+
" axvideo: BoundedDerivedSequencePayload;",
|
|
1106
|
+
" timeline: JsonObject;",
|
|
1107
|
+
" track: JsonObject & { hidden?: boolean; role?: string };",
|
|
1108
|
+
" clip: JsonObject;",
|
|
1109
|
+
" asset: JsonObject;",
|
|
1110
|
+
" video: BoundedNativeSequencePayload;",
|
|
1111
|
+
" audio: BoundedNativeSequencePayload;",
|
|
1112
|
+
" voice: BoundedNativeSequencePayload;",
|
|
1113
|
+
" image: UnboundedConstantSequencePayload;",
|
|
1114
|
+
" 'sequence-marker': JsonObject & {",
|
|
1115
|
+
" sourceRange: { start: number; end: number };",
|
|
1116
|
+
" targetRange?: { start: number; end: number };",
|
|
1117
|
+
" duration: { mode: 'from-source' } | { mode: 'fixed'; value: number };",
|
|
1118
|
+
" timeRemapping?: JsonValue;",
|
|
1119
|
+
" };",
|
|
1120
|
+
" viewport: JsonObject;",
|
|
1121
|
+
" 'audio-script': JsonObject & { segments: ScriptTextSegment[] };",
|
|
1122
|
+
" 'phonetic-script': JsonObject & { segments: ScriptTextSegment[] };",
|
|
1123
|
+
" caption: BoundedNativeSequencePayload;",
|
|
1124
|
+
"}",
|
|
1125
|
+
"",
|
|
1126
|
+
"export type CreateEntityInput = {",
|
|
1127
|
+
" [K in KnownEntityKind]: {",
|
|
1128
|
+
" entity_id?: string;",
|
|
1129
|
+
" entity_kind: K;",
|
|
1130
|
+
" payload: EntityPayloadByKind[K];",
|
|
1131
|
+
" };",
|
|
1132
|
+
"}[KnownEntityKind];",
|
|
1133
|
+
"",
|
|
1134
|
+
"export interface ImportAssetInput {",
|
|
1135
|
+
" asset_id: string;",
|
|
1136
|
+
" entity_id?: string;",
|
|
1137
|
+
" payload?: JsonObject;",
|
|
1138
|
+
"}",
|
|
1139
|
+
"",
|
|
1140
|
+
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
1141
|
+
" entity_id: string;",
|
|
1142
|
+
" entity_kind: K;",
|
|
1143
|
+
" payload: EntityPayloadByKind[K];",
|
|
1144
|
+
"}",
|
|
1145
|
+
"",
|
|
1146
|
+
"export interface SandboxRelation {",
|
|
1147
|
+
" relation_id: string;",
|
|
1148
|
+
" relation_kind: KnownRelationKind;",
|
|
1149
|
+
" endpoint_0_entity_id: string;",
|
|
1150
|
+
" endpoint_1_entity_id: string;",
|
|
1151
|
+
" metadata: JsonObject;",
|
|
1152
|
+
" trace: JsonObject;",
|
|
1153
|
+
"}",
|
|
1154
|
+
"",
|
|
1155
|
+
"export type EmptyRelationKind =",
|
|
1156
|
+
" | 'timeline-track'",
|
|
1157
|
+
" | 'track-clip'",
|
|
1158
|
+
" | 'clip-marker'",
|
|
1159
|
+
" | 'marker-content'",
|
|
1160
|
+
" | 'axvideo-marker'",
|
|
1161
|
+
" | 'marker-timeline';",
|
|
1162
|
+
"export type LinkRelationInput =",
|
|
1163
|
+
" | {",
|
|
1164
|
+
" relation_id?: string;",
|
|
1165
|
+
" relation_kind: EmptyRelationKind;",
|
|
1166
|
+
" endpoint_0_entity_id: string;",
|
|
1167
|
+
" endpoint_1_entity_id: string;",
|
|
1168
|
+
" metadata?: { [key: string]: never };",
|
|
1169
|
+
" trace?: JsonObject;",
|
|
1170
|
+
" }",
|
|
1171
|
+
" | {",
|
|
1172
|
+
" relation_id?: string;",
|
|
1173
|
+
" relation_kind: 'physical-asset';",
|
|
1174
|
+
" /** Canonical endpoint 0 is sequence media; endpoint 1 is Asset. */",
|
|
1175
|
+
" endpoint_0_entity_id: string;",
|
|
1176
|
+
" endpoint_1_entity_id: string;",
|
|
1177
|
+
" metadata?: JsonObject;",
|
|
1178
|
+
" trace?: JsonObject;",
|
|
1179
|
+
" }",
|
|
1180
|
+
" | {",
|
|
1181
|
+
" relation_id?: string;",
|
|
1182
|
+
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
1183
|
+
" endpoint_0_entity_id: string;",
|
|
1184
|
+
" endpoint_1_entity_id: string;",
|
|
1185
|
+
" metadata: JsonObject & { segmentAlignment: JsonValue };",
|
|
1186
|
+
" trace?: JsonObject;",
|
|
1187
|
+
" }",
|
|
1188
|
+
" | {",
|
|
1189
|
+
" relation_id?: string;",
|
|
1190
|
+
" relation_kind: 'caption-alignment';",
|
|
1191
|
+
" endpoint_0_entity_id: string;",
|
|
1192
|
+
" endpoint_1_entity_id: string;",
|
|
1193
|
+
" metadata: JsonObject & { alignment: JsonValue };",
|
|
1194
|
+
" trace?: JsonObject;",
|
|
1195
|
+
" };",
|
|
1196
|
+
"",
|
|
1197
|
+
"export interface LinkGeneratedRelationInput {",
|
|
1198
|
+
" relation_id?: string;",
|
|
1199
|
+
" /** Generated output media Entity; persisted as endpoint 0. */",
|
|
1200
|
+
" output_entity_id: string;",
|
|
1201
|
+
" /** Input media Entity used to generate the output; persisted as endpoint 1. */",
|
|
1202
|
+
" input_entity_id: string;",
|
|
1203
|
+
" trace?: JsonObject;",
|
|
1204
|
+
"}",
|
|
1205
|
+
"",
|
|
1206
|
+
"/** Explicit Entity authoring. Assets and media Entities are not one-to-one. */",
|
|
1207
|
+
"export interface EntityApi {",
|
|
1208
|
+
" list(): SandboxEntity[];",
|
|
1209
|
+
" get(entityId: string): SandboxEntity | null;",
|
|
1210
|
+
" /** Call before importAsset; inspect every match and decide whether to reuse one. */",
|
|
1211
|
+
" findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
|
|
1212
|
+
" create(input: CreateEntityInput): string;",
|
|
1213
|
+
" /** Create only an Asset Entity when no existing match should be reused; this does not infer media. */",
|
|
1214
|
+
" importAsset(input: ImportAssetInput): string;",
|
|
1215
|
+
"}",
|
|
1216
|
+
"",
|
|
1217
|
+
"/** Incident reads ignore endpoint position; relation semantics preserve it. */",
|
|
1218
|
+
"export interface RelationApi {",
|
|
1219
|
+
" list(): SandboxRelation[];",
|
|
1220
|
+
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
1221
|
+
" link(input: LinkRelationInput): string;",
|
|
1222
|
+
" /** Author ordered generated(output,input). */",
|
|
1223
|
+
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
1224
|
+
"}",
|
|
1225
|
+
"",
|
|
1226
|
+
"/** Clip hit from `clipsInRange`. */",
|
|
1227
|
+
"export interface TimelineClipDescriptor {",
|
|
1228
|
+
" id: string;",
|
|
1229
|
+
" start_ms: number;",
|
|
1230
|
+
" end_ms: number;",
|
|
1231
|
+
" duration_ms: number;",
|
|
1232
|
+
" speed_shift: unknown;",
|
|
1233
|
+
" volume: number | undefined;",
|
|
1234
|
+
" media_id: string | undefined;",
|
|
1235
|
+
"}",
|
|
1236
|
+
"",
|
|
1237
|
+
"/** Part descriptor from `part(id)`. */",
|
|
1238
|
+
"export interface TimelinePartDescriptor {",
|
|
1239
|
+
" id: string;",
|
|
1240
|
+
" kind: string;",
|
|
1241
|
+
" lane: string;",
|
|
1242
|
+
" start_ms: number;",
|
|
1243
|
+
" end_ms: number;",
|
|
1244
|
+
" duration_ms: number;",
|
|
1245
|
+
" part: unknown;",
|
|
1246
|
+
"}",
|
|
1247
|
+
"",
|
|
1248
|
+
"/** Opaque VideoDraft projection (full IDL lives in host document types). */",
|
|
1249
|
+
"export type VideoDraftProjection = {",
|
|
1250
|
+
" readonly timeline?: { readonly duration_ms?: number };",
|
|
1251
|
+
" readonly [key: string]: unknown;",
|
|
1252
|
+
"};",
|
|
1253
|
+
"",
|
|
1254
|
+
"/** Agent read surface over the forked document. */",
|
|
1255
|
+
"export interface TimelineApi {",
|
|
1256
|
+
" /** Snapshot the current VideoDraft projection. */",
|
|
1257
|
+
" snapshot(): VideoDraftProjection;",
|
|
1258
|
+
" /** Clips whose midpoint falls in `[startMs, endMs)`. */",
|
|
1259
|
+
" clipsInRange(startMs: number, endMs: number): TimelineClipDescriptor[];",
|
|
1260
|
+
" /** Look up a part by id, or null if missing. */",
|
|
1261
|
+
" part(id: string): TimelinePartDescriptor | null;",
|
|
1262
|
+
"}",
|
|
1263
|
+
"",
|
|
1264
|
+
"/** Opaque checkpoint handle for rollback. */",
|
|
1265
|
+
"export interface SandboxCheckpoint {",
|
|
1266
|
+
" readonly index: number;",
|
|
1267
|
+
"}",
|
|
1268
|
+
"",
|
|
1269
|
+
"export declare const edit: EditApi;",
|
|
1270
|
+
"export declare const timeline: TimelineApi;",
|
|
1271
|
+
"export declare const entities: EntityApi;",
|
|
1272
|
+
"export declare const relations: RelationApi;",
|
|
1273
|
+
"",
|
|
1274
|
+
"/** Capture a rollback point. */",
|
|
1275
|
+
"export declare function checkpoint(): SandboxCheckpoint;",
|
|
1276
|
+
"/** Roll the sandbox document back to a prior checkpoint. */",
|
|
1277
|
+
"export declare function rollbackTo(cp: SandboxCheckpoint): void;",
|
|
1278
|
+
"/** Host-injected, pre-materialized facts. Validate each field before use. */",
|
|
1279
|
+
"export declare const inputs: Readonly<Record<string, unknown>>;",
|
|
1280
|
+
""
|
|
1281
|
+
].join("\n");
|
|
1282
|
+
//#endregion
|
|
160
1283
|
//#region src/prompt.ts
|
|
161
1284
|
const MEDEO_TOOL_DESCRIPTION = `
|
|
162
|
-
Edit a Medeo video document through a deterministic, side-effect-free JavaScript sandbox.
|
|
1285
|
+
Edit a Medeo video document and its explicit Entity/Relation state through a deterministic, side-effect-free JavaScript sandbox.
|
|
163
1286
|
|
|
164
1287
|
Operations:
|
|
165
1288
|
- snapshot: return the compact timeline projection and opaque base version.
|
|
166
|
-
- run-edit-script: execute JavaScript against
|
|
167
|
-
- commit-plan:
|
|
1289
|
+
- run-edit-script: execute JavaScript against forked timeline and Entity/Relation snapshots. Inspect timeline.*, entities.*, and relations.*; call edit.* for timeline mutations or the explicit entity APIs for domain mutations. The sandbox has no network, storage, clock, or generation access. Pass recalled generation/asset facts through inputs. A successful run returns preview, logs, plan_kind, base versions, and plan_id — not the full journals.
|
|
1290
|
+
- commit-plan: commit a cached plan_id. Timeline plans replay into ManualSyncDoc and push one causally complete update; Entity plans replace the authoritative row set through revision CAS. validation=preflight is timeline-only. A failed transport is unconfirmed, never committed; retry the same plan_id.
|
|
168
1291
|
|
|
169
1292
|
Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. On version mismatch, rerun snapshot and the script; never try to patch a rejected journal by hand.
|
|
1293
|
+
|
|
1294
|
+
One plan must mutate exactly one store: timeline or Entity/Relation state. If both are needed, author and commit two separate plans. There is no automatic Asset→Entity projection: select the relevant recalled fact, explicitly import an Asset if useful, explicitly create only known typed Entities, and author relations. Asset and media Entity identity are not one-to-one. relations.linkGenerated({ output_entity_id, input_entity_id }) means generated(output,input); incident lookup with relations.of(entityId) is endpoint-agnostic.
|
|
170
1295
|
`.trim();
|
|
1296
|
+
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
1297
|
+
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
1298
|
+
Use timeline.snapshot() for the whole draft projection. Its duration is timeline.snapshot().timeline?.duration_ms; there is no top-level duration_ms.
|
|
1299
|
+
Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
1300
|
+
Before importing an Asset, call entities.findByAssetId(assetId), inspect every match, and decide whether an existing Entity represents the intended logical asset. Multiple matches are valid; do not assume Asset↔media is one-to-one.
|
|
1301
|
+
For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
|
|
1302
|
+
For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id. For generated lineage, use linkGenerated so endpoint 0 is output and endpoint 1 is input. relations.of remains endpoint-agnostic for lookup.
|
|
1303
|
+
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
1304
|
+
`.trim();
|
|
1305
|
+
/** Render the complete MEngine-owned context injected before one model call. */
|
|
1306
|
+
function renderMedeoModelContext(input) {
|
|
1307
|
+
const updated = input.updatedSincePreviousModelCall == null ? "unknown (first model call)" : String(input.updatedSincePreviousModelCall);
|
|
1308
|
+
return `
|
|
1309
|
+
${MEDEO_TOOL_DESCRIPTION}
|
|
1310
|
+
|
|
1311
|
+
${MEDEO_TOOL_EXECUTION_RULES}
|
|
1312
|
+
|
|
1313
|
+
Current MEngine document state (sampled dynamically immediately before this model call):
|
|
1314
|
+
- document_version: ${JSON.stringify(input.documentVersion)}
|
|
1315
|
+
- updated_since_previous_model_call: ${updated}
|
|
1316
|
+
|
|
1317
|
+
When updated_since_previous_model_call is true, the document changed after the previous model call. The change may have come from this tool or another editor, so take a fresh snapshot before planning further edits.
|
|
1318
|
+
|
|
1319
|
+
Sandbox TypeScript interface:
|
|
1320
|
+
\`\`\`ts
|
|
1321
|
+
${EDIT_SANDBOX_API_DTS}
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
`.trim();
|
|
1324
|
+
}
|
|
171
1325
|
//#endregion
|
|
172
1326
|
//#region src/schema.ts
|
|
173
1327
|
const MEDEO_TOOL_NAME = "medeo";
|
|
@@ -201,11 +1355,11 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
201
1355
|
script: {
|
|
202
1356
|
type: "string",
|
|
203
1357
|
minLength: 1,
|
|
204
|
-
description: "JavaScript body for run-edit-script. It receives edit, timeline, checkpoint, rollbackTo, inputs, and console
|
|
1358
|
+
description: "JavaScript body for run-edit-script. It receives edit, timeline, entities, relations, checkpoint, rollbackTo, inputs, and console. A plan may mutate the timeline or Entity/Relation state, never both."
|
|
205
1359
|
},
|
|
206
1360
|
inputs: {
|
|
207
1361
|
type: "object",
|
|
208
|
-
description: "Pre-materialized, side-effect-free values passed into the script. Generation and network IO must happen in the host before this call."
|
|
1362
|
+
description: "Pre-materialized, side-effect-free values passed into the script, including recalled generation lineage and asset facts. Generation and network IO must happen in the host before this call."
|
|
209
1363
|
},
|
|
210
1364
|
timeout_ms: {
|
|
211
1365
|
type: "integer",
|
|
@@ -229,7 +1383,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
229
1383
|
validation: {
|
|
230
1384
|
type: "string",
|
|
231
1385
|
enum: ["version", "preflight"],
|
|
232
|
-
description: "commit
|
|
1386
|
+
description: "Timeline commit mode: version rejects any concurrent change; preflight revalidates each op. Entity plans always use revision CAS and reject preflight."
|
|
233
1387
|
}
|
|
234
1388
|
},
|
|
235
1389
|
oneOf: [
|
|
@@ -277,39 +1431,37 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
277
1431
|
//#endregion
|
|
278
1432
|
//#region src/session/commit-plan.ts
|
|
279
1433
|
/**
|
|
280
|
-
* Replay a sandbox journal into a
|
|
281
|
-
*
|
|
1434
|
+
* Replay a sandbox journal into a manually-synchronized document and push the
|
|
1435
|
+
* whole plan as one causally complete update.
|
|
282
1436
|
*
|
|
283
|
-
* - Default / `{ validation: 'version' }`: if
|
|
284
|
-
* `plan.base_version`, reject with zero writes.
|
|
1437
|
+
* - Default / `{ validation: 'version' }`: if the current document mark differs
|
|
1438
|
+
* from `plan.base_version`, reject with zero writes.
|
|
285
1439
|
* - `{ validation: 'preflight' }`: skip the version gate; revalidate each op
|
|
286
1440
|
* against a PlainMemoryAdapter seeded from the current live snapshot, then
|
|
287
1441
|
* replay for real. A SchemaValidator failure becomes `op_conflict` with the
|
|
288
1442
|
* failing entry's index. Journal integrity errors (unrecorded/unconsumed
|
|
289
1443
|
* ids) still propagate as throws in both modes.
|
|
290
1444
|
*/
|
|
291
|
-
async function commitPlan(
|
|
292
|
-
if (options?.validation === "preflight") return commitPlanPreflight(
|
|
293
|
-
const actual =
|
|
294
|
-
|
|
1445
|
+
async function commitPlan(doc, plan, options) {
|
|
1446
|
+
if (options?.validation === "preflight") return commitPlanPreflight(doc, plan);
|
|
1447
|
+
const actual = encodeDocVersionMark(doc.versionMark());
|
|
1448
|
+
const expected = decodeDocVersionMark(plan.base_version);
|
|
1449
|
+
if (expected == null || doc.hasChangedSince(expected)) return {
|
|
295
1450
|
kind: "rejected",
|
|
296
1451
|
reason: "version_mismatch",
|
|
297
1452
|
expected: plan.base_version,
|
|
298
1453
|
actual
|
|
299
1454
|
};
|
|
300
|
-
await replayJournal(
|
|
301
|
-
return
|
|
302
|
-
kind: "committed",
|
|
303
|
-
ops_applied: plan.ops.length
|
|
304
|
-
};
|
|
1455
|
+
await doc.replayJournal(plan.ops);
|
|
1456
|
+
return retryPlanPush(doc, plan.ops.length);
|
|
305
1457
|
}
|
|
306
1458
|
/**
|
|
307
1459
|
* Phase-2 path: scratch revalidation then real replay. Each entry is driven
|
|
308
1460
|
* through `replayJournal` alone so a ValidationError maps to a stable index;
|
|
309
1461
|
* integrity throws are not wrapped.
|
|
310
1462
|
*/
|
|
311
|
-
async function commitPlanPreflight(
|
|
312
|
-
const scratch = createPlainMemoryAdapter(
|
|
1463
|
+
async function commitPlanPreflight(doc, plan) {
|
|
1464
|
+
const scratch = createPlainMemoryAdapter(doc.snapshot());
|
|
313
1465
|
for (let index = 0; index < plan.ops.length; index++) {
|
|
314
1466
|
const entry = plan.ops[index];
|
|
315
1467
|
if (entry == null) continue;
|
|
@@ -324,15 +1476,41 @@ async function commitPlanPreflight(session, plan) {
|
|
|
324
1476
|
const entry = plan.ops[index];
|
|
325
1477
|
if (entry == null) continue;
|
|
326
1478
|
try {
|
|
327
|
-
await replayJournal(
|
|
1479
|
+
await doc.replayJournal([entry]);
|
|
328
1480
|
} catch (error) {
|
|
329
1481
|
if (error instanceof ValidationError) return opConflict(index, entry.kind, `real replay: ${error.message}`);
|
|
330
1482
|
throw error;
|
|
331
1483
|
}
|
|
332
1484
|
}
|
|
1485
|
+
return retryPlanPush(doc, plan.ops.length);
|
|
1486
|
+
}
|
|
1487
|
+
/** Push an already-replayed plan again without replaying or re-running its version gate. */
|
|
1488
|
+
async function retryPlanPush(doc, opsApplied) {
|
|
1489
|
+
const result = await doc.push();
|
|
1490
|
+
if (result.kind === "ack" || result.kind === "duplicate" || result.kind === "nothing_to_push") {
|
|
1491
|
+
const reconciled = result.collaborated ? await doc.pull() : void 0;
|
|
1492
|
+
const warnings = reconciled != null && !reconciled.ok ? [{
|
|
1493
|
+
kind: "pull_failed",
|
|
1494
|
+
message: reconciled.error.message
|
|
1495
|
+
}] : void 0;
|
|
1496
|
+
return {
|
|
1497
|
+
kind: "committed",
|
|
1498
|
+
ops_applied: opsApplied,
|
|
1499
|
+
collaborated: result.collaborated,
|
|
1500
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
if (result.kind === "rejected") return {
|
|
1504
|
+
kind: "rejected",
|
|
1505
|
+
reason: "push_rejected",
|
|
1506
|
+
...result.code !== void 0 ? { code: result.code } : {},
|
|
1507
|
+
message: result.error?.message ?? "mengine rejected the sandbox plan"
|
|
1508
|
+
};
|
|
333
1509
|
return {
|
|
334
|
-
kind: "
|
|
335
|
-
|
|
1510
|
+
kind: "unconfirmed",
|
|
1511
|
+
reason: "push_failed",
|
|
1512
|
+
ops_applied: opsApplied,
|
|
1513
|
+
message: result.error?.message ?? "mengine push failed"
|
|
336
1514
|
};
|
|
337
1515
|
}
|
|
338
1516
|
function opConflict(index, op_kind, message) {
|
|
@@ -347,6 +1525,7 @@ function opConflict(index, op_kind, message) {
|
|
|
347
1525
|
//#endregion
|
|
348
1526
|
//#region src/host-tool.ts
|
|
349
1527
|
const DEFAULT_MAX_PLANS = 16;
|
|
1528
|
+
const DEFAULT_MAX_MODEL_CONTEXTS = 128;
|
|
350
1529
|
function isRecord(value) {
|
|
351
1530
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
352
1531
|
}
|
|
@@ -359,6 +1538,89 @@ function requiredContext(value, docId, field) {
|
|
|
359
1538
|
if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
|
|
360
1539
|
return resolved;
|
|
361
1540
|
}
|
|
1541
|
+
async function commitEntityPlan(client, plan) {
|
|
1542
|
+
const rows = plan.entity_rows;
|
|
1543
|
+
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1544
|
+
try {
|
|
1545
|
+
const committed = await client.commit(plan.entity_base_revision, rows);
|
|
1546
|
+
return {
|
|
1547
|
+
kind: "committed",
|
|
1548
|
+
ops_applied: plan.entity_commands.length,
|
|
1549
|
+
collaborated: false,
|
|
1550
|
+
entity_revision: committed.revision
|
|
1551
|
+
};
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (error instanceof MengineEntityHttpRequestError) {
|
|
1554
|
+
if (error.status === 409) {
|
|
1555
|
+
const actualFromPayload = revisionConflictActual(error.payload);
|
|
1556
|
+
try {
|
|
1557
|
+
const current = await client.fetchState();
|
|
1558
|
+
if (current.revision === plan.entity_base_revision + 1 && entityRowsEquivalent(current, rows)) return {
|
|
1559
|
+
kind: "committed",
|
|
1560
|
+
ops_applied: plan.entity_commands.length,
|
|
1561
|
+
collaborated: false,
|
|
1562
|
+
entity_revision: current.revision
|
|
1563
|
+
};
|
|
1564
|
+
return {
|
|
1565
|
+
kind: "rejected",
|
|
1566
|
+
reason: "entity_revision_mismatch",
|
|
1567
|
+
expected: plan.entity_base_revision,
|
|
1568
|
+
actual: current.revision
|
|
1569
|
+
};
|
|
1570
|
+
} catch {
|
|
1571
|
+
if (actualFromPayload !== void 0) return {
|
|
1572
|
+
kind: "rejected",
|
|
1573
|
+
reason: "entity_revision_mismatch",
|
|
1574
|
+
expected: plan.entity_base_revision,
|
|
1575
|
+
actual: actualFromPayload
|
|
1576
|
+
};
|
|
1577
|
+
return {
|
|
1578
|
+
kind: "unconfirmed",
|
|
1579
|
+
reason: "push_failed",
|
|
1580
|
+
ops_applied: plan.entity_commands.length,
|
|
1581
|
+
message: "entity-state conflict could not be reconciled"
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
kind: "rejected",
|
|
1587
|
+
reason: "entity_state_rejected",
|
|
1588
|
+
status: error.status,
|
|
1589
|
+
message: entityHttpErrorMessage(error.payload)
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
kind: "unconfirmed",
|
|
1594
|
+
reason: "push_failed",
|
|
1595
|
+
ops_applied: plan.entity_commands.length,
|
|
1596
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function revisionConflictActual(payload) {
|
|
1601
|
+
if (!isRecord(payload)) return void 0;
|
|
1602
|
+
const actual = payload.actual_revision;
|
|
1603
|
+
return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
|
|
1604
|
+
}
|
|
1605
|
+
function entityHttpErrorMessage(payload) {
|
|
1606
|
+
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1607
|
+
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
1608
|
+
}
|
|
1609
|
+
function commitWarnings(result) {
|
|
1610
|
+
return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
|
|
1611
|
+
}
|
|
1612
|
+
function entityRowsEquivalent(left, right) {
|
|
1613
|
+
const normalize = (state) => ({
|
|
1614
|
+
entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
|
|
1615
|
+
relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
|
|
1616
|
+
});
|
|
1617
|
+
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
1618
|
+
}
|
|
1619
|
+
function canonicalJson(value) {
|
|
1620
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
1621
|
+
if (!isRecord(value)) return value;
|
|
1622
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
|
|
1623
|
+
}
|
|
362
1624
|
function parseInput(value) {
|
|
363
1625
|
if (!isRecord(value)) throw new Error("input must be an object");
|
|
364
1626
|
const op = value.op;
|
|
@@ -402,56 +1664,78 @@ function parseInput(value) {
|
|
|
402
1664
|
/**
|
|
403
1665
|
* Create the self-contained Medeo LLM tool.
|
|
404
1666
|
*
|
|
405
|
-
* The package owns
|
|
1667
|
+
* The package owns document construction, compact projection, sandbox execution,
|
|
406
1668
|
* plan caching, commit, document get-or-create, and shutdown. The host supplies
|
|
407
1669
|
* environment facts plus the authoritative legacy draft loader used only when
|
|
408
1670
|
* Mengine has no document yet.
|
|
409
1671
|
*/
|
|
410
1672
|
function createMedeoTool(options) {
|
|
411
|
-
const
|
|
1673
|
+
const documents = /* @__PURE__ */ new Map();
|
|
1674
|
+
const entityClients = /* @__PURE__ */ new Map();
|
|
1675
|
+
const documentTails = /* @__PURE__ */ new Map();
|
|
1676
|
+
const pendingPushes = /* @__PURE__ */ new Map();
|
|
412
1677
|
const plans = /* @__PURE__ */ new Map();
|
|
1678
|
+
const modelContextVersions = /* @__PURE__ */ new Map();
|
|
413
1679
|
const maxPlans = options.maxPlans ?? DEFAULT_MAX_PLANS;
|
|
1680
|
+
const maxModelContexts = options.maxModelContexts ?? DEFAULT_MAX_MODEL_CONTEXTS;
|
|
414
1681
|
let closed = false;
|
|
415
|
-
async function
|
|
1682
|
+
async function getDocument(docId) {
|
|
416
1683
|
if (closed) throw new Error("medeo tool is closed");
|
|
417
|
-
const existing =
|
|
1684
|
+
const existing = documents.get(docId);
|
|
418
1685
|
if (existing != null) return await existing;
|
|
419
1686
|
const created = (async () => {
|
|
420
|
-
|
|
1687
|
+
return await getOrCreateDocument(new MengineHttpClient({
|
|
421
1688
|
docId,
|
|
422
1689
|
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
423
1690
|
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
424
1691
|
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
425
1692
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
426
|
-
});
|
|
427
|
-
const peerId = optionalContext(options.peerId, docId);
|
|
428
|
-
await getOrCreateDocument(client, docId, peerId);
|
|
429
|
-
const session = new MengineDocSession({
|
|
430
|
-
docId,
|
|
431
|
-
client,
|
|
432
|
-
...peerId !== void 0 ? { peerId } : {},
|
|
433
|
-
...options.sseReconnectDelayMs !== void 0 ? { sseReconnectDelayMs: options.sseReconnectDelayMs } : {}
|
|
434
|
-
});
|
|
435
|
-
try {
|
|
436
|
-
await session.start();
|
|
437
|
-
return session;
|
|
438
|
-
} catch (error) {
|
|
439
|
-
session.destroy();
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
1693
|
+
}), docId, optionalContext(options.peerId, docId));
|
|
442
1694
|
})();
|
|
443
|
-
|
|
1695
|
+
documents.set(docId, created);
|
|
444
1696
|
try {
|
|
445
1697
|
return await created;
|
|
446
1698
|
} catch (error) {
|
|
447
|
-
if (
|
|
1699
|
+
if (documents.get(docId) === created) documents.delete(docId);
|
|
448
1700
|
throw error;
|
|
449
1701
|
}
|
|
450
1702
|
}
|
|
1703
|
+
function getEntityClient(docId) {
|
|
1704
|
+
if (closed) throw new Error("medeo tool is closed");
|
|
1705
|
+
const existing = entityClients.get(docId);
|
|
1706
|
+
if (existing != null) return existing;
|
|
1707
|
+
const client = new EntityHttpClient({
|
|
1708
|
+
docId,
|
|
1709
|
+
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1710
|
+
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
1711
|
+
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
1712
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1713
|
+
});
|
|
1714
|
+
entityClients.set(docId, client);
|
|
1715
|
+
return client;
|
|
1716
|
+
}
|
|
1717
|
+
async function runExclusive(docId, use) {
|
|
1718
|
+
const previous = documentTails.get(docId) ?? Promise.resolve();
|
|
1719
|
+
let release;
|
|
1720
|
+
const gate = new Promise((resolve) => {
|
|
1721
|
+
release = resolve;
|
|
1722
|
+
});
|
|
1723
|
+
const tail = previous.catch(() => {}).then(() => gate);
|
|
1724
|
+
documentTails.set(docId, tail);
|
|
1725
|
+
await previous.catch(() => {});
|
|
1726
|
+
try {
|
|
1727
|
+
return await use(await getDocument(docId));
|
|
1728
|
+
} finally {
|
|
1729
|
+
release();
|
|
1730
|
+
if (documentTails.get(docId) === tail) documentTails.delete(docId);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
451
1733
|
async function getOrCreateDocument(client, docId, peerId) {
|
|
452
1734
|
try {
|
|
453
|
-
await
|
|
454
|
-
|
|
1735
|
+
return await ManualSyncDoc.open({
|
|
1736
|
+
client,
|
|
1737
|
+
...peerId !== void 0 ? { peerId } : {}
|
|
1738
|
+
});
|
|
455
1739
|
} catch (error) {
|
|
456
1740
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
|
|
457
1741
|
if (options.loadInitialDraft === void 0) throw error;
|
|
@@ -464,8 +1748,11 @@ function createMedeoTool(options) {
|
|
|
464
1748
|
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
465
1749
|
} catch (error) {
|
|
466
1750
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 400) throw error;
|
|
467
|
-
await client.fetchSnapshot();
|
|
468
1751
|
}
|
|
1752
|
+
return await ManualSyncDoc.open({
|
|
1753
|
+
client,
|
|
1754
|
+
...peerId !== void 0 ? { peerId } : {}
|
|
1755
|
+
});
|
|
469
1756
|
}
|
|
470
1757
|
function rememberPlan(docId, plan) {
|
|
471
1758
|
const planId = randomUUID();
|
|
@@ -474,85 +1761,214 @@ function createMedeoTool(options) {
|
|
|
474
1761
|
plan
|
|
475
1762
|
});
|
|
476
1763
|
while (plans.size > maxPlans) {
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
plans.
|
|
1764
|
+
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
1765
|
+
protectedPlanIds.add(planId);
|
|
1766
|
+
const oldestEvictable = [...plans.keys()].find((candidate) => !protectedPlanIds.has(candidate));
|
|
1767
|
+
if (oldestEvictable === void 0) break;
|
|
1768
|
+
plans.delete(oldestEvictable);
|
|
480
1769
|
}
|
|
481
1770
|
return planId;
|
|
482
1771
|
}
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
1772
|
+
function assertNoPendingPush(docId) {
|
|
1773
|
+
const pending = pendingPushes.get(docId);
|
|
1774
|
+
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
1775
|
+
}
|
|
1776
|
+
function recordPushResult(docId, planId, plan, result) {
|
|
1777
|
+
if (result.kind === "unconfirmed") {
|
|
1778
|
+
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1779
|
+
kind: "timeline",
|
|
1780
|
+
planId,
|
|
1781
|
+
plan,
|
|
1782
|
+
opsApplied: result.ops_applied
|
|
1783
|
+
} : {
|
|
1784
|
+
kind: "entities",
|
|
1785
|
+
planId,
|
|
1786
|
+
plan
|
|
1787
|
+
});
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
pendingPushes.delete(docId);
|
|
1791
|
+
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1792
|
+
}
|
|
1793
|
+
async function fetchEntityStateForSandbox(docId) {
|
|
1794
|
+
try {
|
|
1795
|
+
return await getEntityClient(docId).fetchState();
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
if (error instanceof MengineEntityHttpRequestError && error.status === 404) return {
|
|
1798
|
+
revision: 0,
|
|
1799
|
+
entities: [],
|
|
1800
|
+
relations: []
|
|
1801
|
+
};
|
|
1802
|
+
throw error;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async function commitCachedPlan(docId, doc, plan, validation) {
|
|
1806
|
+
if (plan.plan_kind === "timeline") return await commitPlan(doc, plan, validation === void 0 ? void 0 : { validation });
|
|
1807
|
+
if (validation === "preflight") throw new Error("validation=preflight applies only to timeline plans; entity plans use revision CAS");
|
|
1808
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1809
|
+
return await commitEntityPlan(getEntityClient(docId), plan);
|
|
1810
|
+
}
|
|
1811
|
+
async function observePull(doc) {
|
|
1812
|
+
const result = await doc.pull();
|
|
1813
|
+
if (result.ok) return { collaborated: result.changed };
|
|
486
1814
|
return {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1815
|
+
collaborated: false,
|
|
1816
|
+
warnings: [{
|
|
1817
|
+
kind: "pull_failed",
|
|
1818
|
+
message: result.error.message
|
|
1819
|
+
}]
|
|
492
1820
|
};
|
|
493
1821
|
}
|
|
1822
|
+
function mergeWarnings(...groups) {
|
|
1823
|
+
const warnings = groups.flatMap((group) => group ?? []);
|
|
1824
|
+
return warnings.length > 0 ? warnings : void 0;
|
|
1825
|
+
}
|
|
1826
|
+
async function getModelContext(input) {
|
|
1827
|
+
const docId = input.doc_id.trim();
|
|
1828
|
+
const contextId = input.context_id.trim();
|
|
1829
|
+
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1830
|
+
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1831
|
+
return await runExclusive(docId, async (doc) => {
|
|
1832
|
+
await observePull(doc);
|
|
1833
|
+
const documentVersion = encodeDocVersionMark(doc.versionMark());
|
|
1834
|
+
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1835
|
+
const previousVersion = modelContextVersions.get(baselineKey);
|
|
1836
|
+
const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
|
|
1837
|
+
modelContextVersions.delete(baselineKey);
|
|
1838
|
+
modelContextVersions.set(baselineKey, documentVersion);
|
|
1839
|
+
while (modelContextVersions.size > maxModelContexts) {
|
|
1840
|
+
const oldest = modelContextVersions.keys().next().value;
|
|
1841
|
+
if (oldest === void 0) break;
|
|
1842
|
+
modelContextVersions.delete(oldest);
|
|
1843
|
+
}
|
|
1844
|
+
return {
|
|
1845
|
+
prompt: renderMedeoModelContext({
|
|
1846
|
+
documentVersion,
|
|
1847
|
+
updatedSincePreviousModelCall
|
|
1848
|
+
}),
|
|
1849
|
+
document_version: documentVersion,
|
|
1850
|
+
updated_since_previous_model_call: updatedSincePreviousModelCall
|
|
1851
|
+
};
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
async function snapshot(input) {
|
|
1855
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1856
|
+
assertNoPendingPush(input.doc_id);
|
|
1857
|
+
const pull = await observePull(doc);
|
|
1858
|
+
return {
|
|
1859
|
+
ok: true,
|
|
1860
|
+
op: "snapshot",
|
|
1861
|
+
doc_id: input.doc_id,
|
|
1862
|
+
version: encodeDocVersionMark(doc.versionMark()),
|
|
1863
|
+
preview: renderCompactProjection(doc.snapshot()),
|
|
1864
|
+
collaborated: pull.collaborated,
|
|
1865
|
+
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1866
|
+
};
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
494
1869
|
async function run(input) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1870
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1871
|
+
assertNoPendingPush(input.doc_id);
|
|
1872
|
+
const [pull, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(input.doc_id)]);
|
|
1873
|
+
const document = doc.snapshot();
|
|
1874
|
+
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1875
|
+
const result = await runEditScript({
|
|
1876
|
+
document,
|
|
1877
|
+
baseVersion,
|
|
1878
|
+
entityState,
|
|
1879
|
+
script: input.script,
|
|
1880
|
+
...input.inputs !== void 0 ? { inputs: input.inputs } : {},
|
|
1881
|
+
timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
|
|
1882
|
+
memoryLimitMb: input.memory_limit_mb ?? options.sandbox?.memoryLimitMb
|
|
1883
|
+
});
|
|
1884
|
+
if (!result.ok) return {
|
|
1885
|
+
ok: false,
|
|
1886
|
+
op: "run-edit-script",
|
|
1887
|
+
doc_id: input.doc_id,
|
|
1888
|
+
phase: result.phase,
|
|
1889
|
+
error: result.error,
|
|
1890
|
+
partial: {
|
|
1891
|
+
ops_count: result.partial.ops.length + result.partial.entityCommands.length,
|
|
1892
|
+
logs: result.partial.logs
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
const plan = {
|
|
1896
|
+
...result.plan,
|
|
1897
|
+
doc_id: input.doc_id
|
|
1898
|
+
};
|
|
1899
|
+
const planId = rememberPlan(input.doc_id, plan);
|
|
1900
|
+
const base = {
|
|
1901
|
+
ok: true,
|
|
1902
|
+
op: "run-edit-script",
|
|
1903
|
+
doc_id: input.doc_id,
|
|
1904
|
+
plan_id: planId,
|
|
1905
|
+
plan_kind: plan.plan_kind,
|
|
1906
|
+
base_version: baseVersion,
|
|
1907
|
+
entity_base_revision: plan.entity_base_revision,
|
|
1908
|
+
ops_count: plan.ops.length + plan.entity_commands.length,
|
|
1909
|
+
preview: plan.preview,
|
|
1910
|
+
logs: plan.logs,
|
|
1911
|
+
duration_ms: result.durationMs,
|
|
1912
|
+
collaborated: pull.collaborated,
|
|
1913
|
+
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1914
|
+
};
|
|
1915
|
+
if (input.auto_commit !== true) return base;
|
|
1916
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan);
|
|
1917
|
+
recordPushResult(input.doc_id, planId, plan, commit);
|
|
1918
|
+
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
1919
|
+
return {
|
|
1920
|
+
...base,
|
|
1921
|
+
committed: commit.kind === "committed",
|
|
1922
|
+
commit_result: commit,
|
|
1923
|
+
collaborated: pull.collaborated || commit.kind === "committed" && commit.collaborated,
|
|
1924
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1925
|
+
};
|
|
505
1926
|
});
|
|
506
|
-
if (!result.ok) return {
|
|
507
|
-
ok: false,
|
|
508
|
-
op: "run-edit-script",
|
|
509
|
-
doc_id: input.doc_id,
|
|
510
|
-
phase: result.phase,
|
|
511
|
-
error: result.error,
|
|
512
|
-
partial: {
|
|
513
|
-
ops_count: result.partial.ops.length,
|
|
514
|
-
logs: result.partial.logs
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
const planId = rememberPlan(input.doc_id, result.plan);
|
|
518
|
-
const base = {
|
|
519
|
-
ok: true,
|
|
520
|
-
op: "run-edit-script",
|
|
521
|
-
doc_id: input.doc_id,
|
|
522
|
-
plan_id: planId,
|
|
523
|
-
base_version: baseVersion,
|
|
524
|
-
ops_count: result.plan.ops.length,
|
|
525
|
-
preview: result.plan.preview,
|
|
526
|
-
logs: result.plan.logs,
|
|
527
|
-
duration_ms: result.durationMs
|
|
528
|
-
};
|
|
529
|
-
if (input.auto_commit !== true) return base;
|
|
530
|
-
const commit = await commitPlan(session, result.plan);
|
|
531
|
-
return {
|
|
532
|
-
...base,
|
|
533
|
-
committed: commit.kind === "committed",
|
|
534
|
-
commit_result: commit
|
|
535
|
-
};
|
|
536
1927
|
}
|
|
537
1928
|
async function commit(input) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1929
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1930
|
+
const pending = pendingPushes.get(input.doc_id);
|
|
1931
|
+
if (pending != null) {
|
|
1932
|
+
if (pending.planId !== input.plan_id) throw new Error(`doc ${input.doc_id} has an unconfirmed push for plan_id ${pending.planId}; retry it before ${input.plan_id}`);
|
|
1933
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation);
|
|
1934
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result);
|
|
1935
|
+
const warnings = commitWarnings(result);
|
|
1936
|
+
return {
|
|
1937
|
+
ok: true,
|
|
1938
|
+
op: "commit-plan",
|
|
1939
|
+
doc_id: input.doc_id,
|
|
1940
|
+
plan_id: input.plan_id,
|
|
1941
|
+
plan_kind: pending.plan.plan_kind,
|
|
1942
|
+
committed: result.kind === "committed",
|
|
1943
|
+
result,
|
|
1944
|
+
collaborated: result.kind === "committed" && result.collaborated,
|
|
1945
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
const cached = plans.get(input.plan_id);
|
|
1949
|
+
if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
|
|
1950
|
+
const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
|
|
1951
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation);
|
|
1952
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result);
|
|
1953
|
+
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
1954
|
+
return {
|
|
1955
|
+
ok: true,
|
|
1956
|
+
op: "commit-plan",
|
|
1957
|
+
doc_id: input.doc_id,
|
|
1958
|
+
plan_id: input.plan_id,
|
|
1959
|
+
plan_kind: cached.plan.plan_kind,
|
|
1960
|
+
committed: result.kind === "committed",
|
|
1961
|
+
result,
|
|
1962
|
+
collaborated: pull.collaborated || result.kind === "committed" && result.collaborated,
|
|
1963
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1964
|
+
};
|
|
1965
|
+
});
|
|
551
1966
|
}
|
|
552
1967
|
return {
|
|
553
1968
|
name: MEDEO_TOOL_NAME,
|
|
554
1969
|
description: MEDEO_TOOL_DESCRIPTION,
|
|
555
1970
|
parameters: MEDEO_TOOL_PARAMETERS,
|
|
1971
|
+
getModelContext,
|
|
556
1972
|
async handle(input) {
|
|
557
1973
|
try {
|
|
558
1974
|
const parsed = parseInput(input);
|
|
@@ -569,17 +1985,22 @@ function createMedeoTool(options) {
|
|
|
569
1985
|
},
|
|
570
1986
|
async close() {
|
|
571
1987
|
closed = true;
|
|
572
|
-
|
|
573
|
-
|
|
1988
|
+
await Promise.allSettled(documentTails.values());
|
|
1989
|
+
const opening = [...documents.values()];
|
|
1990
|
+
documents.clear();
|
|
1991
|
+
entityClients.clear();
|
|
1992
|
+
documentTails.clear();
|
|
1993
|
+
pendingPushes.clear();
|
|
574
1994
|
plans.clear();
|
|
1995
|
+
modelContextVersions.clear();
|
|
575
1996
|
const errors = [];
|
|
576
|
-
for (const
|
|
577
|
-
|
|
1997
|
+
for (const documentPromise of opening) try {
|
|
1998
|
+
await documentPromise;
|
|
578
1999
|
} catch (error) {
|
|
579
2000
|
errors.push(error);
|
|
580
2001
|
}
|
|
581
2002
|
if (errors.length === 1) throw errors[0];
|
|
582
|
-
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool
|
|
2003
|
+
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool documents");
|
|
583
2004
|
}
|
|
584
2005
|
};
|
|
585
2006
|
}
|