wizards 0.2.16 → 0.2.18

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 (90) hide show
  1. package/apps/runtime/dist/agents/project-assistant.js +20 -2
  2. package/apps/runtime/dist/authoring/guide.js +6 -3
  3. package/apps/runtime/dist/credits/estimate.js +14 -8
  4. package/apps/runtime/dist/db/migrations/tenant/0008_run_event_note.sql +1 -0
  5. package/apps/runtime/dist/db/migrations/tenant/0009_knowledge.sql +104 -0
  6. package/apps/runtime/dist/db/migrations/tenant/meta/0008_snapshot.json +2019 -0
  7. package/apps/runtime/dist/db/migrations/tenant/meta/0009_snapshot.json +2837 -0
  8. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +14 -0
  9. package/apps/runtime/dist/db/schema.js +162 -8
  10. package/apps/runtime/dist/engine/events.js +14 -2
  11. package/apps/runtime/dist/engine/prepare.js +5 -2
  12. package/apps/runtime/dist/engine/requirements.js +108 -0
  13. package/apps/runtime/dist/engine/runner.js +2 -2
  14. package/apps/runtime/dist/engine/steps.js +21 -21
  15. package/apps/runtime/dist/engine/template.js +13 -10
  16. package/apps/runtime/dist/env.js +2 -0
  17. package/apps/runtime/dist/files/storage.js +15 -0
  18. package/apps/runtime/dist/models.js +38 -0
  19. package/apps/runtime/dist/plugins/loader.js +2 -0
  20. package/apps/runtime/dist/routes/studio.js +149 -8
  21. package/apps/runtime/dist/services/cloud.js +59 -1
  22. package/apps/runtime/dist/services/knowledge-model.js +558 -0
  23. package/apps/runtime/dist/services/knowledge.js +721 -0
  24. package/apps/runtime/dist/services/marketplace.js +2 -2
  25. package/apps/runtime/dist/services/project-files.js +114 -6
  26. package/apps/runtime/dist/services/project-index.js +632 -94
  27. package/apps/runtime/dist/services/relevance.js +90 -0
  28. package/apps/runtime/dist/services/space-categories.js +882 -0
  29. package/apps/runtime/dist/services/space-data.js +466 -38
  30. package/apps/runtime/dist/services/space-origin.js +164 -0
  31. package/apps/runtime/dist/services/spaces.js +155 -1
  32. package/apps/runtime/dist/tools/browser.js +12 -6
  33. package/apps/runtime/dist/tools/index.js +12 -6
  34. package/apps/runtime/dist/tools/mail.js +5 -2
  35. package/apps/runtime/dist/tools/pages.js +28 -23
  36. package/apps/runtime/dist/tools/project.js +132 -39
  37. package/apps/runtime/dist/tools/store.js +4 -4
  38. package/apps/runtime/dist/widgets/step.js +10 -4
  39. package/apps/web/dist/assets/{Account-BE_W101C.js → Account-Cp1cgqjV.js} +1 -1
  40. package/apps/web/dist/assets/{ChangelogDialog-DjKQJAos.js → ChangelogDialog-Chke1qUe.js} +1 -1
  41. package/apps/web/dist/assets/ChatPanel-DWrwewr6.js +1 -0
  42. package/apps/web/dist/assets/CreditsDialog-Ckema8Ss.js +1 -0
  43. package/apps/web/dist/assets/EditorPage-XX6pfHk5.js +2 -0
  44. package/apps/web/dist/assets/{FlowPreview-DK4RZqTW.js → FlowPreview-DS545n78.js} +1 -1
  45. package/apps/web/dist/assets/{Harness-DcI94f1U.js → Harness-H3yXP-2L.js} +2 -2
  46. package/apps/web/dist/assets/{HomePage-BBNFgryu.js → HomePage-Bds84ShK.js} +1 -1
  47. package/apps/web/dist/assets/{ImportWizard-BoYqovdl.js → ImportWizard-0_NkA8aS.js} +1 -1
  48. package/apps/web/dist/assets/LocalRuntime-DnABzTOe.js +1 -0
  49. package/apps/web/dist/assets/{NewWizardPage-tKV2S4eK.js → NewWizardPage-BqP75V9K.js} +2 -2
  50. package/apps/web/dist/assets/ProjectPage-DbOvwOdZ.js +5 -0
  51. package/apps/web/dist/assets/PublicRunner-CtZ7vxl_.js +1 -0
  52. package/apps/web/dist/assets/RunnerView-xV5bWclu.js +70 -0
  53. package/apps/web/dist/assets/SettingsPage-DKt55zFh.js +1 -0
  54. package/apps/web/dist/assets/{SetupPage-qxHvlf87.js → SetupPage-c6mMD9zN.js} +1 -1
  55. package/apps/web/dist/assets/SharePage-DTKIbMj-.js +1 -0
  56. package/apps/web/dist/assets/{api-t9_w1XNe.js → api-CVG-xDEn.js} +2 -2
  57. package/apps/web/dist/assets/changelog-CXXT0zsp.js +1 -0
  58. package/apps/web/dist/assets/{esm-D0YVMTdo.js → esm-DLM3jdbm.js} +1 -1
  59. package/apps/web/dist/assets/index-1gmaxDSk.js +10 -0
  60. package/apps/web/dist/assets/index-Bwyehu0k.css +1 -0
  61. package/apps/web/dist/assets/{meta-BJHYbMh7.js → meta-m947x6uG.js} +1 -1
  62. package/apps/web/dist/assets/{settings-menu-BqqUAvIY.js → settings-menu-DUY6__C3.js} +1 -1
  63. package/apps/web/dist/assets/ui-BPY2pIh0.js +1 -0
  64. package/apps/web/dist/index.html +4 -4
  65. package/apps/web/dist/mcp/flow.html +54 -54
  66. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +164 -0
  67. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +10 -0
  68. package/node_modules/@engenty-wizards/shared/dist/definition.js +25 -0
  69. package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +199 -0
  70. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +68 -0
  71. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +1 -16
  72. package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
  73. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +149 -0
  74. package/node_modules/@engenty-wizards/shared/dist/run.js +77 -1
  75. package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +20 -0
  76. package/node_modules/@engenty-wizards/shared/dist/space-data.js +2 -1
  77. package/package.json +1 -1
  78. package/apps/web/dist/assets/ChatPanel-BWrcDs4s.js +0 -1
  79. package/apps/web/dist/assets/CreditsDialog-DUjR5xkl.js +0 -1
  80. package/apps/web/dist/assets/EditorPage-CrOqikP2.js +0 -2
  81. package/apps/web/dist/assets/LocalRuntime-BD8zGN8w.js +0 -1
  82. package/apps/web/dist/assets/ProjectPage-voR0hvzy.js +0 -4
  83. package/apps/web/dist/assets/PublicRunner-CYZE_hSi.js +0 -1
  84. package/apps/web/dist/assets/RunnerView-D_C7hn81.js +0 -70
  85. package/apps/web/dist/assets/SettingsPage-DguoPF_3.js +0 -1
  86. package/apps/web/dist/assets/SharePage-Cf3i7KB-.js +0 -1
  87. package/apps/web/dist/assets/changelog-kz0DmHY4.js +0 -1
  88. package/apps/web/dist/assets/index-BPpKz65x.css +0 -1
  89. package/apps/web/dist/assets/index-D5w3fW1p.js +0 -10
  90. package/apps/web/dist/assets/ui-BHXO_e0e.js +0 -1
