wizards 0.2.16 → 0.2.17

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 (63) hide show
  1. package/apps/runtime/dist/credits/estimate.js +14 -8
  2. package/apps/runtime/dist/db/migrations/tenant/0008_run_event_note.sql +1 -0
  3. package/apps/runtime/dist/db/migrations/tenant/meta/0008_snapshot.json +2019 -0
  4. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
  5. package/apps/runtime/dist/db/schema.js +2 -0
  6. package/apps/runtime/dist/engine/events.js +14 -2
  7. package/apps/runtime/dist/engine/prepare.js +5 -2
  8. package/apps/runtime/dist/engine/requirements.js +108 -0
  9. package/apps/runtime/dist/engine/runner.js +2 -2
  10. package/apps/runtime/dist/engine/steps.js +18 -10
  11. package/apps/runtime/dist/engine/template.js +13 -10
  12. package/apps/runtime/dist/routes/studio.js +2 -2
  13. package/apps/runtime/dist/services/marketplace.js +2 -2
  14. package/apps/runtime/dist/tools/browser.js +12 -6
  15. package/apps/runtime/dist/tools/index.js +12 -6
  16. package/apps/runtime/dist/tools/mail.js +5 -2
  17. package/apps/runtime/dist/tools/pages.js +1 -1
  18. package/apps/runtime/dist/tools/project.js +5 -2
  19. package/apps/runtime/dist/tools/store.js +4 -4
  20. package/apps/runtime/dist/widgets/step.js +10 -4
  21. package/apps/web/dist/assets/{Account-BE_W101C.js → Account-LVXhagaS.js} +1 -1
  22. package/apps/web/dist/assets/{ChangelogDialog-DjKQJAos.js → ChangelogDialog-DM0KAoy6.js} +1 -1
  23. package/apps/web/dist/assets/ChatPanel-CxOQmtv-.js +1 -0
  24. package/apps/web/dist/assets/CreditsDialog-DkW3YT44.js +1 -0
  25. package/apps/web/dist/assets/EditorPage-hbisE1Wf.js +2 -0
  26. package/apps/web/dist/assets/{FlowPreview-DK4RZqTW.js → FlowPreview-v7or29ku.js} +1 -1
  27. package/apps/web/dist/assets/{Harness-DcI94f1U.js → Harness-RZF-bBmb.js} +2 -2
  28. package/apps/web/dist/assets/{HomePage-BBNFgryu.js → HomePage-mxG-a_KU.js} +1 -1
  29. package/apps/web/dist/assets/{ImportWizard-BoYqovdl.js → ImportWizard-CiPyUt3E.js} +1 -1
  30. package/apps/web/dist/assets/LocalRuntime-Dqp9wkN3.js +1 -0
  31. package/apps/web/dist/assets/{NewWizardPage-tKV2S4eK.js → NewWizardPage-DHwRsi0_.js} +2 -2
  32. package/apps/web/dist/assets/ProjectPage-Bb7jN_6n.js +4 -0
  33. package/apps/web/dist/assets/PublicRunner-TGkOwbAP.js +1 -0
  34. package/apps/web/dist/assets/RunnerView-DzHGshZj.js +70 -0
  35. package/apps/web/dist/assets/SettingsPage-CG3Xtfej.js +1 -0
  36. package/apps/web/dist/assets/{SetupPage-qxHvlf87.js → SetupPage-O1GfPlqX.js} +1 -1
  37. package/apps/web/dist/assets/SharePage-CCNUhtry.js +1 -0
  38. package/apps/web/dist/assets/{api-t9_w1XNe.js → api-DB22_B7G.js} +2 -2
  39. package/apps/web/dist/assets/changelog-BkIJdZ7M.js +1 -0
  40. package/apps/web/dist/assets/{esm-D0YVMTdo.js → esm-3TT9i_rv.js} +1 -1
  41. package/apps/web/dist/assets/index-CLe9Wrpu.js +10 -0
  42. package/apps/web/dist/assets/{meta-BJHYbMh7.js → meta-CEsBCPfA.js} +1 -1
  43. package/apps/web/dist/assets/{settings-menu-BqqUAvIY.js → settings-menu-pOcjznGJ.js} +1 -1
  44. package/apps/web/dist/assets/ui-I7QYMX0F.js +1 -0
  45. package/apps/web/dist/index.html +3 -3
  46. package/apps/web/dist/mcp/flow.html +53 -53
  47. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +10 -0
  48. package/node_modules/@engenty-wizards/shared/dist/definition.js +25 -0
  49. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +141 -0
  50. package/node_modules/@engenty-wizards/shared/dist/run.js +72 -1
  51. package/package.json +1 -1
  52. package/apps/web/dist/assets/ChatPanel-BWrcDs4s.js +0 -1
  53. package/apps/web/dist/assets/CreditsDialog-DUjR5xkl.js +0 -1
  54. package/apps/web/dist/assets/EditorPage-CrOqikP2.js +0 -2
  55. package/apps/web/dist/assets/LocalRuntime-BD8zGN8w.js +0 -1
  56. package/apps/web/dist/assets/ProjectPage-voR0hvzy.js +0 -4
  57. package/apps/web/dist/assets/PublicRunner-CYZE_hSi.js +0 -1
  58. package/apps/web/dist/assets/RunnerView-D_C7hn81.js +0 -70
  59. package/apps/web/dist/assets/SettingsPage-DguoPF_3.js +0 -1
  60. package/apps/web/dist/assets/SharePage-Cf3i7KB-.js +0 -1
  61. package/apps/web/dist/assets/changelog-kz0DmHY4.js +0 -1
  62. package/apps/web/dist/assets/index-D5w3fW1p.js +0 -10
  63. package/apps/web/dist/assets/ui-BHXO_e0e.js +0 -1
