@pixerate/editor 0.79.0 → 0.81.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.
Files changed (70) hide show
  1. package/README.md +13 -3
  2. package/dist/agent/index.cjs +102 -6
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +4 -2
  5. package/dist/agent/index.d.ts +4 -2
  6. package/dist/agent/index.js +101 -5
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/{builtins-BKYlHH1s.d.ts → builtins-CV1fDj64.d.ts} +5 -1
  9. package/dist/{builtins-1kqwLUnb.d.cts → builtins-c4w0eAYl.d.cts} +5 -1
  10. package/dist/{chunk-2CJ3LPYQ.js → chunk-424H3P4I.js} +13 -295
  11. package/dist/chunk-424H3P4I.js.map +1 -0
  12. package/dist/{chunk-UN33YOXW.cjs → chunk-PA5S7KPC.cjs} +293 -2
  13. package/dist/chunk-PA5S7KPC.cjs.map +1 -0
  14. package/dist/{chunk-IB3ZHQAX.js → chunk-PTYWCFFW.js} +29 -4
  15. package/dist/chunk-PTYWCFFW.js.map +1 -0
  16. package/dist/{chunk-JQ3BB3E6.js → chunk-PXDEFEG2.js} +293 -2
  17. package/dist/chunk-PXDEFEG2.js.map +1 -0
  18. package/dist/{chunk-YU4WQE5M.cjs → chunk-TAPXWP2B.cjs} +95 -377
  19. package/dist/chunk-TAPXWP2B.cjs.map +1 -0
  20. package/dist/{chunk-HXDMZQTD.cjs → chunk-TPOT575P.cjs} +29 -4
  21. package/dist/{chunk-IB3ZHQAX.js.map → chunk-TPOT575P.cjs.map} +1 -1
  22. package/dist/commands/index.cjs +2 -2
  23. package/dist/commands/index.d.cts +1 -1
  24. package/dist/commands/index.d.ts +1 -1
  25. package/dist/commands/index.js +1 -1
  26. package/dist/generation/index.cjs +932 -791
  27. package/dist/generation/index.cjs.map +1 -1
  28. package/dist/generation/index.d.cts +266 -240
  29. package/dist/generation/index.d.ts +266 -240
  30. package/dist/generation/index.js +926 -785
  31. package/dist/generation/index.js.map +1 -1
  32. package/dist/image-editor/index.cjs +266 -2
  33. package/dist/image-editor/index.cjs.map +1 -1
  34. package/dist/image-editor/index.d.cts +12 -1
  35. package/dist/image-editor/index.d.ts +12 -1
  36. package/dist/image-editor/index.js +265 -1
  37. package/dist/image-editor/index.js.map +1 -1
  38. package/dist/index.cjs +3 -3
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.js +16 -16
  41. package/dist/spreadsheet/index.cjs +3 -3
  42. package/dist/spreadsheet/index.cjs.map +1 -1
  43. package/dist/spreadsheet/index.js +16 -16
  44. package/dist/video-editor/index.cjs +461 -101
  45. package/dist/video-editor/index.cjs.map +1 -1
  46. package/dist/video-editor/index.d.cts +67 -3
  47. package/dist/video-editor/index.d.ts +67 -3
  48. package/dist/video-editor/index.js +426 -66
  49. package/dist/video-editor/index.js.map +1 -1
  50. package/package.json +3 -1
  51. package/skills/README.md +55 -0
  52. package/skills/pixerate-agent-tools/SKILL.md +75 -0
  53. package/skills/pixerate-animation-editor/SKILL.md +155 -0
  54. package/skills/pixerate-animation-editor/references/commands.md +123 -0
  55. package/skills/pixerate-canvas/SKILL.md +230 -0
  56. package/skills/pixerate-chart/SKILL.md +190 -0
  57. package/skills/pixerate-diagram/SKILL.md +134 -0
  58. package/skills/pixerate-generation/SKILL.md +109 -0
  59. package/skills/pixerate-image-editor/SKILL.md +139 -0
  60. package/skills/pixerate-parameters/SKILL.md +83 -0
  61. package/skills/pixerate-rich-text/SKILL.md +160 -0
  62. package/skills/pixerate-spreadsheet/SKILL.md +146 -0
  63. package/skills/pixerate-vector/SKILL.md +191 -0
  64. package/skills/pixerate-vector/references/commands.md +138 -0
  65. package/skills/pixerate-video-editor/SKILL.md +173 -0
  66. package/dist/chunk-2CJ3LPYQ.js.map +0 -1
  67. package/dist/chunk-HXDMZQTD.cjs.map +0 -1
  68. package/dist/chunk-JQ3BB3E6.js.map +0 -1
  69. package/dist/chunk-UN33YOXW.cjs.map +0 -1
  70. package/dist/chunk-YU4WQE5M.cjs.map +0 -1
@@ -1101,6 +1101,153 @@ function createGenerationToolProvider(options) {
1101
1101
  };
1102
1102
  }
1103
1103
 
1104
+ // src/generation/assets.ts
1105
+ function toAssetResolver(lookup) {
1106
+ if (!lookup) return void 0;
1107
+ if (typeof lookup === "function") return (id) => lookup(id) ?? void 0;
1108
+ if (lookup instanceof Map) return (id) => lookup.get(id);
1109
+ const byId = new Map(lookup.map((asset) => [asset.id, asset]));
1110
+ return (id) => byId.get(id);
1111
+ }
1112
+ function roleCap(caps, role) {
1113
+ const cap = caps.references?.roles[role];
1114
+ if (cap && cap.max > 0) return cap;
1115
+ if (role === "mask" && caps.inpainting) return { max: 1 };
1116
+ return void 0;
1117
+ }
1118
+ function getAssetRoleOptions(caps) {
1119
+ const roles = supportedReferenceRoles(caps);
1120
+ if (caps.inpainting && !roles.includes("mask")) roles.push("mask");
1121
+ return roles.map((role) => {
1122
+ const cap = roleCap(caps, role) ?? { max: 1 };
1123
+ return {
1124
+ role,
1125
+ label: referenceRoleLabel(role, caps),
1126
+ ...cap.description ? { description: cap.description } : {},
1127
+ min: cap.min ?? 0,
1128
+ max: cap.max,
1129
+ ...cap.accepts?.length ? { accepts: [...cap.accepts] } : {}
1130
+ };
1131
+ });
1132
+ }
1133
+ function validateAssetMentions(mentions, caps, options = {}) {
1134
+ const resolve = toAssetResolver(options.assets);
1135
+ const issues = [];
1136
+ const counts = {};
1137
+ mentions.forEach((mention, index) => {
1138
+ const id = mention.id;
1139
+ const label = `@${mention.label ?? id}`;
1140
+ if (!mention.role) {
1141
+ issues.push({ index, id, code: "missing-role", severity: "warning", message: `${label} has no role.` });
1142
+ return;
1143
+ }
1144
+ let role = mention.role;
1145
+ if (!roleCap(caps, role)) {
1146
+ const alias = normalizeReferenceRole(role, caps);
1147
+ if (alias !== role && roleCap(caps, alias)) {
1148
+ issues.push({
1149
+ index,
1150
+ id,
1151
+ role,
1152
+ code: "renamed-role",
1153
+ severity: "warning",
1154
+ suggestion: alias,
1155
+ message: `${label}: this model calls "${role}" ${referenceRoleLabel(alias, caps)} (${alias}).`
1156
+ });
1157
+ role = alias;
1158
+ } else {
1159
+ issues.push({
1160
+ index,
1161
+ id,
1162
+ role,
1163
+ code: "unsupported-role",
1164
+ severity: "error",
1165
+ message: `${label}: ${caps.name ?? caps.id} does not accept ${referenceRoleLabel(role, caps).toLowerCase()} assets.`
1166
+ });
1167
+ return;
1168
+ }
1169
+ }
1170
+ const cap = roleCap(caps, role);
1171
+ const mediaType = mention.mediaType ?? resolve?.(id)?.mediaType;
1172
+ if (mediaType !== void 0 && !mediaTypeMatches(mediaType, cap.accepts)) {
1173
+ issues.push({
1174
+ index,
1175
+ id,
1176
+ role,
1177
+ code: "media-mismatch",
1178
+ severity: "error",
1179
+ message: `${label} is ${mediaType}; ${referenceRoleLabel(role, caps)} accepts ${(cap.accepts ?? []).join(", ")}.`
1180
+ });
1181
+ return;
1182
+ }
1183
+ counts[role] = (counts[role] ?? 0) + 1;
1184
+ if (counts[role] > cap.max) {
1185
+ issues.push({
1186
+ index,
1187
+ id,
1188
+ role,
1189
+ code: "too-many",
1190
+ severity: "warning",
1191
+ message: `${label}: at most ${cap.max} ${referenceRoleLabel(role, caps).toLowerCase()} asset(s); extra ones are left out.`
1192
+ });
1193
+ }
1194
+ });
1195
+ const missing = getAssetRoleOptions(caps).filter((option) => (counts[option.role] ?? 0) < option.min).map((option) => ({ role: option.role, min: option.min, count: counts[option.role] ?? 0 }));
1196
+ return { ok: !issues.some((issue) => issue.severity === "error") && missing.length === 0, issues, counts, missing };
1197
+ }
1198
+ function resolveAssetMentions(mentions, assets) {
1199
+ const byId = new Map(assets.map((asset) => [asset.id, asset]));
1200
+ const byLabel = new Map(assets.filter((a) => a.label).map((asset) => [asset.label.toLowerCase(), asset]));
1201
+ const result = [];
1202
+ for (const mention of mentions) {
1203
+ const asset = byId.get(mention.handle) ?? byLabel.get(mention.handle.toLowerCase());
1204
+ if (!asset) continue;
1205
+ result.push({
1206
+ id: asset.id,
1207
+ label: mention.handle,
1208
+ ...mention.role ? { role: mention.role } : {},
1209
+ ...asset.mediaType !== void 0 ? { mediaType: asset.mediaType } : {},
1210
+ ...asset.contentHash !== void 0 ? { contentHash: asset.contentHash } : {},
1211
+ ...asset.url !== void 0 ? { url: asset.url } : {}
1212
+ });
1213
+ }
1214
+ return result;
1215
+ }
1216
+ function defaultAssetRoleOptions() {
1217
+ return REFERENCE_ROLES.map((role) => ({ role, label: referenceRoleLabel(role), min: 0, max: 1 }));
1218
+ }
1219
+ function describeAssetMentions(mentions, assets, caps) {
1220
+ const byId = new Map(assets.map((asset) => [asset.id, asset]));
1221
+ const byLabel = new Map(assets.filter((a) => a.label).map((asset) => [asset.label.toLowerCase(), asset]));
1222
+ const chips = [];
1223
+ for (const mention of mentions) {
1224
+ const asset = byId.get(mention.handle) ?? byLabel.get(mention.handle.toLowerCase());
1225
+ if (!asset) continue;
1226
+ const role = mention.role || void 0;
1227
+ chips.push({
1228
+ index: chips.length,
1229
+ ...mention.start !== void 0 ? { start: mention.start } : {},
1230
+ handle: mention.handle,
1231
+ asset,
1232
+ ...role ? { role, roleLabel: referenceRoleLabel(caps ? normalizeReferenceRole(role, caps) : role, caps) } : {},
1233
+ issues: []
1234
+ });
1235
+ }
1236
+ if (caps) {
1237
+ const validation = validateAssetMentions(
1238
+ chips.map((chip) => ({
1239
+ id: chip.asset.id,
1240
+ label: chip.handle,
1241
+ ...chip.role ? { role: chip.role } : {},
1242
+ ...chip.asset.mediaType ? { mediaType: chip.asset.mediaType } : {}
1243
+ })),
1244
+ caps
1245
+ );
1246
+ for (const issue of validation.issues) chips[issue.index].issues.push(issue);
1247
+ }
1248
+ return chips;
1249
+ }
1250
+
1104
1251
  // src/generation/provenance.ts
