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.
- package/apps/runtime/dist/credits/estimate.js +14 -8
- package/apps/runtime/dist/db/migrations/tenant/0008_run_event_note.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0008_snapshot.json +2019 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +2 -0
- package/apps/runtime/dist/engine/events.js +14 -2
- package/apps/runtime/dist/engine/prepare.js +5 -2
- package/apps/runtime/dist/engine/requirements.js +108 -0
- package/apps/runtime/dist/engine/runner.js +2 -2
- package/apps/runtime/dist/engine/steps.js +18 -10
- package/apps/runtime/dist/engine/template.js +13 -10
- package/apps/runtime/dist/routes/studio.js +2 -2
- package/apps/runtime/dist/services/marketplace.js +2 -2
- package/apps/runtime/dist/tools/browser.js +12 -6
- package/apps/runtime/dist/tools/index.js +12 -6
- package/apps/runtime/dist/tools/mail.js +5 -2
- package/apps/runtime/dist/tools/pages.js +1 -1
- package/apps/runtime/dist/tools/project.js +5 -2
- package/apps/runtime/dist/tools/store.js +4 -4
- package/apps/runtime/dist/widgets/step.js +10 -4
- package/apps/web/dist/assets/{Account-BE_W101C.js → Account-LVXhagaS.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-DjKQJAos.js → ChangelogDialog-DM0KAoy6.js} +1 -1
- package/apps/web/dist/assets/ChatPanel-CxOQmtv-.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DkW3YT44.js +1 -0
- package/apps/web/dist/assets/EditorPage-hbisE1Wf.js +2 -0
- package/apps/web/dist/assets/{FlowPreview-DK4RZqTW.js → FlowPreview-v7or29ku.js} +1 -1
- package/apps/web/dist/assets/{Harness-DcI94f1U.js → Harness-RZF-bBmb.js} +2 -2
- package/apps/web/dist/assets/{HomePage-BBNFgryu.js → HomePage-mxG-a_KU.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-BoYqovdl.js → ImportWizard-CiPyUt3E.js} +1 -1
- package/apps/web/dist/assets/LocalRuntime-Dqp9wkN3.js +1 -0
- package/apps/web/dist/assets/{NewWizardPage-tKV2S4eK.js → NewWizardPage-DHwRsi0_.js} +2 -2
- package/apps/web/dist/assets/ProjectPage-Bb7jN_6n.js +4 -0
- package/apps/web/dist/assets/PublicRunner-TGkOwbAP.js +1 -0
- package/apps/web/dist/assets/RunnerView-DzHGshZj.js +70 -0
- package/apps/web/dist/assets/SettingsPage-CG3Xtfej.js +1 -0
- package/apps/web/dist/assets/{SetupPage-qxHvlf87.js → SetupPage-O1GfPlqX.js} +1 -1
- package/apps/web/dist/assets/SharePage-CCNUhtry.js +1 -0
- package/apps/web/dist/assets/{api-t9_w1XNe.js → api-DB22_B7G.js} +2 -2
- package/apps/web/dist/assets/changelog-BkIJdZ7M.js +1 -0
- package/apps/web/dist/assets/{esm-D0YVMTdo.js → esm-3TT9i_rv.js} +1 -1
- package/apps/web/dist/assets/index-CLe9Wrpu.js +10 -0
- package/apps/web/dist/assets/{meta-BJHYbMh7.js → meta-CEsBCPfA.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-BqqUAvIY.js → settings-menu-pOcjznGJ.js} +1 -1
- package/apps/web/dist/assets/ui-I7QYMX0F.js +1 -0
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/mcp/flow.html +53 -53
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +10 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +25 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +141 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +72 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/ChatPanel-BWrcDs4s.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-DUjR5xkl.js +0 -1
- package/apps/web/dist/assets/EditorPage-CrOqikP2.js +0 -2
- package/apps/web/dist/assets/LocalRuntime-BD8zGN8w.js +0 -1
- package/apps/web/dist/assets/ProjectPage-voR0hvzy.js +0 -4
- package/apps/web/dist/assets/PublicRunner-CYZE_hSi.js +0 -1
- package/apps/web/dist/assets/RunnerView-D_C7hn81.js +0 -70
- package/apps/web/dist/assets/SettingsPage-DguoPF_3.js +0 -1
- package/apps/web/dist/assets/SharePage-Cf3i7KB-.js +0 -1
- package/apps/web/dist/assets/changelog-kz0DmHY4.js +0 -1
- package/apps/web/dist/assets/index-D5w3fW1p.js +0 -10
- package/apps/web/dist/assets/ui-BHXO_e0e.js +0 -1
|
@@ -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,
|
|
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({
|
|
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",
|
|
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",
|
|
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
|
-
|
|
17
|
-
|
|
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",
|
|
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
|
-
?
|
|
478
|
-
:
|
|
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",
|
|
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",
|
|
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(
|
|
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"
|
|
22
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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({
|