wizards 0.5.13 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/runtime/dist/authoring/guide.js +63 -7
- package/apps/runtime/dist/credits/estimate.js +1 -1
- package/apps/runtime/dist/engine/requirements.js +1 -1
- package/apps/runtime/dist/engine/runner.js +53 -4
- package/apps/runtime/dist/engine/steps.js +155 -123
- package/apps/runtime/dist/render/downloads.js +16 -3
- package/apps/runtime/dist/render/shaped.js +137 -0
- package/apps/runtime/dist/routes/runs.js +58 -2
- package/apps/runtime/dist/services/brand.js +2 -1
- package/apps/runtime/dist/widgets/runtime.js +25 -2
- package/apps/runtime/dist/widgets/step.js +2 -0
- package/apps/web/dist/assets/AboutDialog-xOyf_juP.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-BCS3WIRA.js → ChangelogDialog-vNG0Smoo.js} +1 -1
- package/apps/web/dist/assets/ChatView-BrDkurRe.js +10 -0
- package/apps/web/dist/assets/EditorPage-97U-JYKn.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-D2haAZwo.js → FlowPreview-DA0sHtr7.js} +1 -1
- package/apps/web/dist/assets/{Harness-CGI6ZMTA.js → Harness-Y9E7o83r.js} +2 -2
- package/apps/web/dist/assets/{NewWizardChannels-C4b_phHw.js → NewWizardChannels-D6v1iDOo.js} +1 -1
- package/apps/web/dist/assets/NewWizardPage-BzaAPMUK.js +7 -0
- package/apps/web/dist/assets/{OnboardingPage-DgySdBgo.js → OnboardingPage-9xZAkhuO.js} +1 -1
- package/apps/web/dist/assets/ProjectPage-dQ7wwxD2.js +1 -0
- package/apps/web/dist/assets/PublicRunner-CKa8M0Qd.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-B-4l3bVt.js → ReadOnly-Bqk14yPD.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-DsfZUojZ.js +4 -0
- package/apps/web/dist/assets/{RunnerView-BZ83HUnO.js → RunnerView-D0JQ1fgM.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-Dzu6Fl6b.js +1 -0
- package/apps/web/dist/assets/SetupPage-D2Kl-8NR.js +1 -0
- package/apps/web/dist/assets/SharePage-QSZxUlPt.js +1 -0
- package/apps/web/dist/assets/changelog-XlAAw5Gv.js +1 -0
- package/apps/web/dist/assets/{esm-BqbWrbsa.js → esm-DqRvG-PU.js} +1 -1
- package/apps/web/dist/assets/index-BhVJwBz7.css +1 -0
- package/apps/web/dist/assets/index-mwmrgVFT.js +14 -0
- package/apps/web/dist/assets/{lock-BkN0Fai6.js → lock-D_KjsGfU.js} +1 -1
- package/apps/web/dist/assets/{phone-Dip_wSUZ.js → phone-YRxkPQk6.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-B7eXkvba.js → settings-menu-CQgehwEX.js} +1 -1
- package/apps/web/dist/assets/ui-DDUCc_4v.js +137 -0
- package/apps/web/dist/assets/{upload-DPQ4pwnl.js → upload-DDo14e2p.js} +1 -1
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/mcp/flow.html +58 -55
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +5 -2
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +187 -3
- package/node_modules/@engenty-wizards/shared/dist/definition.js +89 -18
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +54 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +22 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/runner-texts.d.ts +41 -1
- package/node_modules/@engenty-wizards/shared/dist/runner-texts.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/runners.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/shape.d.ts +102 -0
- package/node_modules/@engenty-wizards/shared/dist/shape.js +318 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +33 -7
- package/node_modules/@engenty-wizards/shared/dist/surface.js +263 -20
- package/package.json +1 -1
- package/apps/web/dist/assets/AboutDialog-CkE7uZNL.js +0 -1
- package/apps/web/dist/assets/ChatView-DClGzMte.js +0 -10
- package/apps/web/dist/assets/EditorPage-Dg27sIZ7.js +0 -3
- package/apps/web/dist/assets/NewWizardPage-DhDXrjKX.js +0 -7
- package/apps/web/dist/assets/ProjectPage-dvtJn_3c.js +0 -1
- package/apps/web/dist/assets/PublicRunner-lP4sVF1q.js +0 -1
- package/apps/web/dist/assets/RunDrawer-B2bsB9Z6.js +0 -4
- package/apps/web/dist/assets/SettingsPage-C0_wAa_j.js +0 -1
- package/apps/web/dist/assets/SetupPage-kuvBk0G0.js +0 -1
- package/apps/web/dist/assets/SharePage-COLVxXMI.js +0 -1
- package/apps/web/dist/assets/changelog-CIYdRaa5.js +0 -1
- package/apps/web/dist/assets/index-Da9Pvzpj.js +0 -14
- package/apps/web/dist/assets/index-DaKjB676.css +0 -1
- package/apps/web/dist/assets/ui-B9AOCT0v.js +0 -134
|
@@ -136,8 +136,9 @@ type AgentStep = {
|
|
|
136
136
|
tools: ("web_search"|"web_fetch"|"browser"|"sandbox"|"image"|"http"|"pages")[]
|
|
137
137
|
mcp?: string[] // ids of the project's MCP servers this step may use
|
|
138
138
|
connections?: string[] // ids of wizard connections this step may read (mail → mail_search, mail_read, mail_save)
|
|
139
|
-
output: { format: "text"|"markdown"|"json", fields?: { id, kind: "text"|"number"|"list"|"table"|"yesno"|"choice"|"score", description, columns?: string[], options?: string[] }[], surface?:
|
|
139
|
+
output: { format: "text"|"markdown"|"json", fields?: { id, kind: "text"|"number"|"list"|"table"|"yesno"|"choice"|"score", description, columns?: string[], options?: string[] }[], schema?: Schema, surface?: SurfaceComponent[] | true }
|
|
140
140
|
// "score" answers its description on the scale in options, lowest first ("low", "medium", "high")
|
|
141
|
+
// fields: one level of keys (the short form). schema: nested data, below. One of the two, not both.
|
|
141
142
|
each?: "steps.<id>.<key>" | "lists.<id>"
|
|
142
143
|
// ONE RUN PER ENTRY (at most 20): the rows of a table output, the entries of a list output, or the
|
|
143
144
|
// rows of a stored list; the instructions read {{item.<column>}}, {{index}}, {{count}}. The output is
|
|
@@ -172,9 +173,45 @@ type SurfaceStep = {
|
|
|
172
173
|
candidates?: { id, description, components: SurfaceComponent[] (the first has the piece's id), group?, required? }[]
|
|
173
174
|
groups?: { id, instructions, optional? }[]
|
|
174
175
|
}
|
|
176
|
+
// SCHEMA (json only): nested data — lists of entries that hold lists of their own (days with meals, recipes with
|
|
177
|
+
// ingredients and steps). Its root is { kind: "object", label, fields: { <key>: Field } }; a Field is
|
|
178
|
+
// { kind: "text", label, description?, long?: true } long: paragraphs, not a line
|
|
179
|
+
// { kind: "number", label, description?, unit?: "Min" }
|
|
180
|
+
// { kind: "yesno", label, description?, yes?: "da", no?: "einkaufen" } (yes/no: the words people read)
|
|
181
|
+
// { kind: "choice", label, description?, options: string[] }
|
|
182
|
+
// { kind: "ref", label, description?, to: "<top-level list key>" } the id of an entry of that list; its entries
|
|
183
|
+
// need { id: { kind: "text" } }
|
|
184
|
+
// { kind: "list", label, description?, of: Field-without-list | { kind: "object", label, fields: { … } } }
|
|
185
|
+
// Labels are in the wizard's language: column heads, part titles, "add <label>" in the admin's editor. Description:
|
|
186
|
+
// what the model is to put there. Objects nest at most 4 deep (the root counted), at most 100 keys in all.
|
|
187
|
+
// The model gets it as a strict JSON Schema: every key is always filled.
|
|
188
|
+
// DESIGN THE VIEW FIRST, then the schema it needs: write output.surface (Tabs, Card, Fold, Group, Chips, Checklist,
|
|
189
|
+
// Steps, Link, Callout …) for what the person should see, then give the schema exactly the lists and keys that
|
|
190
|
+
// view binds — nothing it does not show or a later step does not read. One step returns the whole tree.
|
|
191
|
+
// Paths: {"path": "/recipes"} from the data; inside a repeated entry "title" or "meals" reads the entry; a Link's
|
|
192
|
+
// view reads the entry it opens. The view is checked against the schema.
|
|
193
|
+
// Later steps read top-level keys ({{steps.id.key}}, optionsFrom, each, conditions); a list of entries is a table
|
|
194
|
+
// there ("steps.id.recipes.title"). Downloads: xlsx one sheet per top-level list, csv the first list of flat
|
|
195
|
+
// entries, md and pdf the whole tree.
|
|
196
|
+
// Example (Kühlschrank-Koch): { kind: "object", label: "Menüplan", fields: {
|
|
197
|
+
// summary: { kind: "text", label: "Zusammenfassung", long: true },
|
|
198
|
+
// days: { kind: "list", label: "Tage", of: { kind: "object", label: "Tag", fields: {
|
|
199
|
+
// label: { kind: "text", label: "Tag" }, theme: { kind: "text", label: "Motto" },
|
|
200
|
+
// meals: { kind: "list", label: "Mahlzeiten", of: { kind: "object", label: "Mahlzeit", fields: {
|
|
201
|
+
// slot: { kind: "choice", label: "Mahlzeit", options: ["Frühstück", "Mittag", "Abend"] },
|
|
202
|
+
// recipe_id: { kind: "ref", label: "Rezept", to: "recipes" } } } } } } },
|
|
203
|
+
// recipes: { kind: "list", label: "Rezepte", of: { kind: "object", label: "Rezept", fields: {
|
|
204
|
+
// id: { kind: "text", label: "Kennung" }, title: { kind: "text", label: "Titel" },
|
|
205
|
+
// time_minutes: { kind: "number", label: "Zeit", unit: "Min" },
|
|
206
|
+
// steps: { kind: "list", label: "Zubereitung", of: { kind: "object", label: "Schritt", fields: {
|
|
207
|
+
// text: { kind: "text", label: "Schritt", long: true }, minutes: { kind: "number", label: "Dauer", unit: "Min" } } } } } } } } }
|
|
208
|
+
// with a view: Tabs over a Grid of day Cards (each meal a Link to its recipe) and a Grid of recipe Links whose
|
|
209
|
+
// view is a Card, a Fold of ingredients and the Steps.
|
|
175
210
|
// A SURFACE is a native view in the runner — facts, a list of results, key figures, a chart — themed, on phones, free per run.
|
|
176
211
|
// Prefer it to a widget for such views; a widget is for what the catalog cannot show (a map, an animation).
|
|
177
|
-
// An agent step with output.format "json" can also
|
|
212
|
+
// An agent step with output.format "json" can also carry such a view of its own result: output.surface: [components],
|
|
213
|
+
// written once and bound to its fields or schema by path ({"path": "/<key>"}; a step with "each": "/rows"). Prefer it to
|
|
214
|
+
// output.surface: true, which has a model compose a new view on every run.
|
|
178
215
|
// Reviews and the result show a step's view instead of its plain table.
|
|
179
216
|
type GenerateStep = {
|
|
180
217
|
type: "generate"
|
|
@@ -200,7 +237,10 @@ type WidgetStep = {
|
|
|
200
237
|
entry: string // workspace path of the widget's HTML, e.g. "weather/index.html"
|
|
201
238
|
data: { [key]: string } // what the widget gets as wizard.data: key → fieldId | steps.stepId | steps.stepId.key | brand.name | facts.key | today
|
|
202
239
|
sample?: string // workspace path of example data (same shape as data) for previews
|
|
203
|
-
|
|
240
|
+
layout?: "fixed"|"page" // fixed (default): drawn at size and scaled to the page — maps, charts, films.
|
|
241
|
+
// page: part of the run page, as wide as it and as tall as its content, never scaled —
|
|
242
|
+
// a result people scroll and use (a cook mode, a configurator, a planner)
|
|
243
|
+
size?: { width, height } // design size in px, default 1280×720 (9:16 → 1080×1920); exports render at it
|
|
204
244
|
video?: boolean // a FILM: the timeline is rendered to an MP4 WITH SOUND when the step runs; the person sees and
|
|
205
245
|
// downloads that video. data may then name generate steps: "steps.<id>" of an image / video / voice
|
|
206
246
|
// step arrives as a URL (a list of URLs for a step with "each").
|
|
@@ -238,6 +278,7 @@ type ResultStep = { type: "result", message?: string, deliverables: { from: step
|
|
|
238
278
|
// film widget (video: true)→mp4 png · film→mp4
|
|
239
279
|
// widget→html png pdf mp4 json (mp4 only when the widget registers a timeline)
|
|
240
280
|
// agent text/markdown→md txt docx pdf html · agent json→json csv xlsx md (xlsx: one sheet per table)
|
|
281
|
+
// agent json with schema→json csv xlsx md pdf
|
|
241
282
|
// agent, any format→zip: the FILES the step kept with mail_save / browser_download
|
|
242
283
|
// lists.<id>→xlsx csv json md · zip (a list with check.file: the kept files its rows name, plus the list)
|
|
243
284
|
|
|
@@ -252,9 +293,18 @@ A template may only use fields asked and steps run EARLIER.`;
|
|
|
252
293
|
export const PRINCIPLES = `
|
|
253
294
|
How good wizards look:
|
|
254
295
|
- For non-technical end users. Plain words, friendly titles phrased as questions ("What is the post for?").
|
|
296
|
+
- The wizard speaks for itself: the first page invites in its own voice ("Show me your fridge."), every field's help says in
|
|
297
|
+
one line why it is asked ("For the amounts in the recipes"), every step that takes a while has a "working" line that
|
|
298
|
+
names what happens ("Looking into your fridge…"). Choices with up to five options are buttons, not a dropdown.
|
|
299
|
+
- The result is what people come for: design how it looks first, then the data it needs. A json step's result gets a view
|
|
300
|
+
(output.surface, written once) or a widget; give its data the fields only the view uses — a yes/no that picks a colour
|
|
301
|
+
("in_fridge"), a key that groups ("category"), one human line (a summary, a tip, a theme per day) — and links instead of
|
|
302
|
+
copies (a meal names its recipe's id). Things are cards, ordered work is Steps, things to get or do are a Checklist;
|
|
303
|
+
never a table of things for a person — a table only for figures compared across columns (prices, dates).
|
|
255
304
|
- A page whose fields depend on an earlier answer uses "when" on its fields, never one copy of the page per answer and a branch to each copy. Choices that depend on what an earlier step found use "optionsFrom", never one page per possible result.
|
|
256
305
|
- Few pages, 1–5 fields each, one theme per page. Only ask what the result really needs; prefer selects with sensible defaults.
|
|
257
|
-
- Put a review step before anything expensive (video) and before
|
|
306
|
+
- Put a review step before anything expensive (video) and before anything written elsewhere (mail, CRM), so people can
|
|
307
|
+
correct or regenerate. A result people can simply make again needs no review before it.
|
|
258
308
|
- Money/totals: use an "items" field with vat — the runner computes totals; documents must use {{items}} verbatim. Never let a model compute totals.
|
|
259
309
|
- Facts from the web need an agent step with web_search + web_fetch BEFORE writing; documents then cite those notes.
|
|
260
310
|
- Live data with a free public JSON API (weather and wind: api.open-meteo.com; sunrise/sunset is in it too) → an agent step with the http tool and model "standard" that calls the API and returns json. Far cheaper and more exact than web research; use web research only when no API exists.
|
|
@@ -295,8 +345,12 @@ one-off pages stay "generate" steps. A widget is code in the wizard's WORKSPACE
|
|
|
295
345
|
<script src="lib/name.min.js">. Fetch reference data ONCE now (e.g. lake outlines
|
|
296
346
|
from nominatim.openstreetmap.org/search?q=…&format=json&polygon_geojson=1&polygon_threshold=0.0005)
|
|
297
347
|
and save it as a workspace file.
|
|
298
|
-
- Runtime, window.wizard: data · brand {name, accent, logo, colors [{name, value}], facts {key: value}} · ai "generated"|"edited"|null · mode "view"|"export" · file(path) ·
|
|
299
|
-
json(path) · url(path) (data URL for images/fonts) · ready() · timeline({ duration, seek, poster })
|
|
348
|
+
- Runtime, window.wizard: data · brand {name, accent, logo, colors [{name, value}], facts {key: value}, theme} · ai "generated"|"edited"|null · mode "view"|"export" · file(path) ·
|
|
349
|
+
json(path) · url(path) (data URL for images/fonts) · ready() · timeline({ duration, seek, poster }) ·
|
|
350
|
+
state (what the person did last time, kept with the run) · save(state) (keep ticks, an open tab, a step reached: small JSON) ·
|
|
351
|
+
share(text) (the device's share sheet).
|
|
352
|
+
- layout "page": let the document grow with its content (no height: 100vh, no fixed body height) and lay out for phones
|
|
353
|
+
first; the page sets the frame's height from it. Keep what the person does with wizard.save and start from wizard.state.
|
|
300
354
|
- Animation: call wizard.timeline({ duration: seconds, seek: t => draw(t) }) and drive your own
|
|
301
355
|
play/pause and <input type=range> scrubber from it. seek(t) must draw time t synchronously — the MP4
|
|
302
356
|
export steps through it frame by frame. In mode "export" hide all controls and do not autoplay.
|
|
@@ -308,7 +362,9 @@ one-off pages stay "generate" steps. A widget is code in the wizard's WORKSPACE
|
|
|
308
362
|
arrive as URLs, not inlined.
|
|
309
363
|
- Call wizard.ready() once the first frame is drawn. Lay out for the step's size and scale to fit
|
|
310
364
|
the window on every "resize" event — the window can be 0×0 when your script first runs.
|
|
311
|
-
- Plain HTML/CSS/JS, SVG or canvas.
|
|
365
|
+
- Plain HTML/CSS/JS, SVG or canvas. Take the look from wizard.brand.theme: accent for actions, second for meaning (done,
|
|
366
|
+
there, a tip), background for the ground, display for headings, body for text (system fonts behind them); dark: a dark
|
|
367
|
+
ground. Cards for things, generous spacing, legible labels; no table of things.
|
|
312
368
|
- After writing, ALWAYS run check_widget, read its errors and look at the screenshot; fix until clean.`;
|
|
313
369
|
export function connectorsLine(connectors) {
|
|
314
370
|
const lines = connectors.map((c) => {
|
|
@@ -61,7 +61,7 @@ function stepFormula(step, catalog) {
|
|
|
61
61
|
if (step.output.format === "json") {
|
|
62
62
|
credits += textCredits(cls("classifier"), TOKENS.structure);
|
|
63
63
|
}
|
|
64
|
-
if (step.output.surface) {
|
|
64
|
+
if (step.output.surface === true) {
|
|
65
65
|
credits += textCredits(cls("standard"), TOKENS.structure);
|
|
66
66
|
}
|
|
67
67
|
return credits;
|
|
@@ -36,7 +36,7 @@ function ownClasses(step) {
|
|
|
36
36
|
if (step.output.format === "json") {
|
|
37
37
|
classes.push("classifier");
|
|
38
38
|
}
|
|
39
|
-
if (step.output.surface && !classes.includes("standard")) {
|
|
39
|
+
if (step.output.surface === true && !classes.includes("standard")) {
|
|
40
40
|
classes.push("standard");
|
|
41
41
|
}
|
|
42
42
|
return classes;
|
|
@@ -2,6 +2,7 @@ import { marginOf, } from "@engenty-wizards/shared/decision";
|
|
|
2
2
|
import { branchValues, cardsFromData, conditionsOf, dataRef, followRules, formatsFor, isDecidedField, LIST_FORMATS, listRef, loopKey, MAX_SHOWN_FIELDS, optionsFromData, otherwiseOf, pageNeeds, whenHolds, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
3
|
import { note, saidText, } from "@engenty-wizards/shared/run";
|
|
4
4
|
import { CONVERSATION_RUNNERS, DEFAULT_RUNNER_SETTINGS } from "@engenty-wizards/shared/runners";
|
|
5
|
+
import { fitData } from "@engenty-wizards/shared/shape";
|
|
5
6
|
import { machineTranslated, translateDefinition } from "@engenty-wizards/shared/translation";
|
|
6
7
|
import { eq } from "drizzle-orm";
|
|
7
8
|
import { nanoid } from "nanoid";
|
|
@@ -30,7 +31,7 @@ import { readPageInput } from "./input.js";
|
|
|
30
31
|
import { pushRun } from "./push.js";
|
|
31
32
|
import { blockingMessage, closedChoices, missingModelsSaid, openValue, stepsWithoutModel, } from "./requirements.js";
|
|
32
33
|
import { releaseResources, resourcesFor } from "./resources.js";
|
|
33
|
-
import { runAutomaticStep } from "./steps.js";
|
|
34
|
+
import { outputSchema, runAutomaticStep } from "./steps.js";
|
|
34
35
|
import { answersAsText, resolveRef } from "./template.js";
|
|
35
36
|
import { StepError } from "./types.js";
|
|
36
37
|
/** A command the run cannot take in its current state; a note where the person may read it. */
|
|
@@ -695,6 +696,47 @@ export async function submitPage(runId, stepId, input) {
|
|
|
695
696
|
await updateRun(runId, { state, cursor, status: "running", error: null });
|
|
696
697
|
kick(runId);
|
|
697
698
|
}
|
|
699
|
+
/** Edited data of a json step is kept up to this size. */
|
|
700
|
+
const MAX_EDITED_JSON = 500_000;
|
|
701
|
+
/**
|
|
702
|
+
* A json step's output with data someone edited: checked against the step's schema, and what
|
|
703
|
+
* its view shows too. Refused when it does not fit.
|
|
704
|
+
*/
|
|
705
|
+
export function editedJson(def, stepId, output, data) {
|
|
706
|
+
const step = stepOf(def, stepId);
|
|
707
|
+
if (step?.type !== "agent" || step.output.format !== "json" || step.each || step.call) {
|
|
708
|
+
throw new RunConflict("Not a json step");
|
|
709
|
+
}
|
|
710
|
+
if (JSON.stringify(data).length > MAX_EDITED_JSON) {
|
|
711
|
+
throw new RunConflict("Too large");
|
|
712
|
+
}
|
|
713
|
+
const { schema, shape } = outputSchema(step);
|
|
714
|
+
const parsed = schema.safeParse(fitData(shape, data));
|
|
715
|
+
if (!parsed.success) {
|
|
716
|
+
const issue = parsed.error.issues[0];
|
|
717
|
+
throw new RunConflict(`Does not fit the schema: ${issue?.path.join(".")} ${issue?.message}`);
|
|
718
|
+
}
|
|
719
|
+
const json = parsed.data;
|
|
720
|
+
return {
|
|
721
|
+
...output,
|
|
722
|
+
json,
|
|
723
|
+
...(output.surface ? { surface: { ...output.surface, data: json } } : {}),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/** Data of a finished or waiting run's json step, edited in the studio. */
|
|
727
|
+
export async function editOutput(runId, stepId, data) {
|
|
728
|
+
const run = await loadRun(runId);
|
|
729
|
+
const output = run?.state.outputs[stepId];
|
|
730
|
+
if (!run || !output) {
|
|
731
|
+
throw new RunConflict("Unknown step");
|
|
732
|
+
}
|
|
733
|
+
if (run.status !== "waiting_input" && run.status !== "done") {
|
|
734
|
+
throw new RunConflict({ code: "runBusy" });
|
|
735
|
+
}
|
|
736
|
+
const state = structuredClone(run.state);
|
|
737
|
+
state.outputs[stepId] = editedJson(run.definition, stepId, output, data);
|
|
738
|
+
await updateRun(runId, { state });
|
|
739
|
+
}
|
|
698
740
|
export async function reviewStep(runId, stepId, action) {
|
|
699
741
|
const run = await requireWaiting(runId, stepId);
|
|
700
742
|
const step = stepOf(run.definition, stepId);
|
|
@@ -703,9 +745,16 @@ export async function reviewStep(runId, stepId, action) {
|
|
|
703
745
|
}
|
|
704
746
|
const state = structuredClone(run.state);
|
|
705
747
|
if (action.type === "accept") {
|
|
706
|
-
for (const [target,
|
|
707
|
-
|
|
708
|
-
|
|
748
|
+
for (const [target, edit] of Object.entries(action.edits ?? {})) {
|
|
749
|
+
const output = state.outputs[target];
|
|
750
|
+
if (!step.edit || !step.show.includes(target) || !output) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (typeof edit === "string") {
|
|
754
|
+
state.outputs[target] = { ...output, text: edit.slice(0, 50_000) };
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
state.outputs[target] = editedJson(run.definition, target, output, edit);
|
|
709
758
|
}
|
|
710
759
|
}
|
|
711
760
|
state.history.push(stepId);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { pOf } from "@engenty-wizards/shared/decision";
|
|
2
|
-
import { allFields, dataRef, isDecisionStep, LOCALES, langNameEn, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
|
-
import { mainLogo, ruleLines } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { allFields, dataRef, isDecisionStep, LOCALES, langNameEn, stepShape, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
|
+
import { mainLogo, ruleLines, runTheme } from "@engenty-wizards/shared/projects";
|
|
4
4
|
import { note as runNote } from "@engenty-wizards/shared/run";
|
|
5
|
+
import { fitData, labelOf, recordsOf, shapeJsonSchema, shapeLines, shapeZod, titleKey, } from "@engenty-wizards/shared/shape";
|
|
5
6
|
import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
|
|
6
7
|
import { Agent } from "@mastra/core/agent";
|
|
7
|
-
import { generateText, Output } from "ai";
|
|
8
|
-
import { z } from "zod";
|
|
8
|
+
import { generateText, jsonSchema, Output, parsePartialJson } from "ai";
|
|
9
9
|
import { extFor, loadAsset, loadAssetText } from "../files/storage.js";
|
|
10
10
|
import { snapshotHtmlImages } from "../files/web-images.js";
|
|
11
11
|
import { runFilmStep } from "../film/step.js";
|
|
@@ -109,80 +109,21 @@ const BROWSER_RULES = [
|
|
|
109
109
|
"The person can take the browser over while you work; your next browser action then waits and gets the page as they left it.",
|
|
110
110
|
].join("\n");
|
|
111
111
|
// --- agent -------------------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* What the step returns: its schema, its fields as one, or else one table. The model's
|
|
114
|
+
* structured output takes it as JSON Schema; its own JSON is checked against it.
|
|
115
|
+
*/
|
|
112
116
|
export function outputSchema(step) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
: [{ id: "data", kind: "table", description: "The result as a table" }];
|
|
116
|
-
const shape = {};
|
|
117
|
-
for (const f of fields) {
|
|
118
|
-
const d = f.description ?? f.id;
|
|
119
|
-
switch (f.kind) {
|
|
120
|
-
case "text":
|
|
121
|
-
shape[f.id] = z.string().describe(d);
|
|
122
|
-
break;
|
|
123
|
-
case "number":
|
|
124
|
-
shape[f.id] = z.number().describe(d);
|
|
125
|
-
break;
|
|
126
|
-
case "list":
|
|
127
|
-
shape[f.id] = z.array(z.string()).describe(d);
|
|
128
|
-
break;
|
|
129
|
-
case "yesno":
|
|
130
|
-
shape[f.id] = z.boolean().describe(d);
|
|
131
|
-
break;
|
|
132
|
-
case "choice":
|
|
133
|
-
shape[f.id] = z.enum(f.options).describe(d);
|
|
134
|
-
break;
|
|
135
|
-
case "score":
|
|
136
|
-
shape[f.id] = z
|
|
137
|
-
.enum(f.options)
|
|
138
|
-
.describe(`${d} (lowest to highest)`);
|
|
139
|
-
break;
|
|
140
|
-
case "table":
|
|
141
|
-
shape[f.id] = f.columns?.length
|
|
142
|
-
? z
|
|
143
|
-
.array(z.object(Object.fromEntries(f.columns.map((c) => [c, z.union([z.string(), z.number(), z.null()])]))))
|
|
144
|
-
.describe(d)
|
|
145
|
-
: z
|
|
146
|
-
.object({
|
|
147
|
-
columns: z.array(z.string()),
|
|
148
|
-
rows: z.array(z.array(z.union([z.string(), z.number(), z.null()]))),
|
|
149
|
-
})
|
|
150
|
-
.describe(d);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { schema: z.object(shape), fields };
|
|
155
|
-
}
|
|
156
|
-
/** Tables come back as columns + rows (strict-schema friendly) and are stored as records. */
|
|
157
|
-
function tablesToRecords(value, fields) {
|
|
158
|
-
const out = { ...value };
|
|
159
|
-
for (const f of fields) {
|
|
160
|
-
const t = value[f.id];
|
|
161
|
-
if (f.kind === "table" && t?.columns && t.rows) {
|
|
162
|
-
out[f.id] = t.rows.map((row) => Object.fromEntries(t.columns.map((c, i) => [c, row[i] ?? null])));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return out;
|
|
166
|
-
}
|
|
167
|
-
/** A table cell as the step's schema takes it: a list joined by spaces, anything else as text. */
|
|
168
|
-
function fitCell(value) {
|
|
169
|
-
if (value === null || value === undefined) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
if (typeof value === "string" || typeof value === "number") {
|
|
173
|
-
return value;
|
|
174
|
-
}
|
|
175
|
-
if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) {
|
|
176
|
-
return value.join(" ");
|
|
177
|
-
}
|
|
178
|
-
return typeof value === "boolean" ? String(value) : JSON.stringify(value);
|
|
117
|
+
const shape = stepShape(step);
|
|
118
|
+
return { schema: shapeZod(shape), shape };
|
|
179
119
|
}
|
|
180
120
|
/**
|
|
181
121
|
* What the agent wrote as JSON itself — its last fenced block, else its answer or the object in
|
|
182
|
-
* it — fitted to the step's
|
|
183
|
-
*
|
|
122
|
+
* it — fitted to the step's shape (a number where text belongs as text; in a short-form table a
|
|
123
|
+
* list in a cell joined, a missing column empty) and checked against the schema. Null where none
|
|
124
|
+
* fits; then a model structures the text.
|
|
184
125
|
*/
|
|
185
|
-
export function ownJson(text, schema,
|
|
126
|
+
export function ownJson(text, schema, shape) {
|
|
186
127
|
const blocks = [...text.matchAll(/```(?:json)?[ \t]*\n([\s\S]*?)```/g)].map((m) => m[1]);
|
|
187
128
|
const open = text.indexOf("{");
|
|
188
129
|
const close = text.lastIndexOf("}");
|
|
@@ -202,30 +143,88 @@ export function ownJson(text, schema, fields) {
|
|
|
202
143
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
203
144
|
continue;
|
|
204
145
|
}
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
146
|
+
const parsed = schema.safeParse(fitData(shape, value));
|
|
147
|
+
if (parsed.success) {
|
|
148
|
+
return parsed.data;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/** At most this many entries a step reports while it writes; the rest come with the result. */
|
|
154
|
+
const MAX_FOUND = 40;
|
|
155
|
+
/**
|
|
156
|
+
* Runs a json step's agent as a stream and reports each entry of a top-level list once it is
|
|
157
|
+
* complete in the JSON the agent is writing: "Rezepte: Shakshuka mit Feta". Returns what
|
|
158
|
+
* `generate` would: the text, the steps and the usage.
|
|
159
|
+
*/
|
|
160
|
+
async function streamWithProgress(agent, message, options, step, ctx) {
|
|
161
|
+
const shape = outputSchema(step).shape;
|
|
162
|
+
const reported = new Map();
|
|
163
|
+
let told = 0;
|
|
164
|
+
let written = "";
|
|
165
|
+
let checked = 0;
|
|
166
|
+
const report = async () => {
|
|
167
|
+
const start = written.lastIndexOf("```json");
|
|
168
|
+
if (start < 0 || told >= MAX_FOUND) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const body = written.slice(start + 7).replace(/```\s*$/, "");
|
|
172
|
+
const { value } = await parsePartialJson(body);
|
|
173
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const keys = Object.keys(shape.fields);
|
|
177
|
+
for (const [index, [key, field]] of Object.entries(shape.fields).entries()) {
|
|
178
|
+
const list = value[key];
|
|
179
|
+
if (field.kind !== "list" || !Array.isArray(list)) {
|
|
180
|
+
continue;
|
|
212
181
|
}
|
|
213
|
-
|
|
214
|
-
|
|
182
|
+
// The last entry may still be half written – until a later part has begun.
|
|
183
|
+
const later = keys.slice(index + 1).some((k) => k in value);
|
|
184
|
+
const done = later ? list.length : list.length - 1;
|
|
185
|
+
for (let i = reported.get(key) ?? 0; i < done && told < MAX_FOUND; i++) {
|
|
186
|
+
const entry = list[i];
|
|
187
|
+
const name = field.of.kind === "object"
|
|
188
|
+
? String(entry?.[titleKey(field.of) ?? ""] ?? "")
|
|
189
|
+
: String(entry ?? "");
|
|
190
|
+
if (name.trim()) {
|
|
191
|
+
told += 1;
|
|
192
|
+
await ctx.emit("info", {
|
|
193
|
+
code: "found",
|
|
194
|
+
params: { label: labelOf(key, field), name: name.trim().slice(0, 80) },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
reported.set(key, i + 1);
|
|
215
198
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const stream = await agent.stream(message, options);
|
|
202
|
+
// A model that answers in one piece (a local client) has nothing to report while it writes:
|
|
203
|
+
// its entries come with the result.
|
|
204
|
+
let whole = false;
|
|
205
|
+
for await (const chunk of stream.fullStream) {
|
|
206
|
+
const payload = chunk.payload ?? chunk;
|
|
207
|
+
if (chunk.type === "text-delta") {
|
|
208
|
+
const delta = payload.text ?? payload.delta ?? "";
|
|
209
|
+
whole ||= delta.length > 2_000;
|
|
210
|
+
written += delta;
|
|
211
|
+
// Parsing the growing answer every few hundred characters is enough to keep up.
|
|
212
|
+
if (!whole && written.length - checked > 300) {
|
|
213
|
+
checked = written.length;
|
|
214
|
+
await report();
|
|
221
215
|
}
|
|
222
216
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return parsed.data;
|
|
217
|
+
else if (chunk.type === "error") {
|
|
218
|
+
throw payload.error ?? new Error("step stream error");
|
|
226
219
|
}
|
|
227
220
|
}
|
|
228
|
-
|
|
221
|
+
const s = stream;
|
|
222
|
+
return {
|
|
223
|
+
text: (await s.text) ?? written,
|
|
224
|
+
steps: await s.steps,
|
|
225
|
+
usage: await s.usage,
|
|
226
|
+
totalUsage: await s.totalUsage,
|
|
227
|
+
};
|
|
229
228
|
}
|
|
230
229
|
/** An agent step looks at this many photos itself; more are read one by one with read_document. */
|
|
231
230
|
const MAX_SEEN = 8;
|
|
@@ -331,10 +330,8 @@ function jsonArrayIn(text) {
|
|
|
331
330
|
* for its view.
|
|
332
331
|
*/
|
|
333
332
|
async function composeSurface(step, json, ctx) {
|
|
334
|
-
const
|
|
335
|
-
const fields = (
|
|
336
|
-
.map((f) => `- /${f.id} (${f.kind}${f.columns ? `: ${f.columns.join(", ")}` : ""}): ${f.description ?? ""}`)
|
|
337
|
-
.join("\n");
|
|
333
|
+
const shape = outputSchema(step).shape;
|
|
334
|
+
const fields = shapeLines(shape).join("\n");
|
|
338
335
|
const sample = JSON.stringify(json).slice(0, 4000);
|
|
339
336
|
try {
|
|
340
337
|
const writer = await textModel("standard", ctx.call);
|
|
@@ -356,7 +353,7 @@ async function composeSurface(step, json, ctx) {
|
|
|
356
353
|
});
|
|
357
354
|
await ctx.chargeUsd(costOf(writer, result.usage));
|
|
358
355
|
const components = jsonArrayIn(result.text);
|
|
359
|
-
issues = validateSurface(components,
|
|
356
|
+
issues = validateSurface(components, shape).map((i) => i.message);
|
|
360
357
|
if (!issues.length) {
|
|
361
358
|
return { components: components, data: json };
|
|
362
359
|
}
|
|
@@ -413,21 +410,24 @@ async function runAgentEach(step, ctx) {
|
|
|
413
410
|
assets.push(...(out.assets ?? []));
|
|
414
411
|
}
|
|
415
412
|
const json = { rows };
|
|
416
|
-
const surface = step.output.surface
|
|
417
|
-
?
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
...step
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
413
|
+
const surface = Array.isArray(step.output.surface)
|
|
414
|
+
? { components: step.output.surface, data: json }
|
|
415
|
+
: step.output.surface
|
|
416
|
+
? await composeSurface({
|
|
417
|
+
...step,
|
|
418
|
+
output: {
|
|
419
|
+
...step.output,
|
|
420
|
+
schema: undefined,
|
|
421
|
+
fields: [
|
|
422
|
+
{
|
|
423
|
+
id: "rows",
|
|
424
|
+
kind: "table",
|
|
425
|
+
description: "One row per entry: its columns and the step's fields",
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
}, json, ctx)
|
|
430
|
+
: undefined;
|
|
431
431
|
return {
|
|
432
432
|
text: texts.join("\n\n"),
|
|
433
433
|
json,
|
|
@@ -493,7 +493,11 @@ export async function runAgentStep(step, ctx) {
|
|
|
493
493
|
? "Answer with the finished result in clean Markdown — no preamble, no closing remarks."
|
|
494
494
|
: step.output.format === "text"
|
|
495
495
|
? "Answer with the finished result as plain text — no Markdown, no preamble."
|
|
496
|
-
:
|
|
496
|
+
: [
|
|
497
|
+
"Finish with everything the result needs in your final answer, ending with one ```json block in exactly this shape (keys as named, lists as lists):",
|
|
498
|
+
shapeLines(outputSchema(step).shape).join("\n"),
|
|
499
|
+
'Write the parts in this order, each entry complete before the next. Its values are read on a phone: short names without hedges (doubt goes into its own field, "ca. 1 Becher"), only the values a field allows, no filler entries ("if needed").',
|
|
500
|
+
].join("\n");
|
|
497
501
|
const system = [
|
|
498
502
|
groundRules(ctx),
|
|
499
503
|
step.tools.includes("browser") ? BROWSER_RULES : "",
|
|
@@ -556,7 +560,10 @@ export async function runAgentStep(step, ctx) {
|
|
|
556
560
|
},
|
|
557
561
|
]
|
|
558
562
|
: prompt;
|
|
559
|
-
|
|
563
|
+
// A json step streams, so the person sees its entries as they are written (`found`).
|
|
564
|
+
const result = step.output.format === "json"
|
|
565
|
+
? await streamWithProgress(agent, message, { maxSteps, abortSignal: ctx.signal }, step, ctx)
|
|
566
|
+
: await agent.generate(message, { maxSteps, abortSignal: ctx.signal });
|
|
560
567
|
await ctx.chargeUsd(costOf(resolved, result.totalUsage ?? result.usage));
|
|
561
568
|
// `result.text` strings together what the model said between tool calls ("I open the page …").
|
|
562
569
|
// A written result is its last message; structuring keeps everything, numbers may sit anywhere.
|
|
@@ -569,23 +576,38 @@ export async function runAgentStep(step, ctx) {
|
|
|
569
576
|
}
|
|
570
577
|
return { text, assets: assets.length ? assets : undefined, at: new Date().toISOString() };
|
|
571
578
|
}
|
|
572
|
-
const { schema,
|
|
579
|
+
const { schema, shape } = outputSchema(step);
|
|
573
580
|
// The agent mostly writes the JSON itself; a second model only structures what does not fit.
|
|
574
581
|
// Having it copy kilobytes of JSON again is slow and can fail on its own.
|
|
575
|
-
let output = ownJson(text, schema,
|
|
582
|
+
let output = ownJson(text, schema, shape);
|
|
576
583
|
if (!output) {
|
|
577
584
|
const structurer = await textModel("classifier", ctx.call);
|
|
578
585
|
const structured = await generateText({
|
|
579
586
|
model: structurer.model,
|
|
580
587
|
abortSignal: ctx.signal,
|
|
581
|
-
|
|
588
|
+
// The JSON Schema built from the shape, strict: every key required, no other keys.
|
|
589
|
+
output: Output.object({
|
|
590
|
+
schema: jsonSchema(shapeJsonSchema(shape), {
|
|
591
|
+
validate: (value) => {
|
|
592
|
+
const parsed = schema.safeParse(value);
|
|
593
|
+
return parsed.success
|
|
594
|
+
? { success: true, value: parsed.data }
|
|
595
|
+
: { success: false, error: parsed.error };
|
|
596
|
+
},
|
|
597
|
+
}),
|
|
598
|
+
}),
|
|
582
599
|
prompt: `Turn this result into the requested structure. Keep every number and source exactly.\n\n${text}`,
|
|
583
600
|
});
|
|
584
601
|
await ctx.chargeUsd(costOf(structurer, structured.usage));
|
|
585
602
|
output = structured.output;
|
|
586
603
|
}
|
|
587
|
-
const json =
|
|
588
|
-
|
|
604
|
+
const json = recordsOf(shape, output);
|
|
605
|
+
// A view written once is bound to this run's data; `true` asks a model for one.
|
|
606
|
+
const surface = Array.isArray(step.output.surface)
|
|
607
|
+
? { components: step.output.surface, data: json }
|
|
608
|
+
: step.output.surface
|
|
609
|
+
? await composeSurface(step, json, ctx)
|
|
610
|
+
: undefined;
|
|
589
611
|
return {
|
|
590
612
|
text,
|
|
591
613
|
json,
|
|
@@ -647,7 +669,7 @@ const DOCUMENT_GUIDES = {
|
|
|
647
669
|
offer: "A commercial offer: sender and recipient, offer number and date, a short personal intro, the scope of work, a positions table, net / VAT / total, validity and next steps. Use the totals EXACTLY as given.",
|
|
648
670
|
briefing: "A research briefing: title, a three-sentence executive summary, key findings with numbers, sections with headings, implications, and a numbered source list with links.",
|
|
649
671
|
letter: "A business letter with sender, recipient, date, subject line, body and signature.",
|
|
650
|
-
report: "A structured report with title, summary
|
|
672
|
+
report: "A structured report with title, summary and sections with headings; findings and items as short headed blocks, a table only for figures compared across columns.",
|
|
651
673
|
free: "A well-structured document fitting the brief.",
|
|
652
674
|
};
|
|
653
675
|
/** Images a document may place: earlier generated images, uploads, the project's logos and images. */
|
|
@@ -717,19 +739,29 @@ function placeableImages(step, ctx) {
|
|
|
717
739
|
async function writeHtml(step, ctx, kind) {
|
|
718
740
|
const brief = renderTemplate(step.prompt, ctx.scope);
|
|
719
741
|
const images = placeableImages(step, ctx);
|
|
720
|
-
|
|
742
|
+
// The look comes from the space's brand, as on the run page; what it does not say, the runner's own.
|
|
743
|
+
const theme = runTheme(ctx.scope.brand);
|
|
744
|
+
const accent = theme.accent ?? "#e0531b";
|
|
745
|
+
const look = [
|
|
746
|
+
`headings in ${theme.display ? `"${theme.display}"` : "a characterful sans"}, text in ${theme.body ? `"${theme.body}"` : "a clean sans"} (each with a system font behind it)`,
|
|
747
|
+
`the accent ${accent} for marks and figures`,
|
|
748
|
+
theme.second ? `${theme.second} for what carries meaning (done, there, good)` : "",
|
|
749
|
+
theme.background ? `a ground in the tone of ${theme.background}` : "",
|
|
750
|
+
]
|
|
751
|
+
.filter(Boolean)
|
|
752
|
+
.join(", ");
|
|
721
753
|
const guide = kind === "dashboard"
|
|
722
754
|
? [
|
|
723
755
|
"Build a single self-contained HTML dashboard (one file).",
|
|
724
756
|
"Charts are INLINE SVG you draw yourself — no external scripts, no CDN, no network requests. Bar, line and donut charts with axis labels and values.",
|
|
725
757
|
"Interactivity (tabs, tooltips, a slider over time, filters) may use a small inline <script>. Everything must also read well without interacting.",
|
|
726
|
-
"Layout: a title row, 3–6
|
|
727
|
-
`
|
|
758
|
+
"Layout: a title row with one plain sentence of what it shows, 3–6 key figures as tiles, then the charts; things (offers, products, places, people) as cards; a table only for figures compared across columns; a 'Sources' list at the bottom.",
|
|
759
|
+
`Design: ${look}; generous whitespace, cards for things, width-responsive with CSS grid.`,
|
|
728
760
|
].join("\n")
|
|
729
761
|
: [
|
|
730
762
|
"Write a single self-contained, print-ready HTML document (A4).",
|
|
731
763
|
DOCUMENT_GUIDES[step.options?.template ?? "free"],
|
|
732
|
-
`Design: @page A4 margins 18mm,
|
|
764
|
+
`Design: @page A4 margins 18mm, 10.5pt body, ${look}; a clear type scale, thin rules. No external assets, no scripts.`,
|
|
733
765
|
].join("\n");
|
|
734
766
|
const system = [
|
|
735
767
|
groundRules(ctx),
|