1105
1252
  var PROVENANCE_FORMAT = "pixerate.generation-provenance";
1106
1253
  var PROVENANCE_VERSION = 1;
@@ -1233,849 +1380,842 @@ function replayProvenancePrompt(provenance) {
1233
1380
  return { text: result.text, map: result.map, matches: result.text === provenance.resolvedPrompt };
1234
1381
  }
1235
1382
 
1236
- // src/generation/batch.ts
1237
- function createBatchResolver(options) {
1238
- const maxBatch = Math.max(1, Math.floor(options.maxBatch ?? 50));
1239
- const delayMs = Math.max(0, options.delayMs ?? 0);
1240
- const settled = /* @__PURE__ */ new Map();
1241
- const failures = /* @__PURE__ */ new Set();
1242
- const inFlight = /* @__PURE__ */ new Map();
1243
- let queue = [];
1244
- let timer = null;
1245
- const run = (keys) => {
1246
- const pendings = keys.map((key) => inFlight.get(key));
1247
- const finish = (values, error) => {
1248
- if (values === null) options.onError?.(error, keys);
1249
- keys.forEach((key, index) => {
1250
- const pending = pendings[index];
1251
- const value = values?.get(key);
1252
- if (pending && inFlight.get(key) === pending) {
1253
- inFlight.delete(key);
1254
- settled.set(key, value);
1255
- if (values === null) failures.add(key);
1256
- }
1257
- pending?.resolve(value);
1258
- });
1259
- };
1260
- let promise;
1261
- try {
1262
- promise = Promise.resolve(options.fetch(keys));
1263
- } catch (error) {
1264
- promise = Promise.reject(error);
1265
- }
1266
- promise.then(
1267
- (values) => finish(values instanceof Map ? values : /* @__PURE__ */ new Map()),
1268
- (error) => finish(null, error ?? new Error("Batch fetch failed."))
1269
- );
1270
- };
1271
- const flush = () => {
1272
- if (timer !== null) clearTimeout(timer);
1273
- timer = null;
1274
- const keys = queue;
1275
- queue = [];
1276
- for (let i = 0; i < keys.length; i += maxBatch) run(keys.slice(i, i + maxBatch));
1277
- };
1278
- const load = (key) => {
1279
- if (settled.has(key)) return Promise.resolve(settled.get(key));
1280
- const existing = inFlight.get(key);
1281
- if (existing) return existing.promise;
1282
- let resolve;
1283
- const promise = new Promise((r) => resolve = r);
1284
- inFlight.set(key, { promise, resolve });
1285
- queue.push(key);
1286
- if (queue.length >= maxBatch) flush();
1287
- else if (timer === null) timer = setTimeout(flush, delayMs);
1288
- return promise;
1289
- };
1290
- return {
1291
- load,
1292
- loadMany: (keys) => Promise.all(keys.map(load)),
1293
- peek: (key) => settled.get(key),
1294
- has: (key) => settled.has(key),
1295
- failed: (key) => failures.has(key),
1296
- prime(key, value) {
1297
- settled.set(key, value);
1298
- failures.delete(key);
1299
- },
1300
- clear(key) {
1301
- if (key === void 0) {
1302
- settled.clear();
1303
- failures.clear();
1304
- for (const k of [...inFlight.keys()]) if (!queue.includes(k)) inFlight.delete(k);
1305
- return;
1306
- }
1307
- settled.delete(key);
1308
- failures.delete(key);
1309
- if (!queue.includes(key)) inFlight.delete(key);
1310
- },
1311
- flush
1312
- };
1383
+ // src/generation/record.ts
1384
+ var GENERATION_RECORD_FORMAT = "pixerate.generation-record";
1385
+ var GENERATION_RECORD_VERSION = 1;
1386
+ var idCounter = 0;
1387
+ function createGenerationRecordId() {
1388
+ const uuid = globalThis.crypto?.randomUUID?.();
1389
+ if (typeof uuid === "string") return `gen_${uuid}`;
1390
+ idCounter = (idCounter + 1) % 1679616;
1391
+ const random = Math.floor(Math.random() * 2 ** 48).toString(36).padStart(10, "0");
1392
+ return `gen_${Date.now().toString(36)}${idCounter.toString(36).padStart(4, "0")}${random}`;
1313
1393
  }
1314
-
1315
- // src/generation/progress.ts
1316
- var MAX_PENDING_PERCENT = 95;
1317
- var ALMOST_DONE_MS = 5e3;
1318
- var UNKNOWN_DURATION_TAU_MS = 3e4;
1319
- var toMs = (value) => typeof value === "number" ? value : new Date(value).getTime();
1320
- function formatRemaining(ms) {
1321
- const seconds = Math.ceil(ms / 1e3);
1322
- if (seconds < 60) return `~${Math.max(1, seconds)}s left`;
1323
- const minutes = Math.ceil(ms / 6e4);
1324
- if (minutes < 60) return `~${minutes}m left`;
1325
- const hours = Math.floor(minutes / 60);
1326
- const rest = minutes % 60;
1327
- return rest ? `~${hours}h ${rest}m left` : `~${hours}h left`;
1394
+ var clone2 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
1395
+ function recordTemplates(pinned) {
1396
+ return pinned.map((p) => ({
1397
+ name: p.name,
1398
+ ...p.templateId !== void 0 ? { templateId: p.templateId } : {},
1399
+ ...p.versionId !== void 0 ? { versionId: p.versionId, version: p.version } : {},
1400
+ hash: hashText(p.body)
1401
+ }));
1328
1402
  }
1329
- function estimateJobProgress(input) {
1330
- const started = toMs(input.startedAt);
1331
- const now = toMs(input.now ?? Date.now());
1332
- const elapsedMs = Math.max(0, now - started);
1333
- if (input.done) return { percent: 100, label: "Done", elapsedMs, remainingMs: 0, overdue: false };
1334
- if (!Number.isFinite(started) || now <= started) {
1335
- return { percent: 0, label: "Starting\u2026", elapsedMs: 0, ...input.expectedMs ? { remainingMs: input.expectedMs } : {}, overdue: false };
1336
- }
1337
- const expected = input.expectedMs;
1338
- if (!expected || !(expected > 0)) {
1339
- const percent2 = MAX_PENDING_PERCENT * (1 - Math.exp(-elapsedMs / UNKNOWN_DURATION_TAU_MS));
1340
- return { percent: Math.floor(percent2), label: "Working\u2026", elapsedMs, overdue: false };
1403
+ function recordAssets(references, lookup) {
1404
+ const resolve = toAssetResolver(lookup);
1405
+ return (references ?? []).map((ref) => {
1406
+ const info = resolve?.(ref.id);
1407
+ const contentHash = ref.contentHash ?? info?.contentHash;
1408
+ const mediaType = ref.mediaType ?? info?.mediaType;
1409
+ const url = ref.url ?? info?.url;
1410
+ return {
1411
+ id: ref.id,
1412
+ role: ref.role,
1413
+ ...contentHash !== void 0 ? { contentHash } : {},
1414
+ ...mediaType !== void 0 ? { mediaType } : {},
1415
+ ...url !== void 0 ? { url } : {}
1416
+ };
1417
+ });
1418
+ }
1419
+ function createGenerationRecord(options) {
1420
+ const {
1421
+ model,
1422
+ request: settingsInput,
1423
+ plan: planInput,
1424
+ strategy: strategyInput,
1425
+ mentions,
1426
+ assets,
1427
+ seed: seedInput,
1428
+ id,
1429
+ parentId,
1430
+ kind,
1431
+ ...provenanceOptions
1432
+ } = options;
1433
+ const caps = typeof model === "string" ? void 0 : model;
1434
+ const settings = clone2(settingsInput ?? planInput?.request ?? {});
1435
+ let plan = planInput;
1436
+ if (!plan && caps) {
1437
+ const resolved = resolvePrompt(options.rawPrompt, options.templates ?? [], {
1438
+ bindings: options.bindings,
1439
+ values: options.values,
1440
+ maxDepth: options.maxDepth
1441
+ });
1442
+ plan = planGenerationRequest(settings, caps, {
1443
+ strategy: strategyInput ?? "nearest",
1444
+ resolvedPrompt: resolved.text,
1445
+ ...mentions ? { mentions } : {}
1446
+ });
1341
1447
  }
1342
- const remainingMs = Math.max(0, expected - elapsedMs);
1343
- const percent = Math.min(MAX_PENDING_PERCENT, Math.floor(elapsedMs / expected * 100));
1448
+ const provenance = createProvenance({
1449
+ ...provenanceOptions,
1450
+ model,
1451
+ ...plan ? { plan } : { request: settings }
1452
+ });
1453
+ const seed = seedInput ?? provenance.request.seed;
1344
1454
  return {
1345
- percent,
1346
- label: remainingMs <= ALMOST_DONE_MS ? "Almost done\u2026" : formatRemaining(remainingMs),
1347
- elapsedMs,
1348
- remainingMs,
1349
- overdue: elapsedMs > expected
1455
+ format: GENERATION_RECORD_FORMAT,
1456
+ version: GENERATION_RECORD_VERSION,
1457
+ id: id ?? createGenerationRecordId(),
1458
+ ...parentId !== void 0 ? { parentId } : {},
1459
+ kind: kind ?? (parentId !== void 0 ? "recreate" : "generate"),
1460
+ provenance,
1461
+ templates: recordTemplates(provenance.templates),
1462
+ promptHash: hashText(provenance.resolvedPrompt),
1463
+ ...caps ? { capabilityHash: hashCapabilities(caps) } : {},
1464
+ settings,
1465
+ strategy: strategyInput ?? plan?.strategy ?? "nearest",
1466
+ ...seed !== void 0 ? { seed } : {},
1467
+ assets: recordAssets(provenance.request.references, assets),
1468
+ ...mentions?.length ? { mentions: clone2([...mentions]) } : {},
1469
+ createdAt: provenance.createdAt
1350
1470
  };
1351
1471
  }
1352
- function expectedDurationFor(caps, request = {}) {
1353
- const timing = caps.timing;
1354
- if (!timing) return void 0;
1355
- const count = request.count ?? caps.count?.default ?? 1;
1356
- const duration = request.duration ?? caps.duration?.default ?? 0;
1357
- const resolution = request.resolution ?? caps.resolutions?.default;
1358
- const megapixels = resolution ? resolutionMegapixels(resolution) ?? 0 : 0;
1359
- return Math.round(
1360
- timing.baseMs + (timing.perItemMs ?? 0) * Math.max(0, count - 1) + (timing.perSecondMs ?? 0) * duration + (timing.perMegapixelMs ?? 0) * megapixels
1361
- );
1472
+ function completeGenerationRecord(record, update) {
1473
+ const next = clone2(record);
1474
+ const now = new Date(update.completedAt ?? Date.now()).toISOString();
1475
+ if (update.outputIds) next.provenance.outputIds = [...update.outputIds];
1476
+ next.provenance.completedAt = now;
1477
+ if (update.meta) next.provenance.meta = { ...next.provenance.meta ?? {}, ...clone2(update.meta) };
1478
+ if (update.seed !== void 0) next.seed = update.seed;
1479
+ next.updatedAt = now;
1480
+ return next;
1362
1481
  }
1363
-
1364
- // src/generation/controls.ts
1365
- function enumControl(key, label, cap) {
1366
- if (!cap?.options.length) return null;
1367
- return {
1368
- key,
1369
- label,
1370
- kind: "select",
1371
- valueType: "string",
1372
- options: cap.options.map((value) => ({ value, label: cap.labels?.[value] ?? value })),
1373
- ...cap.default !== void 0 ? { defaultValue: cap.default } : {}
1374
- };
1482
+ function serializeGenerationRecord(record) {
1483
+ return JSON.stringify(record);
1375
1484
  }
1376
- function getGenerationControls(caps) {
1377
- const controls = [];
1378
- const modes = supportedModes(caps);
1379
- if (modes.length > 1) {
1380
- controls.push({
1381
- key: "mode",
1382
- label: "Mode",
1383
- kind: "select",
1384
- valueType: "string",
1385
- options: modes.map((m) => ({ value: m, label: m[0].toUpperCase() + m.slice(1) })),
1386
- defaultValue: "generate"
1387
- });
1485
+ var isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1486
+ function parseGenerationRecord(input) {
1487
+ let data = input;
1488
+ if (typeof input === "string") {
1489
+ try {
1490
+ data = JSON.parse(input);
1491
+ } catch {
1492
+ throw new ProvenanceParseError("Generation record is not valid JSON.");
1493
+ }
1388
1494
  }
1389
- const negative = textFieldCapability(caps.negativePrompt);
1390
- if (negative) controls.push({ key: "negativePrompt", label: "Negative prompt", kind: "textarea", valueType: "string", maxLength: negative.maxLength });
1391
- const system = textFieldCapability(caps.systemPrompt);
1392
- if (system) controls.push({ key: "systemPrompt", label: "System prompt", kind: "textarea", valueType: "string", maxLength: system.maxLength });
1393
- controls.push(enumControl("aspectRatio", "Aspect ratio", caps.aspectRatios));
1394
- controls.push(enumControl("resolution", "Resolution", caps.resolutions));
1395
- controls.push(enumControl("quality", "Quality", caps.quality));
1396
- if (caps.count) {
1397
- const options = caps.count.options?.filter((o) => o >= 1 && o <= caps.count.max);
1398
- controls.push(
1399
- options?.length ? { key: "count", label: "Outputs", kind: "select", valueType: "number", options: options.map((o) => ({ value: o, label: String(o) })), defaultValue: caps.count.default } : { key: "count", label: "Outputs", kind: "range", valueType: "number", min: 1, max: caps.count.max, step: 1, defaultValue: caps.count.default ?? 1 }
1400
- );
1495
+ if (!isObject(data) || data.format !== GENERATION_RECORD_FORMAT) {
1496
+ throw new ProvenanceParseError("Not a generation record.");
1401
1497
  }
1402
- if (caps.duration) {
1403
- const d = caps.duration;
1404
- const options = d.options?.filter((o) => (d.min === void 0 || o >= d.min) && (d.max === void 0 || o <= d.max));
1405
- controls.push(
1406
- options?.length ? { key: "duration", label: "Duration (s)", kind: "select", valueType: "number", options: options.map((o) => ({ value: o, label: `${o}s` })), defaultValue: d.default } : {
1407
- key: "duration",
1408
- label: "Duration (s)",
1409
- kind: d.min !== void 0 && d.max !== void 0 ? "range" : "number",
1410
- valueType: "number",
1411
- min: d.min,
1412
- max: d.max,
1413
- step: d.step,
1414
- defaultValue: d.default
1415
- }
1416
- );
1498
+ if (typeof data.version !== "number" || data.version > GENERATION_RECORD_VERSION) {
1499
+ throw new ProvenanceParseError(`Unsupported generation record version ${JSON.stringify(data.version)}.`);
1417
1500
  }
1418
- const seed = seedCapability(caps.seed);
1419
- if (seed) controls.push({ key: "seed", label: "Seed", kind: "number", valueType: "number", min: seed.min, max: seed.max, step: 1 });
1420
- for (const [name, spec] of Object.entries(caps.params ?? {})) {
1421
- const base = { key: `params.${name}`, label: spec.label ?? name, ...spec.description ? { description: spec.description } : {} };
1422
- if (spec.type === "boolean") {
1423
- controls.push({ ...base, kind: "checkbox", valueType: "boolean", defaultValue: spec.default });
1424
- } else if (spec.type === "enum") {
1425
- const numeric = spec.options.every((o) => typeof o === "number");
1426
- controls.push({
1427
- ...base,
1428
- kind: "select",
1429
- valueType: numeric ? "number" : "string",
1430
- options: spec.options.map((value) => ({ value, label: String(value) })),
1431
- defaultValue: spec.default
1432
- });
1433
- } else {
1434
- controls.push({
1435
- ...base,
1436
- kind: spec.min !== void 0 && spec.max !== void 0 ? "range" : "number",
1437
- valueType: "number",
1438
- min: spec.min,
1439
- max: spec.max,
1440
- step: spec.step ?? (spec.type === "integer" ? 1 : void 0),
1441
- defaultValue: spec.default
1501
+ const checks = [
1502
+ ["id", typeof data.id === "string" && data.id !== ""],
1503
+ ["templates", Array.isArray(data.templates) && data.templates.every((t) => isObject(t) && typeof t.name === "string" && typeof t.hash === "string")],
1504
+ ["assets", Array.isArray(data.assets) && data.assets.every((a) => isObject(a) && typeof a.id === "string" && typeof a.role === "string")],
1505
+ ["settings", isObject(data.settings)],
1506
+ ["promptHash", typeof data.promptHash === "string"],
1507
+ ["createdAt", typeof data.createdAt === "string"]
1508
+ ];
1509
+ const bad = checks.find(([, ok]) => !ok);
1510
+ if (bad) throw new ProvenanceParseError(`Invalid generation record field "${bad[0]}".`);
1511
+ const provenance = parseProvenance(data.provenance);
1512
+ return {
1513
+ ...data,
1514
+ kind: data.kind === "recreate" || data.kind === "variation" ? data.kind : "generate",
1515
+ strategy: ["nearest", "upgrade", "downgrade", "error"].includes(data.strategy) ? data.strategy : "nearest",
1516
+ provenance
1517
+ };
1518
+ }
1519
+ function isGenerationRecord(value) {
1520
+ return isObject(value) && value.format === GENERATION_RECORD_FORMAT;
1521
+ }
1522
+ var SKIPPED_SETTING_FIELDS = /* @__PURE__ */ new Set(["model", "prompt", "references", "params", "seed"]);
1523
+ function findModel(capabilities, modelId) {
1524
+ if (!capabilities) return { given: false };
1525
+ const list2 = Array.isArray(capabilities) ? capabilities : [capabilities];
1526
+ return { given: true, caps: list2.find((m) => m.id === modelId) };
1527
+ }
1528
+ var sameValue = (a, b) => stableStringify(a) === stableStringify(b);
1529
+ var show = (value) => value === void 0 ? "unset" : typeof value === "string" ? `"${value}"` : JSON.stringify(value);
1530
+ var refKey = (ref) => `${ref.role}:${ref.id}`;
1531
+ function templateDrift(record, current, mode) {
1532
+ if (!current) return [];
1533
+ const byName = new Map(current.map((t) => [t.name, t]));
1534
+ const drift = [];
1535
+ for (const recorded of record.templates) {
1536
+ const template = byName.get(recorded.name);
1537
+ const fromVersion = recorded.version ?? recorded.versionId;
1538
+ if (!template) {
1539
+ drift.push({
1540
+ type: "template-missing",
1541
+ severity: "warning",
1542
+ template: recorded.name,
1543
+ from: fromVersion,
1544
+ applied: false,
1545
+ message: `Template "${recorded.name}" no longer exists; the recorded body is used.`
1442
1546
  });
1547
+ continue;
1443
1548
  }
1549
+ const body = getTemplateBody(template, {});
1550
+ const hash = body === void 0 ? void 0 : hashText(body);
1551
+ if (hash === recorded.hash) continue;
1552
+ const latest = getLatestVersion(template);
1553
+ const toVersion = template.versions?.length ? latest?.version ?? latest?.id : void 0;
1554
+ const versions = fromVersion !== void 0 || toVersion !== void 0 ? ` (${fromVersion ?? "unversioned"} \u2192 ${toVersion ?? "unversioned"})` : "";
1555
+ drift.push({
1556
+ type: "template-changed",
1557
+ severity: mode === "latest" ? "info" : "warning",
1558
+ template: recorded.name,
1559
+ from: { version: fromVersion, hash: recorded.hash },
1560
+ to: { version: toVersion, hash },
1561
+ applied: mode === "latest",
1562
+ message: mode === "latest" ? `Template "${recorded.name}" changed since this generation${versions}; using the current version.` : `Template "${recorded.name}" changed since this generation${versions}; recreating with the recorded version.`
1563
+ });
1444
1564
  }
1445
- return controls.filter((c) => c !== null);
1565
+ return drift;
1446
1566
  }
1447
- function getRequestValue(request, key) {
1448
- if (key.startsWith("params.")) return request.params?.[key.slice(7)];
1449
- return request[key];
1450
- }
1451
- function setRequestValue(request, key, value) {
1452
- const remove = value === void 0 || value === "" || typeof value === "number" && Number.isNaN(value);
1453
- if (key.startsWith("params.")) {
1454
- const name = key.slice(7);
1455
- const params = { ...request.params ?? {} };
1456
- if (remove) delete params[name];
1457
- else params[name] = value;
1458
- const next2 = { ...request, params };
1459
- if (Object.keys(params).length === 0) delete next2.params;
1460
- return next2;
1567
+ function randomSeed(caps, previous, random) {
1568
+ const cap = caps ? seedCapability(caps.seed) : {};
1569
+ if (!cap) return void 0;
1570
+ const min = Math.ceil(cap.min ?? 0);
1571
+ const max = Math.floor(cap.max ?? 2 ** 31 - 1);
1572
+ if (max < min) return min;
1573
+ let seed = previous;
1574
+ for (let attempt = 0; attempt < 8 && (seed === void 0 || seed === previous); attempt++) {
1575
+ seed = min + Math.floor(random() * (max - min + 1));
1461
1576
  }
1462
- const next = { ...request };
1463
- if (remove) delete next[key];
1464
- else next[key] = value;
1465
- return next;
1577
+ if (seed === previous && max > min) seed = previous === max ? min : previous + 1;
1578
+ return seed;
1466
1579
  }
1467
- function parseControlValue(control, raw) {
1468
- if (control.valueType === "boolean") return Boolean(raw);
1469
- if (raw === "" || raw === void 0) return void 0;
1470
- if (control.valueType === "number") {
1471
- const n = Number(raw);
1472
- return Number.isFinite(n) ? n : void 0;
1580
+ function rebuild(record, options, kind, overrides) {
1581
+ const mode = options.mode ?? "exact";
1582
+ const provenance = record.provenance;
1583
+ const drift = templateDrift(record, options.templates, mode);
1584
+ const rawPrompt = overrides?.rawPrompt ?? provenance.rawPrompt;
1585
+ const values = overrides?.values !== void 0 ? { ...provenance.values ?? {}, ...overrides.values } : provenance.values ? clone2(provenance.values) : null;
1586
+ const pinned = pinnedTemplatesToTemplates(provenance.templates);
1587
+ let templates;
1588
+ let bindings;
1589
+ if (mode === "latest" && options.templates) {
1590
+ const currentNames = new Set(options.templates.map((t) => t.name));
1591
+ const fallback = pinned.filter((t) => !currentNames.has(t.name));
1592
+ templates = [...options.templates.map((t) => ({ ...t })), ...fallback];
1593
+ bindings = {};
1594
+ for (const t of fallback) {
1595
+ if (provenance.bindings[t.name] !== void 0) bindings[t.name] = provenance.bindings[t.name];
1596
+ }
1597
+ } else {
1598
+ templates = pinned;
1599
+ bindings = { ...provenance.bindings };
1473
1600
  }
1474
- return String(raw);
1475
- }
1476
- function getReferenceSlots(caps, request = {}) {
1477
- const roles = supportedReferenceRoles(caps);
1478
- if (caps.inpainting && !roles.includes("mask")) roles.push("mask");
1479
- const refs = request.references ?? [];
1480
- const total = refs.length;
1481
- const maxTotal = caps.references?.maxTotal ?? Infinity;
1482
- return roles.map((role) => {
1483
- const cap = caps.references?.roles[role] ?? { max: 1 };
1484
- const references = refs.filter((ref) => ref.role === role);
1485
- return {
1486
- role,
1487
- label: referenceRoleLabel(role, caps),
1488
- ...cap.description ? { description: cap.description } : {},
1489
- ..."accepts" in cap && cap.accepts?.length ? { accepts: [...cap.accepts] } : {},
1490
- min: cap.min ?? 0,
1491
- max: cap.max,
1492
- references,
1493
- canAdd: references.length < cap.max && total < maxTotal
1494
- };
1495
- });
1496
- }
1497
- function addReference(request, reference) {
1498
- return { ...request, references: [...request.references ?? [], { ...reference }] };
1499
- }
1500
- function removeReference(request, role, id) {
1501
- const references = (request.references ?? []).filter((ref) => !(ref.role === role && ref.id === id));
1502
- const next = { ...request, references };
1503
- if (references.length === 0) delete next.references;
1504
- return next;
1505
- }
1506
-
1507
- // src/generation/assets.ts
1508
- function toAssetResolver(lookup) {
1509
- if (!lookup) return void 0;
1510
- if (typeof lookup === "function") return (id) => lookup(id) ?? void 0;
1511
- if (lookup instanceof Map) return (id) => lookup.get(id);
1512
- const byId = new Map(lookup.map((asset) => [asset.id, asset]));
1513
- return (id) => byId.get(id);
1514
- }
1515
- function roleCap(caps, role) {
1516
- const cap = caps.references?.roles[role];
1517
- if (cap && cap.max > 0) return cap;
1518
- if (role === "mask" && caps.inpainting) return { max: 1 };
1519
- return void 0;
1520
- }
1521
- function getAssetRoleOptions(caps) {
1522
- const roles = supportedReferenceRoles(caps);
1523
- if (caps.inpainting && !roles.includes("mask")) roles.push("mask");
1524
- return roles.map((role) => {
1525
- const cap = roleCap(caps, role) ?? { max: 1 };
1526
- return {
1527
- role,
1528
- label: referenceRoleLabel(role, caps),
1529
- ...cap.description ? { description: cap.description } : {},
1530
- min: cap.min ?? 0,
1531
- max: cap.max,
1532
- ...cap.accepts?.length ? { accepts: [...cap.accepts] } : {}
1533
- };
1601
+ const resolved = resolvePrompt(rawPrompt, templates, { bindings, ...values ? { values } : {} });
1602
+ const sourceMap = resolved.map.map((segment) => {
1603
+ const { locked: _locked, ...rest } = segment;
1604
+ return rest;
1534
1605
  });
1535
- }
1536
- function validateAssetMentions(mentions, caps, options = {}) {
1537
- const resolve = toAssetResolver(options.assets);
1538
- const issues = [];
1539
- const counts = {};
1540
- mentions.forEach((mention, index) => {
1541
- const id = mention.id;
1542
- const label = `@${mention.label ?? id}`;
1543
- if (!mention.role) {
1544
- issues.push({ index, id, code: "missing-role", severity: "warning", message: `${label} has no role.` });
1545
- return;
1606
+ if (resolved.text !== provenance.resolvedPrompt && !overrides) {
1607
+ drift.push({
1608
+ type: "prompt-changed",
1609
+ severity: "info",
1610
+ from: provenance.resolvedPrompt,
1611
+ to: resolved.text,
1612
+ applied: true,
1613
+ message: "The resolved prompt differs from the recorded one."
1614
+ });
1615
+ }
1616
+ const base = clone2(mode === "exact" ? provenance.request : record.settings);
1617
+ const recordedPlanned = provenance.request;
1618
+ if (base.prompt === void 0 || base.prompt === provenance.resolvedPrompt) base.prompt = resolved.text;
1619
+ else if (base.prompt === provenance.rawPrompt) base.prompt = rawPrompt;
1620
+ if (mode === "latest" && record.mentions?.length) {
1621
+ const plannedRole = new Map((recordedPlanned.references ?? []).map((ref) => [ref.id, ref.role]));
1622
+ base.references = [
1623
+ ...base.references ?? [],
1624
+ ...record.mentions.map((m) => ({
1625
+ id: m.id,
1626
+ role: m.role || plannedRole.get(m.id) || "image_ref",
1627
+ ...m.url !== void 0 ? { url: m.url } : {},
1628
+ ...m.mediaType !== void 0 ? { mediaType: m.mediaType } : {},
1629
+ ...m.contentHash !== void 0 ? { contentHash: m.contentHash } : {}
1630
+ }))
1631
+ ];
1632
+ } else if (mode === "latest" && base.references === void 0 && recordedPlanned.references) {
1633
+ base.references = clone2(recordedPlanned.references);
1634
+ }
1635
+ if (record.seed !== void 0) base.seed = record.seed;
1636
+ base.model = provenance.modelId;
1637
+ const { given, caps } = findModel(options.capabilities, provenance.modelId);
1638
+ if (overrides) {
1639
+ if (overrides.settings) {
1640
+ const { params, ...rest } = overrides.settings;
1641
+ Object.assign(base, clone2(rest));
1642
+ if (params) base.params = { ...base.params ?? {}, ...clone2(params) };
1546
1643
  }
1547
- let role = mention.role;
1548
- if (!roleCap(caps, role)) {
1549
- const alias = normalizeReferenceRole(role, caps);
1550
- if (alias !== role && roleCap(caps, alias)) {
1551
- issues.push({
1552
- index,
1553
- id,
1554
- role,
1555
- code: "renamed-role",
1556
- severity: "warning",
1557
- suggestion: alias,
1558
- message: `${label}: this model calls "${role}" ${referenceRoleLabel(alias, caps)} (${alias}).`
1559
- });
1560
- role = alias;
1561
- } else {
1562
- issues.push({
1563
- index,
1564
- id,
1565
- role,
1566
- code: "unsupported-role",
1644
+ if (overrides.seed === null) delete base.seed;
1645
+ else base.seed = overrides.seed ?? randomSeed(caps, record.seed, overrides.random ?? Math.random);
1646
+ if (base.seed === void 0) delete base.seed;
1647
+ }
1648
+ const resolveAsset = toAssetResolver(options.assets);
1649
+ if (resolveAsset && base.references) {
1650
+ const recordedById = new Map(record.assets.map((a) => [refKey(a), a]));
1651
+ const kept = [];
1652
+ for (const ref of base.references) {
1653
+ const info = resolveAsset(ref.id);
1654
+ if (!info) {
1655
+ drift.push({
1656
+ type: "asset-missing",
1567
1657
  severity: "error",
1568
- message: `${label}: ${caps.name ?? caps.id} does not accept ${referenceRoleLabel(role, caps).toLowerCase()} assets.`
1658
+ assetId: ref.id,
1659
+ role: ref.role,
1660
+ message: `Asset "${ref.id}" (${ref.role}) is no longer available.`
1661
+ });
1662
+ continue;
1663
+ }
1664
+ const recordedHash = recordedById.get(refKey(ref))?.contentHash ?? ref.contentHash;
1665
+ if (recordedHash !== void 0 && info.contentHash !== void 0 && recordedHash !== info.contentHash) {
1666
+ drift.push({
1667
+ type: "asset-changed",
1668
+ severity: "warning",
1669
+ assetId: ref.id,
1670
+ role: ref.role,
1671
+ from: recordedHash,
1672
+ to: info.contentHash,
1673
+ applied: true,
1674
+ message: `Asset "${ref.id}" (${ref.role}) has changed since this generation.`
1569
1675
  });
1570
- return;
1571
1676
  }
1677
+ const next = { ...ref };
1678
+ if (info.contentHash !== void 0) next.contentHash = info.contentHash;
1679
+ if (next.mediaType === void 0 && info.mediaType !== void 0) next.mediaType = info.mediaType;
1680
+ kept.push(next);
1572
1681
  }
1573
- const cap = roleCap(caps, role);
1574
- const mediaType = mention.mediaType ?? resolve?.(id)?.mediaType;
1575
- if (mediaType !== void 0 && !mediaTypeMatches(mediaType, cap.accepts)) {
1576
- issues.push({
1577
- index,
1578
- id,
1579
- role,
1580
- code: "media-mismatch",
1581
- severity: "error",
1582
- message: `${label} is ${mediaType}; ${referenceRoleLabel(role, caps)} accepts ${(cap.accepts ?? []).join(", ")}.`
1682
+ if (kept.length) base.references = kept;
1683
+ else delete base.references;
1684
+ }
1685
+ let plan;
1686
+ let request = base;
1687
+ if (given && !caps) {
1688
+ drift.push({
1689
+ type: "model-missing",
1690
+ severity: "error",
1691
+ modelId: provenance.modelId,
1692
+ message: `Model "${provenance.modelId}" is no longer available.`
1693
+ });
1694
+ } else if (caps) {
1695
+ const capabilityHash = hashCapabilities(caps);
1696
+ if (record.capabilityHash !== void 0 && record.capabilityHash !== capabilityHash) {
1697
+ drift.push({
1698
+ type: "model-changed",
1699
+ severity: "warning",
1700
+ modelId: caps.id,
1701
+ from: record.capabilityHash,
1702
+ to: capabilityHash,
1703
+ message: `The capabilities of "${caps.name ?? caps.id}" changed since this generation.`
1583
1704
  });
1584
- return;
1585
1705
  }
1586
- counts[role] = (counts[role] ?? 0) + 1;
1587
- if (counts[role] > cap.max) {
1588
- issues.push({
1589
- index,
1590
- id,
1591
- role,
1592
- code: "too-many",
1593
- severity: "warning",
1594
- message: `${label}: at most ${cap.max} ${referenceRoleLabel(role, caps).toLowerCase()} asset(s); extra ones are left out.`
1706
+ plan = planGenerationRequest(base, caps, {
1707
+ strategy: options.strategy ?? record.strategy,
1708
+ resolvedPrompt: resolved.text
1709
+ });
1710
+ request = plan.request;
1711
+ const severity = mode === "exact" && !overrides ? "warning" : "info";
1712
+ const type = mode === "exact" && !overrides ? "setting-unsupported" : "setting-changed";
1713
+ const describeChange = (field, from, to) => {
1714
+ const adjustment = plan.adjustments.find((a) => a.field === field || a.field.startsWith(`${field}.`));
1715
+ drift.push({
1716
+ type,
1717
+ severity,
1718
+ field,
1719
+ from,
1720
+ to,
1721
+ applied: true,
1722
+ message: type === "setting-unsupported" ? `${field} ${show(from)} is no longer supported; using ${show(to)}.${adjustment ? ` ${adjustment.reason}` : ""}` : `${field} was ${show(from)}, now ${show(to)}.${adjustment ? ` ${adjustment.reason}` : ""}`
1723
+ });
1724
+ };
1725
+ if (!overrides) {
1726
+ for (const field of GENERATION_REQUEST_FIELDS) {
1727
+ if (SKIPPED_SETTING_FIELDS.has(field)) continue;
1728
+ const from = recordedPlanned[field];
1729
+ const to = request[field];
1730
+ if (!sameValue(from, to)) describeChange(field, from, to);
1731
+ }
1732
+ const paramNames = /* @__PURE__ */ new Set([...Object.keys(recordedPlanned.params ?? {}), ...Object.keys(request.params ?? {})]);
1733
+ for (const name of [...paramNames].sort()) {
1734
+ const from = recordedPlanned.params?.[name];
1735
+ const to = request.params?.[name];
1736
+ if (!sameValue(from, to)) describeChange(`params.${name}`, from, to);
1737
+ }
1738
+ if (!sameValue(record.seed ?? recordedPlanned.seed, request.seed)) describeChange("seed", record.seed ?? recordedPlanned.seed, request.seed);
1739
+ const missing = new Set(drift.filter((d) => d.type === "asset-missing").map((d) => `${d.role}:${d.assetId}`));
1740
+ const fromRefs = (recordedPlanned.references ?? []).map(refKey).filter((key) => !missing.has(key));
1741
+ const toRefs = (request.references ?? []).map(refKey);
1742
+ if (!sameValue(fromRefs, toRefs)) describeChange("references", fromRefs, toRefs);
1743
+ }
1744
+ for (const error of plan.errors) {
1745
+ drift.push({
1746
+ type: "setting-unsupported",
1747
+ severity: "error",
1748
+ field: error.field,
1749
+ from: error.value,
1750
+ message: error.message
1595
1751
  });
1596
1752
  }
1597
- });
1598
- const missing = getAssetRoleOptions(caps).filter((option) => (counts[option.role] ?? 0) < option.min).map((option) => ({ role: option.role, min: option.min, count: counts[option.role] ?? 0 }));
1599
- return { ok: !issues.some((issue) => issue.severity === "error") && missing.length === 0, issues, counts, missing };
1600
- }
1601
- function resolveAssetMentions(mentions, assets) {
1602
- const byId = new Map(assets.map((asset) => [asset.id, asset]));
1603
- const byLabel = new Map(assets.filter((a) => a.label).map((asset) => [asset.label.toLowerCase(), asset]));
1604
- const result = [];
1605
- for (const mention of mentions) {
1606
- const asset = byId.get(mention.handle) ?? byLabel.get(mention.handle.toLowerCase());
1607
- if (!asset) continue;
1608
- result.push({
1609
- id: asset.id,
1610
- label: mention.handle,
1611
- ...mention.role ? { role: mention.role } : {},
1612
- ...asset.mediaType !== void 0 ? { mediaType: asset.mediaType } : {},
1613
- ...asset.contentHash !== void 0 ? { contentHash: asset.contentHash } : {},
1614
- ...asset.url !== void 0 ? { url: asset.url } : {}
1615
- });
1616
1753
  }
1617
- return result;
1618
- }
1619
- function defaultAssetRoleOptions() {
1620
- return REFERENCE_ROLES.map((role) => ({ role, label: referenceRoleLabel(role), min: 0, max: 1 }));
1754
+ const ok = !drift.some((d) => d.severity === "error") && (plan ? plan.ok : true);
1755
+ const recordOptions = {
1756
+ rawPrompt,
1757
+ templates,
1758
+ bindings,
1759
+ ...values ? { values } : {},
1760
+ model: caps ?? provenance.modelId,
1761
+ request,
1762
+ ...plan ? { plan } : {},
1763
+ strategy: options.strategy ?? record.strategy,
1764
+ parentId: record.id,
1765
+ kind
1766
+ };
1767
+ return {
1768
+ mode,
1769
+ ok,
1770
+ request,
1771
+ ...plan ? { plan } : {},
1772
+ rawPrompt,
1773
+ resolvedPrompt: resolved.text,
1774
+ sourceMap,
1775
+ values,
1776
+ templates,
1777
+ bindings,
1778
+ drift,
1779
+ parentId: record.id,
1780
+ kind,
1781
+ recordOptions
1782
+ };
1621
1783
  }
1622
- function describeAssetMentions(mentions, assets, caps) {
1623
- const byId = new Map(assets.map((asset) => [asset.id, asset]));
1624
- const byLabel = new Map(assets.filter((a) => a.label).map((asset) => [asset.label.toLowerCase(), asset]));
1625
- const chips = [];
1626
- for (const mention of mentions) {
1627
- const asset = byId.get(mention.handle) ?? byLabel.get(mention.handle.toLowerCase());
1628
- if (!asset) continue;
1629
- const role = mention.role || void 0;
1630
- chips.push({
1631
- index: chips.length,
1632
- ...mention.start !== void 0 ? { start: mention.start } : {},
1633
- handle: mention.handle,
1634
- asset,
1635
- ...role ? { role, roleLabel: referenceRoleLabel(caps ? normalizeReferenceRole(role, caps) : role, caps) } : {},
1636
- issues: []
1637
- });
1638
- }
1639
- if (caps) {
1640
- const validation = validateAssetMentions(
1641
- chips.map((chip) => ({
1642
- id: chip.asset.id,
1643
- label: chip.handle,
1644
- ...chip.role ? { role: chip.role } : {},
1645
- ...chip.asset.mediaType ? { mediaType: chip.asset.mediaType } : {}
1646
- })),
1647
- caps
1648
- );
1649
- for (const issue of validation.issues) chips[issue.index].issues.push(issue);
1650
- }
1651
- return chips;
1784
+ function recreateRequest(input, options) {
1785
+ if (isGenerationRecord(input)) return rebuild(input, options ?? {}, "recreate");
1786
+ const provenance = input;
1787
+ const request = clone2(provenance.request);
1788
+ request.model = provenance.modelId;
1789
+ if (request.prompt === void 0) request.prompt = provenance.resolvedPrompt;
1790
+ return request;
1652
1791
  }
1653
-
1654
- // src/generation/record.ts
1655
- var GENERATION_RECORD_FORMAT = "pixerate.generation-record";
1656
- var GENERATION_RECORD_VERSION = 1;
1657
- var idCounter = 0;
1658
- function createGenerationRecordId() {
1659
- const uuid = globalThis.crypto?.randomUUID?.();
1660
- if (typeof uuid === "string") return `gen_${uuid}`;
1661
- idCounter = (idCounter + 1) % 1679616;
1662
- const random = Math.floor(Math.random() * 2 ** 48).toString(36).padStart(10, "0");
1663
- return `gen_${Date.now().toString(36)}${idCounter.toString(36).padStart(4, "0")}${random}`;
1792
+ function variationRequest(record, overrides = {}, options = {}) {
1793
+ return rebuild(record, options, "variation", overrides);
1664
1794
  }
1665
- var clone2 = (value) => value === void 0 ? value : JSON.parse(JSON.stringify(value));
1666
- function recordTemplates(pinned) {
1667
- return pinned.map((p) => ({
1668
- name: p.name,
1669
- ...p.templateId !== void 0 ? { templateId: p.templateId } : {},
1670
- ...p.versionId !== void 0 ? { versionId: p.versionId, version: p.version } : {},
1671
- hash: hashText(p.body)
1672
- }));
1795
+ var DRIFT_TYPE_LABELS = {
1796
+ "template-changed": "Template changed",
1797
+ "template-missing": "Template deleted",
1798
+ "prompt-changed": "Prompt changed",
1799
+ "model-missing": "Model unavailable",
1800
+ "model-changed": "Model changed",
1801
+ "setting-unsupported": "Setting unsupported",
1802
+ "setting-changed": "Setting changed",
1803
+ "asset-missing": "Asset missing",
1804
+ "asset-changed": "Asset changed"
1805
+ };
1806
+ function blockingDrift(drift) {
1807
+ return drift.filter((d) => d.severity === "error");
1673
1808
  }
1674
- function recordAssets(references, lookup) {
1675
- const resolve = toAssetResolver(lookup);
1676
- return (references ?? []).map((ref) => {
1677
- const info = resolve?.(ref.id);
1678
- const contentHash = ref.contentHash ?? info?.contentHash;
1679
- const mediaType = ref.mediaType ?? info?.mediaType;
1680
- const url = ref.url ?? info?.url;
1681
- return {
1682
- id: ref.id,
1683
- role: ref.role,
1684
- ...contentHash !== void 0 ? { contentHash } : {},
1685
- ...mediaType !== void 0 ? { mediaType } : {},
1686
- ...url !== void 0 ? { url } : {}
1687
- };
1688
- });
1809
+
1810
+ // src/generation/history-tools.ts
1811
+ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
1812
+ var RECORD_ID = { type: "string", minLength: 1, description: "Generation record id from list_generations." };
1813
+ var read = (value) => typeof value === "function" ? value() : value;
1814
+ function forGenerate(request) {
1815
+ const { model, ...rest } = request;
1816
+ return { model, request: rest };
1817
+ }
1818
+ function rebuiltResult(result) {
1819
+ return {
1820
+ ok: result.ok,
1821
+ parentId: result.parentId,
1822
+ kind: result.kind,
1823
+ mode: result.mode,
1824
+ ...forGenerate(result.request),
1825
+ resolvedPrompt: result.resolvedPrompt,
1826
+ drift: result.drift.map(({ type, severity, message, field, template, assetId }) => ({
1827
+ type,
1828
+ severity,
1829
+ message,
1830
+ ...field !== void 0 ? { field } : {},
1831
+ ...template !== void 0 ? { template } : {},
1832
+ ...assetId !== void 0 ? { assetId } : {}
1833
+ })),
1834
+ ...result.plan ? { planErrors: result.plan.errors } : {}
1835
+ };
1689
1836
  }
1690
- function createGenerationRecord(options) {
1691
- const {
1692
- model,
1693
- request: settingsInput,
1694
- plan: planInput,
1695
- strategy: strategyInput,
1696
- mentions,
1697
- assets,
1698
- seed: seedInput,
1699
- id,
1700
- parentId,
1701
- kind,
1702
- ...provenanceOptions
1703
- } = options;
1704
- const caps = typeof model === "string" ? void 0 : model;
1705
- const settings = clone2(settingsInput ?? planInput?.request ?? {});
1706
- let plan = planInput;
1707
- if (!plan && caps) {
1708
- const resolved = resolvePrompt(options.rawPrompt, options.templates ?? [], {
1709
- bindings: options.bindings,
1710
- values: options.values,
1711
- maxDepth: options.maxDepth
1712
- });
1713
- plan = planGenerationRequest(settings, caps, {
1714
- strategy: strategyInput ?? "nearest",
1715
- resolvedPrompt: resolved.text,
1716
- ...mentions ? { mentions } : {}
1717
- });
1718
- }
1719
- const provenance = createProvenance({
1720
- ...provenanceOptions,
1721
- model,
1722
- ...plan ? { plan } : { request: settings }
1837
+ function createGenerationHistoryToolProvider(options) {
1838
+ const { store } = options;
1839
+ const recreateOptions = () => ({
1840
+ capabilities: read(options.models),
1841
+ templates: read(options.templates),
1842
+ assets: options.assets
1723
1843
  });
1724
- const seed = seedInput ?? provenance.request.seed;
1844
+ const recordOrThrow = async (id) => {
1845
+ const record = await store.get(id);
1846
+ if (!record) throw new Error(`Unknown generation "${id}". Call list_generations for record ids.`);
1847
+ return record;
1848
+ };
1849
+ const listGenerations = {
1850
+ name: "list_generations",
1851
+ title: "List past generations",
1852
+ description: "Lists recorded generations newest first: id, model, kind (generate, recreate, variation), parent id, time, templates, assets and the first 160 characters of the resolved prompt. Filter to find one to inspect, recreate or vary.",
1853
+ inputSchema: {
1854
+ type: "object",
1855
+ properties: {
1856
+ model: { type: "string", description: "Only this model id." },
1857
+ template: { type: "string", description: "Only records that used this template name or id." },
1858
+ text: { type: "string", description: "Case-insensitive text in the prompt preview." },
1859
+ kind: { type: "string", enum: ["generate", "recreate", "variation"], description: "Only this kind of record." },
1860
+ parentId: { type: "string", description: "Only records recreated or varied from this record." },
1861
+ limit: { type: "integer", minimum: 1, maximum: 100, description: "Most records to return (default 20)." },
1862
+ offset: { type: "integer", minimum: 0, description: "Records to skip, for paging (default 0)." }
1863
+ },
1864
+ additionalProperties: false
1865
+ },
1866
+ annotations: { title: "List past generations", ...READ },
1867
+ handler: async ({ limit = 20, offset = 0, ...filter }) => {
1868
+ const all = await store.summaries(filter);
1869
+ return { total: all.length, records: all.slice(offset, offset + limit) };
1870
+ }
1871
+ };
1872
+ const getGeneration = {
1873
+ name: "get_generation",
1874
+ title: "Read a past generation",
1875
+ description: "Returns one generation record: model, raw and resolved prompt, the settings as the user set them, the strategy, the seed used, the assets and templates it used, its parent and when it ran.",
1876
+ inputSchema: { type: "object", properties: { id: RECORD_ID }, required: ["id"], additionalProperties: false },
1877
+ annotations: { title: "Read a past generation", ...READ },
1878
+ handler: async ({ id }) => {
1879
+ const r = await recordOrThrow(id);
1880
+ return {
1881
+ id: r.id,
1882
+ kind: r.kind,
1883
+ ...r.parentId !== void 0 ? { parentId: r.parentId } : {},
1884
+ createdAt: r.createdAt,
1885
+ model: r.provenance.modelId,
1886
+ rawPrompt: r.provenance.rawPrompt,
1887
+ resolvedPrompt: r.provenance.resolvedPrompt,
1888
+ settings: r.settings,
1889
+ strategy: r.strategy,
1890
+ ...r.seed !== void 0 ? { seed: r.seed } : {},
1891
+ assets: r.assets,
1892
+ templates: r.templates.map(({ name, templateId, version }) => ({ name, templateId, version }))
1893
+ };
1894
+ }
1895
+ };
1896
+ const recreate = {
1897
+ name: "recreate_generation",
1898
+ title: "Rebuild a past generation",
1899
+ description: "Builds the request that reproduces a recorded generation, without running it. Returns `model` and `request` to pass to generate, `ok` (false when something blocks an exact reproduction), and drift: templates edited or deleted, the model changed or gone, settings no longer supported, assets missing or changed. Report drift to the user before generating.",
1900
+ inputSchema: {
1901
+ type: "object",
1902
+ properties: {
1903
+ id: RECORD_ID,
1904
+ mode: {
1905
+ type: "string",
1906
+ enum: ["exact", "latest"],
1907
+ description: "exact (default) reproduces the recorded prompt and settings; latest uses the current templates and re-plans the settings for the current model."
1908
+ }
1909
+ },
1910
+ required: ["id"],
1911
+ additionalProperties: false
1912
+ },
1913
+ annotations: { title: "Rebuild a past generation", ...READ },
1914
+ handler: async ({ id, mode }) => rebuiltResult(recreateRequest(await recordOrThrow(id), { ...recreateOptions(), mode }))
1915
+ };
1916
+ const vary = {
1917
+ name: "vary_generation",
1918
+ title: "Build a variation of a past generation",
1919
+ description: "Builds a request for a variation of a recorded generation, without running it: the same prompt, settings and assets with a new seed (random unless given) and any changes. Returns `model` and `request` to pass to generate, plus drift.",
1920
+ inputSchema: {
1921
+ type: "object",
1922
+ properties: {
1923
+ id: RECORD_ID,
1924
+ seed: { type: ["integer", "null"], description: "Seed to use; null for none. Default: a random seed different from the record's." },
1925
+ values: {
1926
+ type: "object",
1927
+ additionalProperties: { type: "string" },
1928
+ description: 'Prompt variable values merged over the recorded ones, e.g. {"subject": "a red fox"}.'
1929
+ },
1930
+ settings: {
1931
+ type: "object",
1932
+ description: `Request settings merged over the recorded ones, e.g. {"aspectRatio": "16:9"} (params are merged too). See list_models for each model's fields.`
1933
+ },
1934
+ rawPrompt: { type: "string", description: "A different raw prompt (templates and variables are resolved as usual)." }
1935
+ },
1936
+ required: ["id"],
1937
+ additionalProperties: false
1938
+ },
1939
+ annotations: { title: "Build a variation of a past generation", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1940
+ handler: async ({ id, seed, values, settings, rawPrompt }) => rebuiltResult(variationRequest(await recordOrThrow(id), { seed, values, settings, rawPrompt }, recreateOptions()))
1941
+ };
1725
1942
  return {
1726
- format: GENERATION_RECORD_FORMAT,
1727
- version: GENERATION_RECORD_VERSION,
1728
- id: id ?? createGenerationRecordId(),
1729
- ...parentId !== void 0 ? { parentId } : {},
1730
- kind: kind ?? (parentId !== void 0 ? "recreate" : "generate"),
1731
- provenance,
1732
- templates: recordTemplates(provenance.templates),
1733
- promptHash: hashText(provenance.resolvedPrompt),
1734
- ...caps ? { capabilityHash: hashCapabilities(caps) } : {},
1735
- settings,
1736
- strategy: strategyInput ?? plan?.strategy ?? "nearest",
1737
- ...seed !== void 0 ? { seed } : {},
1738
- assets: recordAssets(provenance.request.references, assets),
1739
- ...mentions?.length ? { mentions: clone2([...mentions]) } : {},
1740
- createdAt: provenance.createdAt
1943
+ id: options.id ?? "generation-history",
1944
+ namespace: options.namespace,
1945
+ tools: [listGenerations, getGeneration, recreate, vary],
1946
+ subscribe: (notify) => store.subscribe(notify)
1947
+ };
1948
+ }
1949
+
1950
+ // src/generation/batch.ts
1951
+ function createBatchResolver(options) {
1952
+ const maxBatch = Math.max(1, Math.floor(options.maxBatch ?? 50));
1953
+ const delayMs = Math.max(0, options.delayMs ?? 0);
1954
+ const settled = /* @__PURE__ */ new Map();
1955
+ const failures = /* @__PURE__ */ new Set();
1956
+ const inFlight = /* @__PURE__ */ new Map();
1957
+ let queue = [];
1958
+ let timer = null;
1959
+ const run = (keys) => {
1960
+ const pendings = keys.map((key) => inFlight.get(key));
1961
+ const finish = (values, error) => {
1962
+ if (values === null) options.onError?.(error, keys);
1963
+ keys.forEach((key, index) => {
1964
+ const pending = pendings[index];
1965
+ const value = values?.get(key);
1966
+ if (pending && inFlight.get(key) === pending) {
1967
+ inFlight.delete(key);
1968
+ settled.set(key, value);
1969
+ if (values === null) failures.add(key);
1970
+ }
1971
+ pending?.resolve(value);
1972
+ });
1973
+ };
1974
+ let promise;
1975
+ try {
1976
+ promise = Promise.resolve(options.fetch(keys));
1977
+ } catch (error) {
1978
+ promise = Promise.reject(error);
1979
+ }
1980
+ promise.then(
1981
+ (values) => finish(values instanceof Map ? values : /* @__PURE__ */ new Map()),
1982
+ (error) => finish(null, error ?? new Error("Batch fetch failed."))
1983
+ );
1984
+ };
1985
+ const flush = () => {
1986
+ if (timer !== null) clearTimeout(timer);
1987
+ timer = null;
1988
+ const keys = queue;
1989
+ queue = [];
1990
+ for (let i = 0; i < keys.length; i += maxBatch) run(keys.slice(i, i + maxBatch));
1991
+ };
1992
+ const load = (key) => {
1993
+ if (settled.has(key)) return Promise.resolve(settled.get(key));
1994
+ const existing = inFlight.get(key);
1995
+ if (existing) return existing.promise;
1996
+ let resolve;
1997
+ const promise = new Promise((r) => resolve = r);
1998
+ inFlight.set(key, { promise, resolve });
1999
+ queue.push(key);
2000
+ if (queue.length >= maxBatch) flush();
2001
+ else if (timer === null) timer = setTimeout(flush, delayMs);
2002
+ return promise;
2003
+ };
2004
+ return {
2005
+ load,
2006
+ loadMany: (keys) => Promise.all(keys.map(load)),
2007
+ peek: (key) => settled.get(key),
2008
+ has: (key) => settled.has(key),
2009
+ failed: (key) => failures.has(key),
2010
+ prime(key, value) {
2011
+ settled.set(key, value);
2012
+ failures.delete(key);
2013
+ },
2014
+ clear(key) {
2015
+ if (key === void 0) {
2016
+ settled.clear();
2017
+ failures.clear();
2018
+ for (const k of [...inFlight.keys()]) if (!queue.includes(k)) inFlight.delete(k);
2019
+ return;
2020
+ }
2021
+ settled.delete(key);
2022
+ failures.delete(key);
2023
+ if (!queue.includes(key)) inFlight.delete(key);
2024
+ },
2025
+ flush
1741
2026
  };
1742
2027
  }
1743
- function completeGenerationRecord(record, update) {
1744
- const next = clone2(record);
1745
- const now = new Date(update.completedAt ?? Date.now()).toISOString();
1746
- if (update.outputIds) next.provenance.outputIds = [...update.outputIds];
1747
- next.provenance.completedAt = now;
1748
- if (update.meta) next.provenance.meta = { ...next.provenance.meta ?? {}, ...clone2(update.meta) };
1749
- if (update.seed !== void 0) next.seed = update.seed;
1750
- next.updatedAt = now;
1751
- return next;
1752
- }
1753
- function serializeGenerationRecord(record) {
1754
- return JSON.stringify(record);
2028
+
2029
+ // src/generation/progress.ts
2030
+ var MAX_PENDING_PERCENT = 95;
2031
+ var ALMOST_DONE_MS = 5e3;
2032
+ var UNKNOWN_DURATION_TAU_MS = 3e4;
2033
+ var toMs = (value) => typeof value === "number" ? value : new Date(value).getTime();
2034
+ function formatRemaining(ms) {
2035
+ const seconds = Math.ceil(ms / 1e3);
2036
+ if (seconds < 60) return `~${Math.max(1, seconds)}s left`;
2037
+ const minutes = Math.ceil(ms / 6e4);
2038
+ if (minutes < 60) return `~${minutes}m left`;
2039
+ const hours = Math.floor(minutes / 60);
2040
+ const rest = minutes % 60;
2041
+ return rest ? `~${hours}h ${rest}m left` : `~${hours}h left`;
1755
2042
  }
1756
- var isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1757
- function parseGenerationRecord(input) {
1758
- let data = input;
1759
- if (typeof input === "string") {
1760
- try {
1761
- data = JSON.parse(input);
1762
- } catch {
1763
- throw new ProvenanceParseError("Generation record is not valid JSON.");
1764
- }
1765
- }
1766
- if (!isObject(data) || data.format !== GENERATION_RECORD_FORMAT) {
1767
- throw new ProvenanceParseError("Not a generation record.");
2043
+ function estimateJobProgress(input) {
2044
+ const started = toMs(input.startedAt);
2045
+ const now = toMs(input.now ?? Date.now());
2046
+ const elapsedMs = Math.max(0, now - started);
2047
+ if (input.done) return { percent: 100, label: "Done", elapsedMs, remainingMs: 0, overdue: false };
2048
+ if (!Number.isFinite(started) || now <= started) {
2049
+ return { percent: 0, label: "Starting\u2026", elapsedMs: 0, ...input.expectedMs ? { remainingMs: input.expectedMs } : {}, overdue: false };
1768
2050
  }
1769
- if (typeof data.version !== "number" || data.version > GENERATION_RECORD_VERSION) {
1770
- throw new ProvenanceParseError(`Unsupported generation record version ${JSON.stringify(data.version)}.`);
2051
+ const expected = input.expectedMs;
2052
+ if (!expected || !(expected > 0)) {
2053
+ const percent2 = MAX_PENDING_PERCENT * (1 - Math.exp(-elapsedMs / UNKNOWN_DURATION_TAU_MS));
2054
+ return { percent: Math.floor(percent2), label: "Working\u2026", elapsedMs, overdue: false };
1771
2055
  }
1772
- const checks = [
1773
- ["id", typeof data.id === "string" && data.id !== ""],
1774
- ["templates", Array.isArray(data.templates) && data.templates.every((t) => isObject(t) && typeof t.name === "string" && typeof t.hash === "string")],
1775
- ["assets", Array.isArray(data.assets) && data.assets.every((a) => isObject(a) && typeof a.id === "string" && typeof a.role === "string")],
1776
- ["settings", isObject(data.settings)],
1777
- ["promptHash", typeof data.promptHash === "string"],
1778
- ["createdAt", typeof data.createdAt === "string"]
1779
- ];
1780
- const bad = checks.find(([, ok]) => !ok);
1781
- if (bad) throw new ProvenanceParseError(`Invalid generation record field "${bad[0]}".`);
1782
- const provenance = parseProvenance(data.provenance);
2056
+ const remainingMs = Math.max(0, expected - elapsedMs);
2057
+ const percent = Math.min(MAX_PENDING_PERCENT, Math.floor(elapsedMs / expected * 100));
1783
2058
  return {
1784
- ...data,
1785
- kind: data.kind === "recreate" || data.kind === "variation" ? data.kind : "generate",
1786
- strategy: ["nearest", "upgrade", "downgrade", "error"].includes(data.strategy) ? data.strategy : "nearest",
1787
- provenance
2059
+ percent,
2060
+ label: remainingMs <= ALMOST_DONE_MS ? "Almost done\u2026" : formatRemaining(remainingMs),
2061
+ elapsedMs,
2062
+ remainingMs,
2063
+ overdue: elapsedMs > expected
1788
2064
  };
1789
2065
  }
1790
- function isGenerationRecord(value) {
1791
- return isObject(value) && value.format === GENERATION_RECORD_FORMAT;
1792
- }
1793
- var SKIPPED_SETTING_FIELDS = /* @__PURE__ */ new Set(["model", "prompt", "references", "params", "seed"]);
1794
- function findModel(capabilities, modelId) {
1795
- if (!capabilities) return { given: false };
1796
- const list2 = Array.isArray(capabilities) ? capabilities : [capabilities];
1797
- return { given: true, caps: list2.find((m) => m.id === modelId) };
1798
- }
1799
- var sameValue = (a, b) => stableStringify(a) === stableStringify(b);
1800
- var show = (value) => value === void 0 ? "unset" : typeof value === "string" ? `"${value}"` : JSON.stringify(value);
1801
- var refKey = (ref) => `${ref.role}:${ref.id}`;
1802
- function templateDrift(record, current, mode) {
1803
- if (!current) return [];
1804
- const byName = new Map(current.map((t) => [t.name, t]));
1805
- const drift = [];
1806
- for (const recorded of record.templates) {
1807
- const template = byName.get(recorded.name);
1808
- const fromVersion = recorded.version ?? recorded.versionId;
1809
- if (!template) {
1810
- drift.push({
1811
- type: "template-missing",
1812
- severity: "warning",
1813
- template: recorded.name,
1814
- from: fromVersion,
1815
- applied: false,
1816
- message: `Template "${recorded.name}" no longer exists; the recorded body is used.`
1817
- });
1818
- continue;
1819
- }
1820
- const body = getTemplateBody(template, {});
1821
- const hash = body === void 0 ? void 0 : hashText(body);
1822
- if (hash === recorded.hash) continue;
1823
- const latest = getLatestVersion(template);
1824
- const toVersion = template.versions?.length ? latest?.version ?? latest?.id : void 0;
1825
- const versions = fromVersion !== void 0 || toVersion !== void 0 ? ` (${fromVersion ?? "unversioned"} \u2192 ${toVersion ?? "unversioned"})` : "";
1826
- drift.push({
1827
- type: "template-changed",
1828
- severity: mode === "latest" ? "info" : "warning",
1829
- template: recorded.name,
1830
- from: { version: fromVersion, hash: recorded.hash },
1831
- to: { version: toVersion, hash },
1832
- applied: mode === "latest",
1833
- message: mode === "latest" ? `Template "${recorded.name}" changed since this generation${versions}; using the current version.` : `Template "${recorded.name}" changed since this generation${versions}; recreating with the recorded version.`
1834
- });
1835
- }
1836
- return drift;
2066
+ function expectedDurationFor(caps, request = {}) {
2067
+ const timing = caps.timing;
2068
+ if (!timing) return void 0;
2069
+ const count = request.count ?? caps.count?.default ?? 1;
2070
+ const duration = request.duration ?? caps.duration?.default ?? 0;
2071
+ const resolution = request.resolution ?? caps.resolutions?.default;
2072
+ const megapixels = resolution ? resolutionMegapixels(resolution) ?? 0 : 0;
2073
+ return Math.round(
2074
+ timing.baseMs + (timing.perItemMs ?? 0) * Math.max(0, count - 1) + (timing.perSecondMs ?? 0) * duration + (timing.perMegapixelMs ?? 0) * megapixels
2075
+ );
1837
2076
  }
1838
- function randomSeed(caps, previous, random) {
1839
- const cap = caps ? seedCapability(caps.seed) : {};
1840
- if (!cap) return void 0;
1841
- const min = Math.ceil(cap.min ?? 0);
1842
- const max = Math.floor(cap.max ?? 2 ** 31 - 1);
1843
- if (max < min) return min;
1844
- let seed = previous;
1845
- for (let attempt = 0; attempt < 8 && (seed === void 0 || seed === previous); attempt++) {
1846
- seed = min + Math.floor(random() * (max - min + 1));
1847
- }
1848
- if (seed === previous && max > min) seed = previous === max ? min : previous + 1;
1849
- return seed;
2077
+
2078
+ // src/generation/controls.ts
2079
+ function enumControl(key, label, cap) {
2080
+ if (!cap?.options.length) return null;
2081
+ return {
2082
+ key,
2083
+ label,
2084
+ kind: "select",
2085
+ valueType: "string",
2086
+ options: cap.options.map((value) => ({ value, label: cap.labels?.[value] ?? value })),
2087
+ ...cap.default !== void 0 ? { defaultValue: cap.default } : {}
2088
+ };
1850
2089
  }
1851
- function rebuild(record, options, kind, overrides) {
1852
- const mode = options.mode ?? "exact";
1853
- const provenance = record.provenance;
1854
- const drift = templateDrift(record, options.templates, mode);
1855
- const rawPrompt = overrides?.rawPrompt ?? provenance.rawPrompt;
1856
- const values = overrides?.values !== void 0 ? { ...provenance.values ?? {}, ...overrides.values } : provenance.values ? clone2(provenance.values) : null;
1857
- const pinned = pinnedTemplatesToTemplates(provenance.templates);
1858
- let templates;
1859
- let bindings;
1860
- if (mode === "latest" && options.templates) {
1861
- const currentNames = new Set(options.templates.map((t) => t.name));
1862
- const fallback = pinned.filter((t) => !currentNames.has(t.name));
1863
- templates = [...options.templates.map((t) => ({ ...t })), ...fallback];
1864
- bindings = {};
1865
- for (const t of fallback) {
1866
- if (provenance.bindings[t.name] !== void 0) bindings[t.name] = provenance.bindings[t.name];
1867
- }
1868
- } else {
1869
- templates = pinned;
1870
- bindings = { ...provenance.bindings };
1871
- }
1872
- const resolved = resolvePrompt(rawPrompt, templates, { bindings, ...values ? { values } : {} });
1873
- const sourceMap = resolved.map.map((segment) => {
1874
- const { locked: _locked, ...rest } = segment;
1875
- return rest;
1876
- });
1877
- if (resolved.text !== provenance.resolvedPrompt && !overrides) {
1878
- drift.push({
1879
- type: "prompt-changed",
1880
- severity: "info",
1881
- from: provenance.resolvedPrompt,
1882
- to: resolved.text,
1883
- applied: true,
1884
- message: "The resolved prompt differs from the recorded one."
2090
+ function getGenerationControls(caps) {
2091
+ const controls = [];
2092
+ const modes = supportedModes(caps);
2093
+ if (modes.length > 1) {
2094
+ controls.push({
2095
+ key: "mode",
2096
+ label: "Mode",
2097
+ kind: "select",
2098
+ valueType: "string",
2099
+ options: modes.map((m) => ({ value: m, label: m[0].toUpperCase() + m.slice(1) })),
2100
+ defaultValue: "generate"
1885
2101
  });
1886
2102
  }
1887
- const base = clone2(mode === "exact" ? provenance.request : record.settings);
1888
- const recordedPlanned = provenance.request;
1889
- if (base.prompt === void 0 || base.prompt === provenance.resolvedPrompt) base.prompt = resolved.text;
1890
- else if (base.prompt === provenance.rawPrompt) base.prompt = rawPrompt;
1891
- if (mode === "latest" && record.mentions?.length) {
1892
- const plannedRole = new Map((recordedPlanned.references ?? []).map((ref) => [ref.id, ref.role]));
1893
- base.references = [
1894
- ...base.references ?? [],
1895
- ...record.mentions.map((m) => ({
1896
- id: m.id,
1897
- role: m.role || plannedRole.get(m.id) || "image_ref",
1898
- ...m.url !== void 0 ? { url: m.url } : {},
1899
- ...m.mediaType !== void 0 ? { mediaType: m.mediaType } : {},
1900
- ...m.contentHash !== void 0 ? { contentHash: m.contentHash } : {}
1901
- }))
1902
- ];
1903
- } else if (mode === "latest" && base.references === void 0 && recordedPlanned.references) {
1904
- base.references = clone2(recordedPlanned.references);
1905
- }
1906
- if (record.seed !== void 0) base.seed = record.seed;
1907
- base.model = provenance.modelId;
1908
- const { given, caps } = findModel(options.capabilities, provenance.modelId);
1909
- if (overrides) {
1910
- if (overrides.settings) {
1911
- const { params, ...rest } = overrides.settings;
1912
- Object.assign(base, clone2(rest));
1913
- if (params) base.params = { ...base.params ?? {}, ...clone2(params) };
1914
- }
1915
- if (overrides.seed === null) delete base.seed;
1916
- else base.seed = overrides.seed ?? randomSeed(caps, record.seed, overrides.random ?? Math.random);
1917
- if (base.seed === void 0) delete base.seed;
2103
+ const negative = textFieldCapability(caps.negativePrompt);
2104
+ if (negative) controls.push({ key: "negativePrompt", label: "Negative prompt", kind: "textarea", valueType: "string", maxLength: negative.maxLength });
2105
+ const system = textFieldCapability(caps.systemPrompt);
2106
+ if (system) controls.push({ key: "systemPrompt", label: "System prompt", kind: "textarea", valueType: "string", maxLength: system.maxLength });
2107
+ controls.push(enumControl("aspectRatio", "Aspect ratio", caps.aspectRatios));
2108
+ controls.push(enumControl("resolution", "Resolution", caps.resolutions));
2109
+ controls.push(enumControl("quality", "Quality", caps.quality));
2110
+ if (caps.count) {
2111
+ const options = caps.count.options?.filter((o) => o >= 1 && o <= caps.count.max);
2112
+ controls.push(
2113
+ options?.length ? { key: "count", label: "Outputs", kind: "select", valueType: "number", options: options.map((o) => ({ value: o, label: String(o) })), defaultValue: caps.count.default } : { key: "count", label: "Outputs", kind: "range", valueType: "number", min: 1, max: caps.count.max, step: 1, defaultValue: caps.count.default ?? 1 }
2114
+ );
1918
2115
  }
1919
- const resolveAsset = toAssetResolver(options.assets);
1920
- if (resolveAsset && base.references) {
1921
- const recordedById = new Map(record.assets.map((a) => [refKey(a), a]));
1922
- const kept = [];
1923
- for (const ref of base.references) {
1924
- const info = resolveAsset(ref.id);
1925
- if (!info) {
1926
- drift.push({
1927
- type: "asset-missing",
1928
- severity: "error",
1929
- assetId: ref.id,
1930
- role: ref.role,
1931
- message: `Asset "${ref.id}" (${ref.role}) is no longer available.`
1932
- });
1933
- continue;
1934
- }
1935
- const recordedHash = recordedById.get(refKey(ref))?.contentHash ?? ref.contentHash;
1936
- if (recordedHash !== void 0 && info.contentHash !== void 0 && recordedHash !== info.contentHash) {
1937
- drift.push({
1938
- type: "asset-changed",
1939
- severity: "warning",
1940
- assetId: ref.id,
1941
- role: ref.role,
1942
- from: recordedHash,
1943
- to: info.contentHash,
1944
- applied: true,
1945
- message: `Asset "${ref.id}" (${ref.role}) has changed since this generation.`
1946
- });
2116
+ if (caps.duration) {
2117
+ const d = caps.duration;
2118
+ const options = d.options?.filter((o) => (d.min === void 0 || o >= d.min) && (d.max === void 0 || o <= d.max));
2119
+ controls.push(
2120
+ options?.length ? { key: "duration", label: "Duration (s)", kind: "select", valueType: "number", options: options.map((o) => ({ value: o, label: `${o}s` })), defaultValue: d.default } : {
2121
+ key: "duration",
2122
+ label: "Duration (s)",
2123
+ kind: d.min !== void 0 && d.max !== void 0 ? "range" : "number",
2124
+ valueType: "number",
2125
+ min: d.min,
2126
+ max: d.max,
2127
+ step: d.step,
2128
+ defaultValue: d.default
1947
2129
  }
1948
- const next = { ...ref };
1949
- if (info.contentHash !== void 0) next.contentHash = info.contentHash;
1950
- if (next.mediaType === void 0 && info.mediaType !== void 0) next.mediaType = info.mediaType;
1951
- kept.push(next);
1952
- }
1953
- if (kept.length) base.references = kept;
1954
- else delete base.references;
2130
+ );
1955
2131
  }
1956
- let plan;
1957
- let request = base;
1958
- if (given && !caps) {
1959
- drift.push({
1960
- type: "model-missing",
1961
- severity: "error",
1962
- modelId: provenance.modelId,
1963
- message: `Model "${provenance.modelId}" is no longer available.`
1964
- });
1965
- } else if (caps) {
1966
- const capabilityHash = hashCapabilities(caps);
1967
- if (record.capabilityHash !== void 0 && record.capabilityHash !== capabilityHash) {
1968
- drift.push({
1969
- type: "model-changed",
1970
- severity: "warning",
1971
- modelId: caps.id,
1972
- from: record.capabilityHash,
1973
- to: capabilityHash,
1974
- message: `The capabilities of "${caps.name ?? caps.id}" changed since this generation.`
1975
- });
1976
- }
1977
- plan = planGenerationRequest(base, caps, {
1978
- strategy: options.strategy ?? record.strategy,
1979
- resolvedPrompt: resolved.text
1980
- });
1981
- request = plan.request;
1982
- const severity = mode === "exact" && !overrides ? "warning" : "info";
1983
- const type = mode === "exact" && !overrides ? "setting-unsupported" : "setting-changed";
1984
- const describeChange = (field, from, to) => {
1985
- const adjustment = plan.adjustments.find((a) => a.field === field || a.field.startsWith(`${field}.`));
1986
- drift.push({
1987
- type,
1988
- severity,
1989
- field,
1990
- from,
1991
- to,
1992
- applied: true,
1993
- message: type === "setting-unsupported" ? `${field} ${show(from)} is no longer supported; using ${show(to)}.${adjustment ? ` ${adjustment.reason}` : ""}` : `${field} was ${show(from)}, now ${show(to)}.${adjustment ? ` ${adjustment.reason}` : ""}`
2132
+ const seed = seedCapability(caps.seed);
2133
+ if (seed) controls.push({ key: "seed", label: "Seed", kind: "number", valueType: "number", min: seed.min, max: seed.max, step: 1 });
2134
+ for (const [name, spec] of Object.entries(caps.params ?? {})) {
2135
+ const base = { key: `params.${name}`, label: spec.label ?? name, ...spec.description ? { description: spec.description } : {} };
2136
+ if (spec.type === "boolean") {
2137
+ controls.push({ ...base, kind: "checkbox", valueType: "boolean", defaultValue: spec.default });
2138
+ } else if (spec.type === "enum") {
2139
+ const numeric = spec.options.every((o) => typeof o === "number");
2140
+ controls.push({
2141
+ ...base,
2142
+ kind: "select",
2143
+ valueType: numeric ? "number" : "string",
2144
+ options: spec.options.map((value) => ({ value, label: String(value) })),
2145
+ defaultValue: spec.default
1994
2146
  });
1995
- };
1996
- if (!overrides) {
1997
- for (const field of GENERATION_REQUEST_FIELDS) {
1998
- if (SKIPPED_SETTING_FIELDS.has(field)) continue;
1999
- const from = recordedPlanned[field];
2000
- const to = request[field];
2001
- if (!sameValue(from, to)) describeChange(field, from, to);
2002
- }
2003
- const paramNames = /* @__PURE__ */ new Set([...Object.keys(recordedPlanned.params ?? {}), ...Object.keys(request.params ?? {})]);
2004
- for (const name of [...paramNames].sort()) {
2005
- const from = recordedPlanned.params?.[name];
2006
- const to = request.params?.[name];
2007
- if (!sameValue(from, to)) describeChange(`params.${name}`, from, to);
2008
- }
2009
- if (!sameValue(record.seed ?? recordedPlanned.seed, request.seed)) describeChange("seed", record.seed ?? recordedPlanned.seed, request.seed);
2010
- const missing = new Set(drift.filter((d) => d.type === "asset-missing").map((d) => `${d.role}:${d.assetId}`));
2011
- const fromRefs = (recordedPlanned.references ?? []).map(refKey).filter((key) => !missing.has(key));
2012
- const toRefs = (request.references ?? []).map(refKey);
2013
- if (!sameValue(fromRefs, toRefs)) describeChange("references", fromRefs, toRefs);
2014
- }
2015
- for (const error of plan.errors) {
2016
- drift.push({
2017
- type: "setting-unsupported",
2018
- severity: "error",
2019
- field: error.field,
2020
- from: error.value,
2021
- message: error.message
2147
+ } else {
2148
+ controls.push({
2149
+ ...base,
2150
+ kind: spec.min !== void 0 && spec.max !== void 0 ? "range" : "number",
2151
+ valueType: "number",
2152
+ min: spec.min,
2153
+ max: spec.max,
2154
+ step: spec.step ?? (spec.type === "integer" ? 1 : void 0),
2155
+ defaultValue: spec.default
2022
2156
  });
2023
2157
  }
2024
2158
  }
2025
- const ok = !drift.some((d) => d.severity === "error") && (plan ? plan.ok : true);
2026
- const recordOptions = {
2027
- rawPrompt,
2028
- templates,
2029
- bindings,
2030
- ...values ? { values } : {},
2031
- model: caps ?? provenance.modelId,
2032
- request,
2033
- ...plan ? { plan } : {},
2034
- strategy: options.strategy ?? record.strategy,
2035
- parentId: record.id,
2036
- kind
2037
- };
2038
- return {
2039
- mode,
2040
- ok,
2041
- request,
2042
- ...plan ? { plan } : {},
2043
- rawPrompt,
2044
- resolvedPrompt: resolved.text,
2045
- sourceMap,
2046
- values,
2047
- templates,
2048
- bindings,
2049
- drift,
2050
- parentId: record.id,
2051
- kind,
2052
- recordOptions
2053
- };
2159
+ return controls.filter((c) => c !== null);
2054
2160
  }
2055
- function recreateRequest(input, options) {
2056
- if (isGenerationRecord(input)) return rebuild(input, options ?? {}, "recreate");
2057
- const provenance = input;
2058
- const request = clone2(provenance.request);
2059
- request.model = provenance.modelId;
2060
- if (request.prompt === void 0) request.prompt = provenance.resolvedPrompt;
2061
- return request;
2161
+ function getRequestValue(request, key) {
2162
+ if (key.startsWith("params.")) return request.params?.[key.slice(7)];
2163
+ return request[key];
2062
2164
  }
2063
- function variationRequest(record, overrides = {}, options = {}) {
2064
- return rebuild(record, options, "variation", overrides);
2165
+ function setRequestValue(request, key, value) {
2166
+ const remove = value === void 0 || value === "" || typeof value === "number" && Number.isNaN(value);
2167
+ if (key.startsWith("params.")) {
2168
+ const name = key.slice(7);
2169
+ const params = { ...request.params ?? {} };
2170
+ if (remove) delete params[name];
2171
+ else params[name] = value;
2172
+ const next2 = { ...request, params };
2173
+ if (Object.keys(params).length === 0) delete next2.params;
2174
+ return next2;
2175
+ }
2176
+ const next = { ...request };
2177
+ if (remove) delete next[key];
2178
+ else next[key] = value;
2179
+ return next;
2065
2180
  }
2066
- var DRIFT_TYPE_LABELS = {
2067
- "template-changed": "Template changed",
2068
- "template-missing": "Template deleted",
2069
- "prompt-changed": "Prompt changed",
2070
- "model-missing": "Model unavailable",
2071
- "model-changed": "Model changed",
2072
- "setting-unsupported": "Setting unsupported",
2073
- "setting-changed": "Setting changed",
2074
- "asset-missing": "Asset missing",
2075
- "asset-changed": "Asset changed"
2076
- };
2077
- function blockingDrift(drift) {
2078
- return drift.filter((d) => d.severity === "error");
2181
+ function parseControlValue(control, raw) {
2182
+ if (control.valueType === "boolean") return Boolean(raw);
2183
+ if (raw === "" || raw === void 0) return void 0;
2184
+ if (control.valueType === "number") {
2185
+ const n = Number(raw);
2186
+ return Number.isFinite(n) ? n : void 0;
2187
+ }
2188
+ return String(raw);
2189
+ }
2190
+ function getReferenceSlots(caps, request = {}) {
2191
+ const roles = supportedReferenceRoles(caps);
2192
+ if (caps.inpainting && !roles.includes("mask")) roles.push("mask");
2193
+ const refs = request.references ?? [];
2194
+ const total = refs.length;
2195
+ const maxTotal = caps.references?.maxTotal ?? Infinity;
2196
+ return roles.map((role) => {
2197
+ const cap = caps.references?.roles[role] ?? { max: 1 };
2198
+ const references = refs.filter((ref) => ref.role === role);
2199
+ return {
2200
+ role,
2201
+ label: referenceRoleLabel(role, caps),
2202
+ ...cap.description ? { description: cap.description } : {},
2203
+ ..."accepts" in cap && cap.accepts?.length ? { accepts: [...cap.accepts] } : {},
2204
+ min: cap.min ?? 0,
2205
+ max: cap.max,
2206
+ references,
2207
+ canAdd: references.length < cap.max && total < maxTotal
2208
+ };
2209
+ });
2210
+ }
2211
+ function addReference(request, reference) {
2212
+ return { ...request, references: [...request.references ?? [], { ...reference }] };
2213
+ }
2214
+ function removeReference(request, role, id) {
2215
+ const references = (request.references ?? []).filter((ref) => !(ref.role === role && ref.id === id));
2216
+ const next = { ...request, references };
2217
+ if (references.length === 0) delete next.references;
2218
+ return next;
2079
2219
  }
2080
2220
 
2081
2221
  // src/generation/store.ts
@@ -2260,6 +2400,7 @@ export {
2260
2400
  capabilitiesToJsonSchema,
2261
2401
  completeGenerationRecord,
2262
2402
  createBatchResolver,
2403
+ createGenerationHistoryToolProvider,
2263
2404
  createGenerationRecord,
2264
2405
  createGenerationRecordId,
2265
2406
  createGenerationRecordStore,