@@ -57,6 +57,13 @@
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
60
67
  }
61
68
  ]
62
69
  }
@@ -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(),
@@ -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";
@@ -13,8 +13,13 @@ import { runWidgetStep } from "../widgets/step.js";
13
13
  import { prepareInputs } from "./prepare.js";
14
14
  import { answersAsText, colorLine, factLines, renderTemplate, resolveRef, } from "./template.js";
15
15
  import { StepError } from "./types.js";
16
- function today() {
17
- return new Date().toLocaleDateString("de-DE", { year: "numeric", month: "long", day: "numeric" });
16
+ /** Today's date as the wizard's people write it. */
17
+ function today(ctx) {
18
+ return new Date().toLocaleDateString(LOCALES[wizardLang(ctx.def)], {
19
+ year: "numeric",
20
+ month: "long",
21
+ day: "numeric",
22
+ });
18
23
  }
19
24
  function brandBlock(ctx) {
20
25
  const b = ctx.scope.brand;
@@ -166,7 +171,7 @@ export async function runAgentStep(step, ctx) {
166
171
  : "Finish with everything the result needs in your final answer; it is turned into structured data afterwards.";
167
172
  const system = [
168
173
  GROUND_RULES,
169
- `Today is ${today()}.`,
174
+ `Today is ${today(ctx)}.`,
170
175
  brandBlock(ctx),
171
176
  documentsBlock(ctx),
172
177
  // What the space's plugins hold for its wizards: a wiki, questions and answers.
@@ -221,7 +226,7 @@ export async function runAgentStep(step, ctx) {
221
226
  // Working through a mailbox or a row of portals takes many small tool calls.
222
227
  const maxSteps = step.tools.includes("browser") || step.connections?.length ? 60 : 20;
223
228
  for (const [i, photo] of photos.entries()) {
224
- await ctx.emit("info", `Sieht sich Foto ${i + 1} von ${photos.length} an`, photo.id);
229
+ await ctx.emit("info", { code: "photo", params: { n: i + 1, total: photos.length } }, photo.id);
225
230
  }
226
231
  const message = photos.length
227
232
  ? [
@@ -381,7 +386,7 @@ async function writeHtml(step, ctx, kind) {
381
386
  ].join("\n");
382
387
  const system = [
383
388
  GROUND_RULES,
384
- `Today is ${today()}.`,
389
+ `Today is ${today(ctx)}.`,
385
390
  brandBlock(ctx),
386
391
  guide,
387
392
  images.length
@@ -474,8 +479,8 @@ async function runMediaStep(step, ctx, asset) {
474
479
  const todo = entries.filter((e) => !keep(e));
475
480
  if (asset === "video") {
476
481
  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.");
482
+ ? { code: "clipsRendering", params: { count: todo.length } }
483
+ : { code: "videoRendering" });
479
484
  }
480
485
  let finished = 0;
481
486
  const note = ctx.state.notes[step.id];
@@ -496,7 +501,10 @@ async function runMediaStep(step, ctx, asset) {
496
501
  .join("\n")
497
502
  : await visualPrompt(step, ctx, entry);
498
503
  if (entry.reference && todo.length > 1) {
499
- await ctx.emit("info", `${asset === "image" ? "Bearbeitet" : "Dreht"} ${entry.index + 1} von ${entries.length}`, entry.reference);
504
+ await ctx.emit("info", {
505
+ code: asset === "image" ? "editing" : "filming",
506
+ params: { n: entry.index + 1, total: entries.length },
507
+ }, entry.reference);
500
508
  }
501
509
  const reference = await loadReference(entry.reference, ctx);
502
510
  const media = asset === "image"
@@ -529,7 +537,7 @@ async function runMediaStep(step, ctx, asset) {
529
537
  });
530
538
  finished++;
531
539
  if (todo.length > 1) {
532
- await ctx.emit("info", `${finished} von ${todo.length} fertig`, asset === "image" ? ref.id : undefined);
540
+ await ctx.emit("info", { code: "batchDone", params: { done: finished, total: todo.length } }, asset === "image" ? ref.id : undefined);
533
541
  }
534
542
  return { index: entry.index, prompt, ref };
535
543
  });
@@ -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]];
@@ -16,7 +16,7 @@ import { layout } from "../cli/home.js";
16
16
  import { connectorImportSchema, importConnector, listConnectors, previewSource, refreshConnector, registryService, removeConnector, searchRegistry, } from "../connectors/external.js";
17
17
  import { balanceCredits, canSpend } from "../credits/credits.js";
18
18
  import { estimateRun } from "../credits/estimate.js";
19
- import { missingModels } from "../engine/requirements.js";
19
+ import { missingModels, stepsWithoutModel } from "../engine/requirements.js";
20
20
  import { env } from "../env.js";
21
21
  import { freshAuth, loginEnv } from "../harness/env.js";
22
22
  import { detectHarness, detectHarnesses, HARNESS_IDS, harness, installSpec, } from "../harness/index.js";
@@ -639,7 +639,7 @@ export const studio = new Hono()
639
639
  // What a run of the draft is expected to cost, per step and in total.
640
640
  .get("/wizards/:id/estimate", async (c) => {
641
641
  const w = await ownedWizard(c.get("user").id, c.req.param("id"));
642
- return c.json(await estimateRun(w.id, null, w.draft));
642
+ return c.json(await estimateRun(w.id, null, w.draft, await stepsWithoutModel(w.draft)));
643
643
  })
644
644
  // The models the draft needs that this runtime cannot serve, by class, with the steps that call them.
645
645
  .get("/wizards/:id/models", async (c) => {
@@ -5,7 +5,7 @@ import { searchEntries } from "@engenty-wizards/shared/marketplace-search";
5
5
  import { and, eq, inArray, notInArray } from "drizzle-orm";
6
6
  import { formulaEstimate } from "../credits/estimate.js";
7
7
  import { control, controlDb, db, schema, withTenant } from "../db/client.js";
8
- import { missingCapabilities, optionalCapabilities } from "../engine/requirements.js";
8
+ import { missingCapabilities, optionalCapabilities, stepsWithoutModel, } from "../engine/requirements.js";
9
9
  import { env } from "../env.js";
10
10
  import { managed } from "../manage.js";
11
11
  import { classCatalog, imageCostUsd, tokenCostUsd, videoCostUsd, WEB_SEARCH_COST_USD, } from "../models.js";
@@ -212,7 +212,7 @@ function listPrices() {
212
212
  /** What a run costs: at the gateway's prices where there is one, else at list prices. */
213
213
  async function priced(definition) {
214
214
  const catalog = await classCatalog().catch(() => null);
215
- const price = formulaEstimate(definition, catalog ?? listPrices());
215
+ const price = formulaEstimate(definition, catalog ?? listPrices(), await stepsWithoutModel(definition));
216
216
  return { credits: Math.round(price.credits), high: price.high };
217
217
  }
218
218
  /** An entry in a language with its wizard read against this app; null where there is none. */
@@ -141,7 +141,7 @@ export function browserTools(ctx, assets, files) {
141
141
  inputSchema: z.object({ url: z.string() }),
142
142
  execute: ({ url }) => inTurn(async () => {
143
143
  const safe = await assertPublicUrl(url);
144
- await ctx.emit("tool", `Öffnet ${safe.hostname}`);
144
+ await ctx.emit("tool", { code: "opens", params: { name: safe.hostname } });
145
145
  const p = await page();
146
146
  await p.goto(safe.toString(), { waitUntil: "domcontentloaded", timeout: 30_000 });
147
147
  await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => undefined);
@@ -154,7 +154,7 @@ export function browserTools(ctx, assets, files) {
154
154
  inputSchema: z.object({ ref: z.number() }),
155
155
  execute: ({ ref: n }) => inTurn(async () => {
156
156
  const p = await page();
157
- await ctx.emit("tool", "Klickt im Browser");
157
+ await ctx.emit("tool", { code: "clicks" });
158
158
  const popup = p
159
159
  .context()
160
160
  .waitForEvent("page", { timeout: 3000 })
@@ -176,7 +176,7 @@ export function browserTools(ctx, assets, files) {
176
176
  inputSchema: z.object({ ref: z.number(), text: z.string(), submit: z.boolean().optional() }),
177
177
  execute: ({ ref: n, text, submit }) => inTurn(async () => {
178
178
  const p = await page();
179
- await ctx.emit("tool", "Tippt im Browser");
179
+ await ctx.emit("tool", { code: "types" });
180
180
  const el = p.locator(ref(n)).first();
181
181
  await el.fill(text, { timeout: 10_000 });
182
182
  if (submit) {
@@ -192,7 +192,7 @@ export function browserTools(ctx, assets, files) {
192
192
  inputSchema: z.object({ save: z.boolean().optional(), name: z.string().optional() }),
193
193
  execute: ({ save, name }) => inTurn(async () => {
194
194
  const p = await page();
195
- await ctx.emit("tool", "Sieht sich die Seite an");
195
+ await ctx.emit("tool", { code: "looksAtPage" });
196
196
  const png = await p.screenshot({ type: "png" });
197
197
  let saved;
198
198
  if (save) {
@@ -235,7 +235,10 @@ export function browserTools(ctx, assets, files) {
235
235
  };
236
236
  }
237
237
  }
238
- await ctx.emit("tool", `Wartet auf deine Anmeldung bei ${new URL(origin).hostname}`);
238
+ await ctx.emit("tool", {
239
+ code: "waitsForSignIn",
240
+ params: { name: new URL(origin).hostname },
241
+ });
239
242
  const answer = await ctx.ask({
240
243
  kind: "login",
241
244
  reason: input.reason,
@@ -321,7 +324,10 @@ export function browserTools(ctx, assets, files) {
321
324
  }),
322
325
  execute: ({ ref: n, url, path, source }) => inTurn(async () => {
323
326
  const p = await page();
324
- await ctx.emit("tool", `Lädt ${path.split("/").pop()} herunter`);
327
+ await ctx.emit("tool", {
328
+ code: "downloads",
329
+ params: { name: path.split("/").pop() ?? "" },
330
+ });
325
331
  let data;
326
332
  let mime;
327
333
  if (url) {
@@ -49,7 +49,7 @@ export async function buildStepTools(step, ctx, resolved, uploads) {
49
49
  inputSchema: z.object({ url: z.string().describe("Absolute http(s) URL") }),
50
50
  execute: async ({ url }) => {
51
51
  const safe = await assertPublicUrl(url);
52
- await ctx.emit("tool", `Liest ${safe.hostname}`);
52
+ await ctx.emit("tool", { code: "reads", params: { name: safe.hostname } });
53
53
  const res = await safeFetch(safe.toString(), {
54
54
  signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(20_000)]),
55
55
  headers: { "user-agent": "Mozilla/5.0 (compatible; engenty-wizards/0.1)" },
@@ -115,7 +115,7 @@ export async function buildStepTools(step, ctx, resolved, uploads) {
115
115
  description: shellToolDescription(sandbox),
116
116
  inputSchema: z.object({ command: z.string(), timeoutSeconds: z.number().optional() }),
117
117
  execute: async ({ command, timeoutSeconds }) => {
118
- await ctx.emit("tool", `Führt aus: ${command.slice(0, 80)}`);
118
+ await ctx.emit("tool", { code: "runs", params: { command: command.slice(0, 80) } });
119
119
  return report(await ctx.resources
120
120
  .sandboxHandle()
121
121
  .exec(command, { timeoutMs: timeoutMs(timeoutSeconds) }));
@@ -131,7 +131,7 @@ export async function buildStepTools(step, ctx, resolved, uploads) {
131
131
  timeoutSeconds: z.number().optional(),
132
132
  }),
133
133
  execute: async ({ code, packages, timeoutSeconds }) => {
134
- await ctx.emit("tool", "Führt Python aus");
134
+ await ctx.emit("tool", { code: "python" });
135
135
  return report(await ctx.resources.sandboxHandle().runPython(code, {
136
136
  packages,
137
137
  timeoutMs: timeoutMs(timeoutSeconds),
@@ -174,7 +174,7 @@ export async function buildStepTools(step, ctx, resolved, uploads) {
174
174
  aspectRatio: z.enum(["1:1", "16:9", "9:16", "4:5", "3:2", "2:3"]).optional(),
175
175
  }),
176
176
  execute: async ({ prompt, aspectRatio }) => {
177
- await ctx.emit("tool", "Zeichnet ein Bild");
177
+ await ctx.emit("tool", { code: "drawing" });
178
178
  const media = await generateImageMedia({
179
179
  call: ctx.call,
180
180
  prompt,
@@ -225,10 +225,16 @@ export async function buildStepTools(step, ctx, resolved, uploads) {
225
225
  });
226
226
  try {
227
227
  Object.assign(tools, await mcp.listTools());
228
- await ctx.emit("tool", `Verbunden mit ${servers.map((s) => s.name).join(", ")}`);
228
+ await ctx.emit("tool", {
229
+ code: "mcpConnected",
230
+ params: { names: servers.map((s) => s.name).join(", ") },
231
+ });
229
232
  }
230
233
  catch (err) {
231
- await ctx.emit("info", `MCP-Server nicht erreichbar: ${err.message}`);
234
+ await ctx.emit("info", {
235
+ code: "mcpUnreachable",
236
+ params: { detail: err.message },
237
+ });
232
238
  }
233
239
  }
234
240
  return {
@@ -29,7 +29,7 @@ export function mailTools(step, ctx, files) {
29
29
  inputSchema: mailSearchInput,
30
30
  execute: (input) => attempt(async () => {
31
31
  const { connector, ctx: actionCtx, label } = await open();
32
- await ctx.emit("tool", `Durchsucht ${label}`);
32
+ await ctx.emit("tool", { code: "searchesMail", params: { label } });
33
33
  const { mails } = await mailAction(connector, "search", input, actionCtx);
34
34
  return {
35
35
  mailbox: label,
@@ -64,7 +64,10 @@ export function mailTools(step, ctx, files) {
64
64
  }),
65
65
  execute: ({ items }) => attempt(async () => {
66
66
  const { connector, ctx: actionCtx, label } = await open();
67
- await ctx.emit("tool", `Speichert ${items.length} Belege aus ${label}`);
67
+ await ctx.emit("tool", {
68
+ code: "savesReceipts",
69
+ params: { count: items.length, label },
70
+ });
68
71
  const saved = [];
69
72
  for (const item of items) {
70
73
  try {
@@ -43,7 +43,7 @@ export function pageTools(ctx) {
43
43
  }),
44
44
  execute: (input) => attempt(async () => {
45
45
  const page = await writeRunPage(projectId, wizardId, input);
46
- await ctx.emit("tool", `Schreibt die Seite „${page.title}“`);
46
+ await ctx.emit("tool", { code: "writesPage", params: { title: page.title } });
47
47
  return page;
48
48
  }),
49
49
  }),
@@ -21,7 +21,10 @@ export async function projectTools(ctx) {
21
21
  limit: z.number().int().min(1).max(12).optional(),
22
22
  }),
23
23
  execute: ({ query, limit }) => attempt(async () => {
24
- await ctx.emit("tool", `Sucht in den Dokumenten: ${query.slice(0, 60)}`);
24
+ await ctx.emit("tool", {
25
+ code: "searchesDocuments",
26
+ params: { query: query.slice(0, 60) },
27
+ });
25
28
  const hits = await searchProject(ctx.project.id, query, limit ?? 6);
26
29
  return {
27
30
  passages: hits.map((h) => ({ document: h.name, part: h.index, text: h.text })),
@@ -45,7 +48,7 @@ export async function projectTools(ctx) {
45
48
  if (!doc) {
46
49
  throw new Error(`No document "${name}". There are: ${docs.map((d) => d.name).join(", ")}`);
47
50
  }
48
- await ctx.emit("tool", `Liest ${doc.name}`);
51
+ await ctx.emit("tool", { code: "reads", params: { name: doc.name } });
49
52
  const text = await documentText(doc);
50
53
  const start = Math.min(from ?? 0, text.length);
51
54
  const part = text.slice(start, start + 12_000);
@@ -113,7 +113,7 @@ export function storeTools(ctx, uploads, keeper) {
113
113
  ? await saveRows(ctx.store, def, rows)
114
114
  : { added: 0, updated: 0 };
115
115
  const deleted = remove?.length ? await deleteRows(ctx.store, def, remove) : 0;
116
- await ctx.emit("tool", `Merkt sich ${def.title}`);
116
+ await ctx.emit("tool", { code: "remembers", params: { title: def.title } });
117
117
  return { ...saved, deleted };
118
118
  }
119
119
  catch (err) {
@@ -159,7 +159,7 @@ export function storeTools(ctx, uploads, keeper) {
159
159
  .max(40),
160
160
  }),
161
161
  execute: ({ items }) => attempt(async () => {
162
- await ctx.emit("tool", `Legt ${items.length} Dateien ab`);
162
+ await ctx.emit("tool", { code: "files", params: { count: items.length } });
163
163
  const kept = [];
164
164
  for (const item of items) {
165
165
  try {
@@ -190,7 +190,7 @@ export function storeTools(ctx, uploads, keeper) {
190
190
  }),
191
191
  execute: ({ file, offset }) => attempt(async () => {
192
192
  const source = await resolveFile(ctx, file);
193
- await ctx.emit("tool", `Liest ${source.name}`, file.startsWith("upload:") && source.mime.startsWith("image/")
193
+ await ctx.emit("tool", { code: "reads", params: { name: source.name } }, file.startsWith("upload:") && source.mime.startsWith("image/")
194
194
  ? file.slice("upload:".length)
195
195
  : undefined);
196
196
  const doc = await parseDocument(source, { charge, signal: ctx.signal, call: ctx.call });
@@ -212,7 +212,7 @@ export function storeTools(ctx, uploads, keeper) {
212
212
  kind: z.enum(["invoice", "receipt"]),
213
213
  }),
214
214
  execute: ({ files, kind }) => attempt(async () => {
215
- await ctx.emit("tool", `Liest ${files.length} ${kind === "invoice" ? "Rechnungen" : "Belege"}`);
215
+ await ctx.emit("tool", { code: "readsMany", params: { count: files.length } });
216
216
  const results = [];
217
217
  for (let i = 0; i < files.length; i += 4) {
218
218
  const batch = await Promise.all(files.slice(i, i + 4).map(async (file) => {
@@ -80,19 +80,22 @@ async function widgetData(step, ctx) {
80
80
  }
81
81
  /** A film: the widget's timeline rendered to an MP4 with sound — that video is the result. */
82
82
  async function runFilmStep(step, ctx, html, data, media, origin) {
83
- await ctx.emit("info", "Der Film wird geschnitten – das dauert etwa eine Minute.");
83
+ await ctx.emit("info", { code: "filmCutting" });
84
84
  let told = 0;
85
85
  const film = await renderFilm(html, widgetSize(step), media, (done) => {
86
86
  if (done - told >= 0.25) {
87
87
  told = done;
88
- void ctx.emit("info", `${Math.round(done * 100)} % geschnitten`);
88
+ void ctx.emit("info", { code: "filmProgress", params: { pct: Math.round(done * 100) } });
89
89
  }
90
90
  });
91
91
  if (!film) {
92
92
  throw new StepError("Der Film konnte nicht gerendert werden: dazu braucht es ffmpeg und eine Zeitleiste im Widget.");
93
93
  }
94
94
  if (film.errors.length) {
95
- await ctx.emit("info", `Der Film meldet: ${film.errors[0].slice(0, 200)}`);
95
+ await ctx.emit("info", {
96
+ code: "filmError",
97
+ params: { detail: film.errors[0].slice(0, 200) },
98
+ });
96
99
  }
97
100
  const assets = [
98
101
  await ctx.saveAsset({
@@ -142,7 +145,10 @@ export async function runWidgetStep(step, ctx) {
142
145
  console.error(`[widget ${step.id}]`, err);
143
146
  }
144
147
  if (probe?.errors.length) {
145
- await ctx.emit("info", `Das Widget meldet einen Fehler: ${probe.errors[0].slice(0, 200)}`);
148
+ await ctx.emit("info", {
149
+ code: "widgetError",
150
+ params: { detail: probe.errors[0].slice(0, 200) },
151
+ });
146
152
  }
147
153
  const assets = [
148
154
  await ctx.saveAsset({