@@ -57,6 +57,20 @@
57
57
  "when": 1791287441521,
58
58
  "tag": "0007_space_data",
59
59
  "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "6",
64
+ "when": 1791307814904,
65
+ "tag": "0008_run_event_note",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "6",
71
+ "when": 1791311715390,
72
+ "tag": "0009_knowledge",
73
+ "breakpoints": true
60
74
  }
61
75
  ]
62
76
  }
@@ -1,5 +1,5 @@
1
1
  import { sql } from "drizzle-orm";
2
- import { blob, index, integer, primaryKey, sqliteTable, text, uniqueIndex, } from "drizzle-orm/sqlite-core";
2
+ import { blob, index, integer, primaryKey, real, sqliteTable, text, uniqueIndex, } from "drizzle-orm/sqlite-core";
3
3
  const now = sql `(unixepoch() * 1000)`;
4
4
  const createdAt = () => integer("created_at", { mode: "timestamp_ms" }).notNull().default(now);
5
5
  const updatedAt = () => integer("updated_at", { mode: "timestamp_ms" }).notNull().default(now);
@@ -129,6 +129,8 @@ export const runEvent = sqliteTable("run_event", {
129
129
  stepId: text("step_id"),
130
130
  type: text("type", { enum: ["step_started", "step_done", "tool", "info", "error"] }).notNull(),
131
131
  message: text("message").notNull(),
132
+ /** The message as a note of `RUN_NOTES`, for a runner in another language. */
133
+ note: text("note", { mode: "json" }).$type(),
132
134
  /** A picture of the run the event is about; shown while the step works. */
133
135
  assetId: text("asset_id"),
134
136
  createdAt: createdAt(),
@@ -194,14 +196,23 @@ export const projectFile = sqliteTable("project_file", {
194
196
  chars: integer("chars"),
195
197
  /** What the index holds of a document; `embeddings` includes the keywords. */
196
198
  indexed: text("indexed", { enum: ["embeddings", "keywords"] }),
199
+ /** A plugin's file (`server.spaceData.putFile`): the plugin, its key and what it calls it. */
200
+ origin: text("origin"),
201
+ originKey: text("origin_key"),
202
+ originLabel: text("origin_label"),
197
203
  createdAt: createdAt(),
198
204
  updatedAt: updatedAt(),
199
- }, (t) => [index("project_file_project").on(t.projectId, t.kind, t.position)]);
205
+ }, (t) => [
206
+ index("project_file_project").on(t.projectId, t.kind, t.position),
207
+ uniqueIndex("project_file_origin").on(t.projectId, t.origin, t.originKey),
208
+ ]);
200
209
  /**
201
- * The document index: passages of a project's documents, and of the texts plugins put in
202
- * (`server.index`). Keywords live in the FTS5 table `project_chunk_fts`, which triggers keep
203
- * in step (see the migrations); `embedding` is the passage's vector as float32 bytes, compared
204
- * with libSQL's `vector_distance_cos`.
210
+ * The index of Wissen: one unit per section of a page, row of a table, file, summary of a
211
+ * Kategorie's value, passage of a document read before pages, and passage of a plugin's text
212
+ * (`server.index`). Each unit's text starts with where it stands and its Kategorien. Keywords
213
+ * live in two FTS5 tables, words (`project_chunk_fts`) and trigrams (`project_chunk_tri`), which
214
+ * triggers keep in step (see the migrations); `embedding` is the unit's vector as float32
215
+ * bytes, compared with libSQL's `vector_distance_cos`.
205
216
  */
206
217
  export const projectChunk = sqliteTable("project_chunk", {
207
218
  id: integer("id").primaryKey({ autoIncrement: true }),
@@ -210,6 +221,23 @@ export const projectChunk = sqliteTable("project_chunk", {
210
221
  .references(() => project.id, { onDelete: "cascade" }),
211
222
  /** The document the passage is of; null for a plugin's text. */
212
223
  fileId: text("file_id").references(() => projectFile.id, { onDelete: "cascade" }),
224
+ /** What the unit is of: a page, a row (with its table), a table (FAQ format: a row), a value. */
225
+ pageId: text("page_id").references(() => spacePage.id, {
226
+ onDelete: "cascade",
227
+ }),
228
+ rowId: text("row_id").references(() => spaceTableRow.id, {
229
+ onDelete: "cascade",
230
+ }),
231
+ tableId: text("table_id").references(() => spaceTable.id, {
232
+ onDelete: "cascade",
233
+ }),
234
+ valueId: text("value_id").references(() => spaceCategoryValue.id, {
235
+ onDelete: "cascade",
236
+ }),
237
+ /** A section's headings, outermost first, joined by " › ". */
238
+ heading: text("heading"),
239
+ /** The page of the original a section starts on (`<!-- S. 3 -->`). */
240
+ sheet: integer("sheet"),
213
241
  /** A plugin's text: the plugin, its key for the text, its title and where the studio shows it. */
214
242
  plugin: text("plugin"),
215
243
  ref: text("ref"),
@@ -224,6 +252,10 @@ export const projectChunk = sqliteTable("project_chunk", {
224
252
  index("project_chunk_project").on(t.projectId, t.model),
225
253
  index("project_chunk_file").on(t.fileId),
226
254
  index("project_chunk_ref").on(t.projectId, t.plugin, t.ref),
255
+ index("project_chunk_page").on(t.pageId),
256
+ index("project_chunk_row").on(t.rowId),
257
+ index("project_chunk_table").on(t.tableId),
258
+ index("project_chunk_value").on(t.valueId),
227
259
  ]);
228
260
  // --- The space's data -------------------------------------------------------
229
261
  // Tables and pages a space keeps, its own or one of its wizards'. Shared by everyone who works
@@ -245,12 +277,30 @@ export const spaceTable = sqliteTable("space_table", {
245
277
  /** The column a row is matched on, as the list says. */
246
278
  keyColumn: text("key_column"),
247
279
  title: text("title").notNull(),
280
+ /** Its name in a path (`tables/<slug>`), unique among the space's tables. */
281
+ slug: text("slug"),
282
+ /** `faq`: a question and its answer per row. */
283
+ format: text("format", { enum: ["faq"] }),
248
284
  columns: text("columns", { mode: "json" }).$type().notNull(),
285
+ /**
286
+ * Written by a plugin (`server.spaceData`): the plugin, its key for the item and what it
287
+ * calls where it came from ("Tagesberichte"). Null: made in the studio or uploaded.
288
+ */
289
+ origin: text("origin"),
290
+ originKey: text("origin_key"),
291
+ originLabel: text("origin_label"),
292
+ /** A person changed what its origin wrote: the origin no longer writes it. */
293
+ kept: integer("kept", { mode: "boolean" }).notNull().default(false),
294
+ /** Why a person should look at it ("hardly readable"); null when nothing is in doubt. */
295
+ review: text("review"),
296
+ /** The file it was read from, kept as the evidence. */
297
+ fileId: text("file_id").references(() => projectFile.id, { onDelete: "set null" }),
249
298
  createdAt: createdAt(),
250
299
  updatedAt: updatedAt(),
251
300
  }, (t) => [
252
301
  index("space_table_project").on(t.projectId, t.wizardId),
253
302
  uniqueIndex("space_table_list").on(t.wizardId, t.list),
303
+ uniqueIndex("space_table_origin").on(t.projectId, t.origin, t.originKey),
254
304
  ]);
255
305
  /** A row of a table: cells by column id. Rows stand in the order they were added. */
256
306
  export const spaceTableRow = sqliteTable("space_table_row", {
@@ -267,7 +317,10 @@ export const spaceTableRow = sqliteTable("space_table_row", {
267
317
  index("space_table_row_table").on(t.tableId, t.createdAt),
268
318
  index("space_table_row_key").on(t.tableId, t.key),
269
319
  ]);
270
- /** A page: a title and markdown, what the editor shows and an agent reads and writes. */
320
+ /**
321
+ * A page: a title and markdown, what the editor shows and an agent reads and writes. A page of
322
+ * the space's own may have sub-pages: a long document split along its own headings.
323
+ */
271
324
  export const spacePage = sqliteTable("space_page", {
272
325
  id: text("id").primaryKey(),
273
326
  tenantId: text("tenant_id").notNull(),
@@ -275,11 +328,112 @@ export const spacePage = sqliteTable("space_page", {
275
328
  .notNull()
276
329
  .references(() => project.id, { onDelete: "cascade" }),
277
330
  wizardId: text("wizard_id").references(() => wizard.id, { onDelete: "cascade" }),
331
+ parentId: text("parent_id").references(() => spacePage.id, {
332
+ onDelete: "cascade",
333
+ }),
334
+ /** Its place among its parent's sub-pages. */
335
+ position: integer("position").notNull().default(0),
278
336
  title: text("title").notNull(),
337
+ /** Its name in a path (`pages/<parent>/<slug>`), unique among its siblings. */
338
+ slug: text("slug"),
279
339
  markdown: text("markdown").notNull().default(""),
340
+ /**
341
+ * Written by a plugin (`server.spaceData`): the plugin, its key for the item and what it
342
+ * calls where it came from ("Tagesberichte"). Null: made in the studio or uploaded.
343
+ */
344
+ origin: text("origin"),
345
+ originKey: text("origin_key"),
346
+ originLabel: text("origin_label"),
347
+ /** A person changed what its origin wrote: the origin no longer writes it. */
348
+ kept: integer("kept", { mode: "boolean" }).notNull().default(false),
349
+ /** Why a person should look at it ("hardly readable"); null when nothing is in doubt. */
350
+ review: text("review"),
351
+ /** The file it was read from, kept as the evidence. */
352
+ fileId: text("file_id").references(() => projectFile.id, { onDelete: "set null" }),
280
353
  createdAt: createdAt(),
281
354
  updatedAt: updatedAt(),
282
- }, (t) => [index("space_page_project").on(t.projectId, t.wizardId)]);
355
+ }, (t) => [
356
+ index("space_page_project").on(t.projectId, t.wizardId),
357
+ index("space_page_parent").on(t.parentId, t.position),
358
+ uniqueIndex("space_page_origin").on(t.projectId, t.origin, t.originKey),
359
+ ]);
360
+ // --- Kategorien -----------------------------------------------------------------
361
+ // Typed properties that run across Wissen: set on pages, rows, tables and files, any number of
362
+ // each, so a step filters before it searches.
363
+ /** A Kategorie of the space: a name, a type and, for a choice, the values it has met. */
364
+ export const spaceCategory = sqliteTable("space_category", {
365
+ id: text("id").primaryKey(),
366
+ tenantId: text("tenant_id").notNull(),
367
+ projectId: text("project_id")
368
+ .notNull()
369
+ .references(() => project.id, { onDelete: "cascade" }),
370
+ name: text("name").notNull(),
371
+ type: text("type", { enum: ["choice", "date", "number", "text", "boolean"] }).notNull(),
372
+ /** A number's unit: `h`, `€`, `kg`. */
373
+ unit: text("unit"),
374
+ /** A choice: several values per item. */
375
+ multiple: integer("multiple", { mode: "boolean" }).notNull().default(false),
376
+ /** A choice whose values are ranked (niedrig, mittel, hoch): their position is the rank. */
377
+ ordered: integer("ordered", { mode: "boolean" }).notNull().default(false),
378
+ /** Where its values come from, said in a few words ("Kopfzeile", "Spalte WLL"). */
379
+ hint: text("hint"),
380
+ /** The plugin that brought it; null: made in the studio. */
381
+ origin: text("origin"),
382
+ /** Proposed, not yet taken: shown to be confirmed, never filled. */
383
+ proposed: integer("proposed", { mode: "boolean" }).notNull().default(false),
384
+ /** A column of a table whose cells are its values for the table's rows. */
385
+ tableId: text("table_id").references(() => spaceTable.id, { onDelete: "set null" }),
386
+ columnId: text("column_id"),
387
+ position: integer("position").notNull().default(0),
388
+ createdAt: createdAt(),
389
+ updatedAt: updatedAt(),
390
+ }, (t) => [uniqueIndex("space_category_name").on(t.projectId, t.name)]);
391
+ /** A value of a choice: grows as items arrive; its summary is the Übersicht of its items. */
392
+ export const spaceCategoryValue = sqliteTable("space_category_value", {
393
+ id: text("id").primaryKey(),
394
+ categoryId: text("category_id")
395
+ .notNull()
396
+ .references(() => spaceCategory.id, { onDelete: "cascade" }),
397
+ value: text("value").notNull(),
398
+ position: integer("position").notNull().default(0),
399
+ /** The Übersicht: Markdown a model wrote of the value's items, when it last did. */
400
+ summary: text("summary"),
401
+ summaryAt: integer("summary_at", { mode: "timestamp_ms" }),
402
+ createdAt: createdAt(),
403
+ }, (t) => [uniqueIndex("space_category_value_value").on(t.categoryId, t.value)]);
404
+ /**
405
+ * A Kategorie set on an item: exactly one of page, row, table and file. A choice points to its
406
+ * value; the other types keep theirs typed: a date as milliseconds, a number, a short text, yes
407
+ * or no. A choice with several values has a row per value.
408
+ */
409
+ export const spaceItemCategory = sqliteTable("space_item_category", {
410
+ id: integer("id").primaryKey({ autoIncrement: true }),
411
+ projectId: text("project_id")
412
+ .notNull()
413
+ .references(() => project.id, { onDelete: "cascade" }),
414
+ categoryId: text("category_id")
415
+ .notNull()
416
+ .references(() => spaceCategory.id, { onDelete: "cascade" }),
417
+ pageId: text("page_id").references(() => spacePage.id, { onDelete: "cascade" }),
418
+ rowId: text("row_id").references(() => spaceTableRow.id, { onDelete: "cascade" }),
419
+ tableId: text("table_id").references(() => spaceTable.id, { onDelete: "cascade" }),
420
+ fileId: text("file_id").references(() => projectFile.id, { onDelete: "cascade" }),
421
+ valueId: text("value_id").references(() => spaceCategoryValue.id, { onDelete: "cascade" }),
422
+ text: text("text"),
423
+ num: real("num"),
424
+ at: integer("at"),
425
+ bool: integer("bool", { mode: "boolean" }),
426
+ /** Who set it: a person, the item's origin (a source's structure), or a model reading it. */
427
+ by: text("by", { enum: ["person", "origin", "model"] })
428
+ .notNull()
429
+ .default("person"),
430
+ }, (t) => [
431
+ index("space_item_category_category").on(t.categoryId, t.valueId),
432
+ index("space_item_category_page").on(t.pageId),
433
+ index("space_item_category_row").on(t.rowId),
434
+ index("space_item_category_table").on(t.tableId),
435
+ index("space_item_category_file").on(t.fileId),
436
+ ]);
283
437
  // --- The wizard's store ------------------------------------------------------
284
438
  // What a wizard keeps between runs for one person. `holder` is that person: "u:<userId>" for a
285
439
  // signed-in admin, "v:<visitorId>" for an end user on a shared link.
@@ -1,13 +1,23 @@
1
1
  import { AsyncResource } from "node:async_hooks";
2
2
  import { EventEmitter } from "node:events";
3
+ import { noteText } from "@engenty-wizards/shared/run";
3
4
  import { and, desc, eq, gt } from "drizzle-orm";
4
5
  import { db, schema } from "../db/client.js";
5
6
  const bus = new EventEmitter();
6
7
  bus.setMaxListeners(0);
7
- export async function emitEvent(runId, stepId, type, message, asset) {
8
+ export async function emitEvent(runId, stepId, type, said, asset) {
9
+ const note = typeof said === "string" ? null : said;
10
+ const message = note ? noteText(note, "de") : said;
8
11
  const [row] = await db
9
12
  .insert(schema.runEvent)
10
- .values({ runId, stepId, type, message: message.slice(0, 2000), assetId: asset ?? null })
13
+ .values({
14
+ runId,
15
+ stepId,
16
+ type,
17
+ message: message.slice(0, 2000),
18
+ note,
19
+ assetId: asset ?? null,
20
+ })
11
21
  .returning();
12
22
  const event = {
13
23
  id: row.id,
@@ -15,6 +25,7 @@ export async function emitEvent(runId, stepId, type, message, asset) {
15
25
  stepId: row.stepId,
16
26
  type: row.type,
17
27
  message: row.message,
28
+ note: row.note ?? null,
18
29
  asset: row.assetId,
19
30
  };
20
31
  bus.emit(runId, { runId, event });
@@ -41,6 +52,7 @@ export async function recentEvents(runId, afterId = 0, limit = 40) {
41
52
  stepId: r.stepId,
42
53
  type: r.type,
43
54
  message: r.message,
55
+ note: r.note ?? null,
44
56
  asset: r.assetId,
45
57
  }));
46
58
  }
@@ -28,7 +28,7 @@ export async function prepareInputs(ctx) {
28
28
  delete values[field.id];
29
29
  continue;
30
30
  }
31
- await ctx.emit("info", `Hört „${field.label}“ an …`);
31
+ await ctx.emit("info", { code: "listen", params: { label: field.label } });
32
32
  try {
33
33
  const transcript = await transcribeAudio({
34
34
  bytes: new Uint8Array(found.data),
@@ -45,7 +45,10 @@ export async function prepareInputs(ctx) {
45
45
  }
46
46
  // The step goes on without the words; the next one tries again.
47
47
  console.error(`[run ${ctx.runId}] transcribe ${field.id}`, err);
48
- await ctx.emit("info", `„${field.label}“ konnte nicht verschriftlicht werden.`);
48
+ await ctx.emit("info", {
49
+ code: "listenFailed",
50
+ params: { label: field.label },
51
+ });
49
52
  }
50
53
  }
51
54
  }
@@ -100,6 +100,114 @@ export function unavoidableFrom(def, from, known) {
100
100
  };
101
101
  return new Set(def.steps.filter((_, i) => !reachesEnd(i)).map((s) => s.id));
102
102
  }
103
+ /** Whether a rule matches given what is known: true, false, or null = it may go either way. */
104
+ function ruleHolds(rule, facts) {
105
+ const k = facts[rule.when.field];
106
+ const value = rule.when.value;
107
+ switch (rule.when.op) {
108
+ case "empty":
109
+ return k?.filled === undefined ? (k?.is !== undefined ? false : null) : !k.filled;
110
+ case "notEmpty":
111
+ return k?.filled === undefined ? (k?.is !== undefined ? true : null) : k.filled;
112
+ case "equals":
113
+ return k?.is !== undefined
114
+ ? k.is === String(value)
115
+ : k?.not.includes(String(value))
116
+ ? false
117
+ : null;
118
+ case "notEquals":
119
+ return k?.is !== undefined
120
+ ? k.is !== String(value)
121
+ : k?.not.includes(String(value))
122
+ ? true
123
+ : null;
124
+ case "in": {
125
+ const list = Array.isArray(value) ? value.map(String) : [];
126
+ if (k?.is !== undefined) {
127
+ return list.includes(k.is);
128
+ }
129
+ return list.every((v) => k?.not.includes(v)) ? false : null;
130
+ }
131
+ }
132
+ }
133
+ /** What is known once a rule did (`held`) or did not match. */
134
+ function learn(rule, held, facts) {
135
+ const field = rule.when.field;
136
+ const k = { ...(facts[field] ?? { not: [] }) };
137
+ k.not = [...k.not];
138
+ const value = String(rule.when.value);
139
+ const op = rule.when.op;
140
+ if ((op === "equals" && held) || (op === "notEquals" && !held)) {
141
+ k.is = value;
142
+ k.filled = true;
143
+ }
144
+ else if ((op === "equals" && !held) || (op === "notEquals" && held)) {
145
+ k.not.push(value);
146
+ }
147
+ else if (op === "in" && !held && Array.isArray(rule.when.value)) {
148
+ k.not.push(...rule.when.value.map(String));
149
+ }
150
+ else if (op === "empty" || op === "notEmpty") {
151
+ k.filled = (op === "notEmpty") === held;
152
+ }
153
+ return { ...facts, [field]: k };
154
+ }
155
+ /**
156
+ * The most a run can add up to along one way through the wizard. A branch remembers what it
157
+ * implies of the answers, so ways that contradict themselves (the 720p clips and the 480p
158
+ * ones) are not added up. Steps in `closed` are not passed (no model for them here); where that
159
+ * leaves no way to the end, they count again. A branch back to an earlier step is not followed
160
+ * twice.
161
+ */
162
+ export function mostAlongOnePath(def, cost, closed = new Set()) {
163
+ const index = new Map(def.steps.map((s, i) => [s.id, i]));
164
+ const end = def.steps.length;
165
+ const walk = (avoid) => {
166
+ const best = (i, facts, seen) => {
167
+ if (i >= end) {
168
+ return 0;
169
+ }
170
+ const step = def.steps[i];
171
+ if (i < 0 || avoid.has(step.id) || seen.has(i)) {
172
+ return null;
173
+ }
174
+ const passed = new Set(seen).add(i);
175
+ let most = null;
176
+ const consider = (j, known) => {
177
+ const rest = best(j, known, passed);
178
+ if (rest !== null && (most === null || rest > most)) {
179
+ most = rest;
180
+ }
181
+ };
182
+ // The rules are tried in order; the first that matches decides.
183
+ let known = facts;
184
+ let decided = false;
185
+ for (const rule of step.next ?? []) {
186
+ const holds = ruleHolds(rule, known);
187
+ if (holds === false) {
188
+ continue;
189
+ }
190
+ consider(rule.goto === "end" ? end : (index.get(rule.goto) ?? -1), learn(rule, true, known));
191
+ if (holds === true) {
192
+ decided = true;
193
+ break;
194
+ }
195
+ known = learn(rule, false, known);
196
+ }
197
+ if (!decided) {
198
+ consider(i + 1, known);
199
+ }
200
+ return most === null ? null : most + cost(step);
201
+ };
202
+ return best(0, {}, new Set());
203
+ };
204
+ return walk(closed) ?? walk(new Set()) ?? def.steps.reduce((sum, s) => sum + cost(s), 0);
205
+ }
206
+ /** The steps that call a class this runtime cannot serve. */
207
+ export async function stepsWithoutModel(def) {
208
+ const missing = await missingClasses();
209
+ return new Set(def.steps.filter((s) => stepClasses(s).some((cls) => missing.has(cls))).map((s) => s.id));
210
+ }
103
211
  /** The classes this wizard needs that this runtime cannot serve; empty = every step can run. */
104
212
  export async function missingModels(def) {
105
213
  const unavoidable = unavoidableSteps(def);
@@ -20,7 +20,7 @@ import { askPerson, clearStaleAsk, unattended } from "./asks.js";
20
20
  import { emitEvent, recentEvents, signalChanged } from "./events.js";
21
21
  import { readPageInput } from "./input.js";
22
22
  import { pushRun } from "./push.js";
23
- import { blockingMessage, closedChoices, missingModels, openValue } from "./requirements.js";
23
+ import { blockingMessage, closedChoices, missingModels, openValue, stepsWithoutModel, } from "./requirements.js";
24
24
  import { releaseResources, resourcesFor } from "./resources.js";
25
25
  import { runAutomaticStep } from "./steps.js";
26
26
  import { resolveRef } from "./template.js";
@@ -161,7 +161,7 @@ export async function createRun(input) {
161
161
  throw new ModelUnavailableError(blocked);
162
162
  }
163
163
  // The run's expected cost is held before it starts; what it really costs is booked per call.
164
- const estimate = await estimateRun(input.wizardId, input.version, input.definition);
164
+ const estimate = await estimateRun(input.wizardId, input.version, input.definition, await stepsWithoutModel(input.definition));
165
165
  if (!(await reserveForRun(id, estimate.reserve))) {
166
166
  throw new NoCreditsError();
167
167
  }
@@ -1,4 +1,4 @@
1
- import { allFields, dataRef, isDecisionStep, templateRefs, } from "@engenty-wizards/shared/definition";
1
+ import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
2
2
  import { Agent } from "@mastra/core/agent";
3
3
  import { generateText, Output } from "ai";
4
4
  import { z } from "zod";
@@ -6,6 +6,7 @@ import { extFor, loadAsset, loadAssetText } from "../files/storage.js";
6
6
  import { generateImageMedia, generateSpeechMedia, generateVideoMedia, } from "../media/generate.js";
7
7
  import { markMedia } from "../media/marking.js";
8
8
  import { attachTools, costOf, isHarnessVendor, textModel } from "../models.js";
9
+ import { knowledgeBlock } from "../services/knowledge.js";
9
10
  import { buildStepTools } from "../tools/index.js";
10
11
  import { spaceContextOf } from "../tools/plugin.js";
11
12
  import { personUploads } from "../tools/store.js";
@@ -13,8 +14,13 @@ import { runWidgetStep } from "../widgets/step.js";
13
14
  import { prepareInputs } from "./prepare.js";
14
15
  import { answersAsText, colorLine, factLines, renderTemplate, resolveRef, } from "./template.js";
15
16
  import { StepError } from "./types.js";
16
- function today() {
17
- return new Date().toLocaleDateString("de-DE", { year: "numeric", month: "long", day: "numeric" });
17
+ /** Today's date as the wizard's people write it. */
18
+ function today(ctx) {
19
+ return new Date().toLocaleDateString(LOCALES[wizardLang(ctx.def)], {
20
+ year: "numeric",
21
+ month: "long",
22
+ day: "numeric",
23
+ });
18
24
  }
19
25
  function brandBlock(ctx) {
20
26
  const b = ctx.scope.brand;
@@ -26,16 +32,6 @@ function brandBlock(ctx) {
26
32
  ].filter(Boolean);
27
33
  return parts.length ? `# WHO THIS WIZARD BELONGS TO\n${parts.join("\n")}` : "";
28
34
  }
29
- /** The project's documents, for a step that has the tools to search and read them. */
30
- function documentsBlock(ctx) {
31
- const docs = ctx.projectFiles.filter((f) => f.kind === "document").slice(0, 40);
32
- if (!docs.length) {
33
- return "";
34
- }
35
- return `# THE PROJECT'S DOCUMENTS\nKnowledge the wizard's owner gave for all wizards. Find passages with project_search, read a whole document with project_document. Use them when the task touches what they cover.\n${docs
36
- .map((d) => `- ${d.name}${d.description ? ` — ${d.description}` : ""}`)
37
- .join("\n")}`;
38
- }
39
35
  /** Previous output of a step, for a revision. */
40
36
  async function previousResult(ctx, stepId) {
41
37
  const out = ctx.state.outputs[stepId];
@@ -166,9 +162,10 @@ export async function runAgentStep(step, ctx) {
166
162
  : "Finish with everything the result needs in your final answer; it is turned into structured data afterwards.";
167
163
  const system = [
168
164
  GROUND_RULES,
169
- `Today is ${today()}.`,
165
+ `Today is ${today(ctx)}.`,
170
166
  brandBlock(ctx),
171
- documentsBlock(ctx),
167
+ // Wissen: what the space holds for its wizards, and how to find and read it.
168
+ await knowledgeBlock(ctx.project.id),
172
169
  // What the space's plugins hold for its wizards: a wiki, questions and answers.
173
170
  ...(await spaceContextOf(ctx)).blocks,
174
171
  formatHint,
@@ -221,7 +218,7 @@ export async function runAgentStep(step, ctx) {
221
218
  // Working through a mailbox or a row of portals takes many small tool calls.
222
219
  const maxSteps = step.tools.includes("browser") || step.connections?.length ? 60 : 20;
223
220
  for (const [i, photo] of photos.entries()) {
224
- await ctx.emit("info", `Sieht sich Foto ${i + 1} von ${photos.length} an`, photo.id);
221
+ await ctx.emit("info", { code: "photo", params: { n: i + 1, total: photos.length } }, photo.id);
225
222
  }
226
223
  const message = photos.length
227
224
  ? [
@@ -381,7 +378,7 @@ async function writeHtml(step, ctx, kind) {
381
378
  ].join("\n");
382
379
  const system = [
383
380
  GROUND_RULES,
384
- `Today is ${today()}.`,
381
+ `Today is ${today(ctx)}.`,
385
382
  brandBlock(ctx),
386
383
  guide,
387
384
  images.length
@@ -474,8 +471,8 @@ async function runMediaStep(step, ctx, asset) {
474
471
  const todo = entries.filter((e) => !keep(e));
475
472
  if (asset === "video") {
476
473
  await ctx.emit("info", todo.length > 1
477
- ? `${todo.length} Clips werden gerendert – das dauert einige Minuten.`
478
- : "Das Video wird gerendert – das dauert meist 1–3 Minuten.");
474
+ ? { code: "clipsRendering", params: { count: todo.length } }
475
+ : { code: "videoRendering" });
479
476
  }
480
477
  let finished = 0;
481
478
  const note = ctx.state.notes[step.id];
@@ -496,7 +493,10 @@ async function runMediaStep(step, ctx, asset) {
496
493
  .join("\n")
497
494
  : await visualPrompt(step, ctx, entry);
498
495
  if (entry.reference && todo.length > 1) {
499
- await ctx.emit("info", `${asset === "image" ? "Bearbeitet" : "Dreht"} ${entry.index + 1} von ${entries.length}`, entry.reference);
496
+ await ctx.emit("info", {
497
+ code: asset === "image" ? "editing" : "filming",
498
+ params: { n: entry.index + 1, total: entries.length },
499
+ }, entry.reference);
500
500
  }
501
501
  const reference = await loadReference(entry.reference, ctx);
502
502
  const media = asset === "image"
@@ -529,7 +529,7 @@ async function runMediaStep(step, ctx, asset) {
529
529
  });
530
530
  finished++;
531
531
  if (todo.length > 1) {
532
- await ctx.emit("info", `${finished} von ${todo.length} fertig`, asset === "image" ? ref.id : undefined);
532
+ await ctx.emit("info", { code: "batchDone", params: { done: finished, total: todo.length } }, asset === "image" ? ref.id : undefined);
533
533
  }
534
534
  return { index: entry.index, prompt, ref };
535
535
  });
@@ -1,16 +1,17 @@
1
- import { allFields, isAudioValue, isLocationValue, itemsTotals, locationText, } from "@engenty-wizards/shared/definition";
1
+ import { allFields, isAudioValue, isLocationValue, itemsTotals, LOCALES, locationText, wizardLang, } from "@engenty-wizards/shared/definition";
2
2
  import { listAsMarkdown } from "@engenty-wizards/shared/store";
3
3
  const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
4
- function money(n, currency) {
4
+ function money(n, currency, locale) {
5
5
  try {
6
- return new Intl.NumberFormat("de-DE", { style: "currency", currency }).format(n);
6
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(n);
7
7
  }
8
8
  catch {
9
9
  return `${n.toFixed(2)} ${currency}`;
10
10
  }
11
11
  }
12
12
  /** Line items as a Markdown table with their totals — what a model needs to write a document. */
13
- export function itemsAsMarkdown(field, value, values) {
13
+ export function itemsAsMarkdown(field, value, values, lang = "de") {
14
+ const locale = LOCALES[lang];
14
15
  const totals = itemsTotals(field, value, values);
15
16
  const cols = field.columns ?? [];
16
17
  if (!totals.rows.length) {
@@ -18,12 +19,14 @@ export function itemsAsMarkdown(field, value, values) {
18
19
  }
19
20
  const head = `| ${cols.map((c) => c.label).join(" | ")} | Amount |\n|${cols.map(() => " --- ").join("|")}| ---: |`;
20
21
  const body = totals.rows.map((r) => `| ${cols
21
- .map((c) => c.kind === "money" ? money(Number(r[c.id]) || 0, totals.currency) : String(r[c.id] ?? ""))
22
- .join(" | ")} | ${money(r.amount, totals.currency)} |`);
22
+ .map((c) => c.kind === "money"
23
+ ? money(Number(r[c.id]) || 0, totals.currency, locale)
24
+ : String(r[c.id] ?? ""))
25
+ .join(" | ")} | ${money(r.amount, totals.currency, locale)} |`);
23
26
  const sums = [
24
- `Net: ${money(totals.net, totals.currency)}`,
25
- totals.vatRate ? `VAT ${totals.vatRate}%: ${money(totals.vat, totals.currency)}` : null,
26
- `Total: ${money(totals.gross, totals.currency)}`,
27
+ `Net: ${money(totals.net, totals.currency, locale)}`,
28
+ totals.vatRate ? `VAT ${totals.vatRate}%: ${money(totals.vat, totals.currency, locale)}` : null,
29
+ `Total: ${money(totals.gross, totals.currency, locale)}`,
27
30
  ].filter(Boolean);
28
31
  return [head, ...body, "", ...sums].join("\n");
29
32
  }
@@ -118,7 +121,7 @@ export function resolveRef(ref, scope) {
118
121
  const value = state.values[head];
119
122
  if (field?.kind === "items") {
120
123
  if (!rest.length) {
121
- return itemsAsMarkdown(field, value, state.values);
124
+ return itemsAsMarkdown(field, value, state.values, wizardLang(def));
122
125
  }
123
126
  const totals = itemsTotals(field, value, state.values);
124
127
  return totals[rest[0]];
@@ -158,6 +158,8 @@ export const env = {
158
158
  falKey: str("FAL_KEY"),
159
159
  elevenlabsKey: str("ELEVENLABS_API_KEY"),
160
160
  replicateKey: str("REPLICATE_API_TOKEN"),
161
+ /** TypeSafe's own API, for the classifier that checks what a search found (Jev). */
162
+ typesafeKey: str("TYPESAFE_API_KEY"),
161
163
  ollamaUrl: str("OLLAMA_URL", "http://127.0.0.1:11434/v1"),
162
164
  /** What each model class runs on when this runtime resolves classes itself (own keys, Ollama). */
163
165
  models: {
@@ -77,6 +77,21 @@ export async function inlineAssetRefs(html) {
77
77
  }
78
78
  return out;
79
79
  }
80
+ /** Gives an asset new content under the id it has: what points to it keeps pointing to it. */
81
+ export async function replaceAsset(id, input) {
82
+ const row = await db.query.asset.findFirst({ where: eq(schema.asset.id, id) });
83
+ if (!row) {
84
+ return;
85
+ }
86
+ const bytes = Buffer.from(input.data);
87
+ const file = `${id}-${nanoid(6)}.${extFor(input.mime)}`;
88
+ await objects.put(assetKey(file), bytes);
89
+ await db
90
+ .update(schema.asset)
91
+ .set({ mime: input.mime, name: input.name, path: file, size: bytes.byteLength })
92
+ .where(eq(schema.asset.id, id));
93
+ await objects.remove(assetKey(row.path));
94
+ }
80
95
  /** Deletes one asset: its file and its row. */
81
96
  export async function removeAsset(id) {
82
97
  const row = await db.query.asset.findFirst({ where: eq(schema.asset.id, id) });