wizards 0.2.25 → 0.2.27
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/agents/architect.js +5 -0
- package/apps/runtime/dist/agents/project-assistant.js +10 -1
- package/apps/runtime/dist/authoring/guide.js +61 -8
- package/apps/runtime/dist/connectors/builtin.js +17 -1
- package/apps/runtime/dist/connectors/index.js +43 -14
- package/apps/runtime/dist/connectors/plugin-connections.js +248 -0
- package/apps/runtime/dist/connectors/via-account.js +63 -0
- package/apps/runtime/dist/credits/estimate.js +12 -2
- package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +21 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +25 -3
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +29 -1
- package/apps/runtime/dist/engine/decide.js +82 -0
- package/apps/runtime/dist/engine/input.js +21 -5
- package/apps/runtime/dist/engine/requirements.js +80 -15
- package/apps/runtime/dist/engine/runner.js +252 -6
- package/apps/runtime/dist/engine/steps.js +206 -18
- package/apps/runtime/dist/engine/surface.js +90 -0
- package/apps/runtime/dist/mcp/tools.js +3 -2
- package/apps/runtime/dist/plugins/loader.js +22 -1
- package/apps/runtime/dist/plugins/registry.js +10 -0
- package/apps/runtime/dist/render/downloads.js +7 -0
- package/apps/runtime/dist/routes/runs.js +28 -7
- package/apps/runtime/dist/routes/studio.js +3 -1
- package/apps/runtime/dist/services/marketplace.js +134 -3
- package/apps/runtime/dist/services/relevance.js +17 -49
- package/apps/runtime/dist/services/runs.js +43 -15
- package/apps/runtime/dist/services/wizards.js +4 -2
- package/apps/runtime/dist/tools/pages.js +31 -6
- package/apps/runtime/dist/tools/plugin.js +29 -1
- package/apps/web/dist/assets/Account-DW396ig5.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-B7C0r9dF.js → ChangelogDialog-BdZ1SmEd.js} +1 -1
- package/apps/web/dist/assets/CreditsDialog-fOYdoev3.js +1 -0
- package/apps/web/dist/assets/EditorPage-CAARFsaI.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-D0m5Qk6S.js → FlowPreview-U2AEnKlX.js} +1 -1
- package/apps/web/dist/assets/{Harness-bIL3pHB_.js → Harness-CtDlm_2b.js} +2 -2
- package/apps/web/dist/assets/HomePage-Bd-mkLxW.js +1 -0
- package/apps/web/dist/assets/ImportWizard-BMPGjd_t.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-DjSNCRnV.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-DZPcDR9i.js +2 -0
- package/apps/web/dist/assets/ProjectPage-_LAtG3tn.js +2 -0
- package/apps/web/dist/assets/PublicRunner-6Iqp0DmU.js +1 -0
- package/apps/web/dist/assets/RunDrawer-CtQoY3Ha.js +4 -0
- package/apps/web/dist/assets/RunnerView-BLOCNZaZ.js +2 -0
- package/apps/web/dist/assets/SettingsPage-Z6skBf6I.js +1 -0
- package/apps/web/dist/assets/SetupPage-4-Lic4pT.js +1 -0
- package/apps/web/dist/assets/SharePage-DfdhXxZm.js +1 -0
- package/apps/web/dist/assets/arrow-right-BOtqnsA1.js +1 -0
- package/apps/web/dist/assets/brand-CCAI9p0f.js +2 -0
- package/apps/web/dist/assets/changelog-DQM0oz9S.js +1 -0
- package/apps/web/dist/assets/circle-alert-BC6xmJ9r.js +1 -0
- package/apps/web/dist/assets/definition-D5V79jHA.js +1 -0
- package/apps/web/dist/assets/esm-FEWi61F_.js +7 -0
- package/apps/web/dist/assets/external-link-wKL1HE6G.js +1 -0
- package/apps/web/dist/assets/image-BMdSChOR.js +1 -0
- package/apps/web/dist/assets/index-BOp2fz-2.css +1 -0
- package/apps/web/dist/assets/index-YJMooz0J.js +132 -0
- package/apps/web/dist/assets/lock-BpqzLMuv.js +1 -0
- package/apps/web/dist/assets/meta-BqZ7SlZz.js +1 -0
- package/apps/web/dist/assets/pen-line-DM-u0dES.js +1 -0
- package/apps/web/dist/assets/refresh-cw-Bk7HkOIM.js +1 -0
- package/apps/web/dist/assets/settings-menu-BM_NfZfn.js +1 -0
- package/apps/web/dist/assets/star-JHzYYl5R.js +1 -0
- package/apps/web/dist/assets/trash-CdLg-Ela.js +1 -0
- package/apps/web/dist/assets/ui-8QmvuC9u.js +9 -0
- package/apps/web/dist/assets/upload-CL1R6shM.js +1 -0
- package/apps/web/dist/assets/video-BioLmXNO.js +1 -0
- package/apps/web/dist/assets/volume-2-_IOfPNCv.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-DYtDQSJZ.js +1 -0
- package/apps/web/dist/index.html +4 -5
- package/apps/web/dist/mcp/flow.html +61 -61
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +85 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +23 -0
- package/node_modules/@engenty-wizards/shared/dist/decision.d.ts +76 -0
- package/node_modules/@engenty-wizards/shared/dist/decision.js +147 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1076 -51
- package/node_modules/@engenty-wizards/shared/dist/definition.js +597 -44
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +19 -2
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +6 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +55 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +89 -0
- package/node_modules/@engenty-wizards/shared/dist/surface.js +198 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/Account-CpxsROpe.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-Com2Y3g8.js +0 -1
- package/apps/web/dist/assets/EditorPage-BCagWjrz.js +0 -2
- package/apps/web/dist/assets/HomePage-DPE_Z9HB.js +0 -1
- package/apps/web/dist/assets/ImportWizard-Dz2uKznp.js +0 -1
- package/apps/web/dist/assets/LocalRuntime-wMKUVZOu.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-BOxU2jWq.js +0 -2
- package/apps/web/dist/assets/ProjectPage-BJp2wqIk.js +0 -5
- package/apps/web/dist/assets/PublicRunner-BsrqVK0J.js +0 -1
- package/apps/web/dist/assets/RunDrawer-CLfJ0W_V.js +0 -1
- package/apps/web/dist/assets/RunnerView-BXMELFC4.js +0 -70
- package/apps/web/dist/assets/SettingsPage-BkuUm-bf.js +0 -1
- package/apps/web/dist/assets/SetupPage-CoyXpcvS.js +0 -1
- package/apps/web/dist/assets/SharePage-BuR-he1L.js +0 -1
- package/apps/web/dist/assets/api-rJBQEu1G.js +0 -10
- package/apps/web/dist/assets/arrow-right-6FyN3e7D.js +0 -1
- package/apps/web/dist/assets/changelog-Bmw6dg-T.js +0 -1
- package/apps/web/dist/assets/check-CUpby_J9.js +0 -1
- package/apps/web/dist/assets/chevron-down-DymW-2BP.js +0 -1
- package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +0 -1
- package/apps/web/dist/assets/createLucideIcon-q3MZMmnf.js +0 -1
- package/apps/web/dist/assets/definition-BznNdlhf.js +0 -55
- package/apps/web/dist/assets/esm-BrGYYg2h.js +0 -7
- package/apps/web/dist/assets/external-link-MAL7vJYj.js +0 -1
- package/apps/web/dist/assets/image-CC_2mJd6.js +0 -1
- package/apps/web/dist/assets/index-BZs1IxAk.css +0 -1
- package/apps/web/dist/assets/index-Co5QOA9_.js +0 -10
- package/apps/web/dist/assets/lock-DZL2N67v.js +0 -1
- package/apps/web/dist/assets/meta-B3jzXP27.js +0 -1
- package/apps/web/dist/assets/pen-line-NK46C_nG.js +0 -1
- package/apps/web/dist/assets/plus-Du-Jx8aI.js +0 -1
- package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +0 -1
- package/apps/web/dist/assets/settings-menu-CCNUb0Fd.js +0 -1
- package/apps/web/dist/assets/table-2-BdDuBcF2.js +0 -1
- package/apps/web/dist/assets/trash-0IafyhZB.js +0 -1
- package/apps/web/dist/assets/ui-DJOZSSKo.js +0 -1
- package/apps/web/dist/assets/upload-DNRENWzQ.js +0 -1
- package/apps/web/dist/assets/video-9fDPRPCv.js +0 -1
- package/apps/web/dist/assets/volume-2-DE67_rue.js +0 -1
- package/apps/web/dist/assets/wand-sparkles-Clj3VRB5.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { marginOf, } from "@engenty-wizards/shared/decision";
|
|
2
|
+
import { branchValues, conditionsOf, dataRef, followRules, formatsFor, isDecidedField, LIST_FORMATS, listRef, loopKey, MAX_SHOWN_FIELDS, optionsFromData, otherwiseOf, pageNeeds, whenHolds, } from "@engenty-wizards/shared/definition";
|
|
2
3
|
import { eq } from "drizzle-orm";
|
|
3
4
|
import { nanoid } from "nanoid";
|
|
4
5
|
import { connectionViews } from "../connectors/index.js";
|
|
@@ -17,13 +18,14 @@ import { listRows, scopeOf } from "../store/index.js";
|
|
|
17
18
|
import { activeRuns, indexRun, markRunActive } from "../tenants/control.js";
|
|
18
19
|
import { currentTenant } from "../tenants/tenant.js";
|
|
19
20
|
import { askPerson, clearStaleAsk, unattended } from "./asks.js";
|
|
21
|
+
import { decide } from "./decide.js";
|
|
20
22
|
import { emitEvent, recentEvents, signalChanged } from "./events.js";
|
|
21
23
|
import { readPageInput } from "./input.js";
|
|
22
24
|
import { pushRun } from "./push.js";
|
|
23
25
|
import { blockingMessage, closedChoices, missingModels, openValue, stepsWithoutModel, } from "./requirements.js";
|
|
24
26
|
import { releaseResources, resourcesFor } from "./resources.js";
|
|
25
27
|
import { runAutomaticStep } from "./steps.js";
|
|
26
|
-
import { resolveRef } from "./template.js";
|
|
28
|
+
import { answersAsText, resolveRef } from "./template.js";
|
|
27
29
|
import { StepError } from "./types.js";
|
|
28
30
|
/** A command the run cannot take in its current state. */
|
|
29
31
|
export class RunConflict extends Error {
|
|
@@ -226,6 +228,45 @@ export function kick(runId) {
|
|
|
226
228
|
signalChanged(runId);
|
|
227
229
|
});
|
|
228
230
|
}
|
|
231
|
+
/** What conditions read in this run: answers, outputs, and how many rows each stored list has. */
|
|
232
|
+
async function flowValues(run, state) {
|
|
233
|
+
const counts = {};
|
|
234
|
+
if (run.definition.lists?.length) {
|
|
235
|
+
for (const [id, list] of Object.entries(await storedLists(run))) {
|
|
236
|
+
counts[id] = list.rows.length;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return branchValues(state.values, state.outputs, counts);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* What a page knows besides the person's input: what its fields' `when` read from before the
|
|
243
|
+
* page (its own earlier answers left out, so a revisit decides on what is typed now) and the
|
|
244
|
+
* choices its `optionsFrom` fields find in the run.
|
|
245
|
+
*/
|
|
246
|
+
async function pageContext(run, step) {
|
|
247
|
+
const needs = pageNeeds(step.fields);
|
|
248
|
+
const sourced = step.fields.filter((f) => f.optionsFrom);
|
|
249
|
+
const decided = step.fields.some(isDecidedField) ? (run.state.pages?.[step.id]?.shown ?? []) : [];
|
|
250
|
+
if (!needs.length && !sourced.length) {
|
|
251
|
+
return { known: {}, options: {}, decided };
|
|
252
|
+
}
|
|
253
|
+
const all = await flowValues(run, run.state);
|
|
254
|
+
const known = Object.fromEntries(needs.map((ref) => [ref, all[ref]]));
|
|
255
|
+
const lists = sourced.some((f) => f.optionsFrom?.startsWith("lists."))
|
|
256
|
+
? await storedLists(run)
|
|
257
|
+
: {};
|
|
258
|
+
const options = {};
|
|
259
|
+
for (const field of sourced) {
|
|
260
|
+
const found = optionsFromData(field.optionsFrom ?? "", run.state.outputs, lists);
|
|
261
|
+
if (found.length) {
|
|
262
|
+
options[field.id] = found;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { known, options, decided };
|
|
266
|
+
}
|
|
267
|
+
function viewContext(ctx) {
|
|
268
|
+
return { known: ctx.known, options: ctx.options, decidedFields: ctx.decided };
|
|
269
|
+
}
|
|
229
270
|
/** The wizard's lists with the person's rows, by list id. */
|
|
230
271
|
async function storedLists(run) {
|
|
231
272
|
const scope = scopeOf(run);
|
|
@@ -243,6 +284,7 @@ async function makeContext(run, project, state, signal) {
|
|
|
243
284
|
return {
|
|
244
285
|
runId: run.id,
|
|
245
286
|
tenantId: run.tenantId,
|
|
287
|
+
test: run.mode === "test",
|
|
246
288
|
stepId,
|
|
247
289
|
store,
|
|
248
290
|
def: run.definition,
|
|
@@ -271,6 +313,146 @@ async function makeContext(run, project, state, signal) {
|
|
|
271
313
|
}),
|
|
272
314
|
};
|
|
273
315
|
}
|
|
316
|
+
/** A decided branch is taken only when its pick leads the runner-up by this much. */
|
|
317
|
+
const BRANCH_MARGIN = 0.1;
|
|
318
|
+
/**
|
|
319
|
+
* Where the run goes after `stepId`: a `when` rule or the next step at once — counted, so a loop
|
|
320
|
+
* stops at its rule's max — or, for `ask` rules, a decision the drive loop makes (`deciding`).
|
|
321
|
+
*/
|
|
322
|
+
async function advance(run, stepId, state) {
|
|
323
|
+
const outcome = followRules(run.definition, stepId, await flowValues(run, state), state.loops);
|
|
324
|
+
if (outcome.kind === "decide") {
|
|
325
|
+
state.deciding = stepId;
|
|
326
|
+
return stepId;
|
|
327
|
+
}
|
|
328
|
+
return taken(run.definition, stepId, outcome.rule, outcome.cursor, state);
|
|
329
|
+
}
|
|
330
|
+
function taken(def, stepId, rule, cursor, state) {
|
|
331
|
+
const goto = rule === null ? null : stepOf(def, stepId)?.next?.[rule]?.goto;
|
|
332
|
+
if (goto) {
|
|
333
|
+
const key = loopKey(stepId, goto);
|
|
334
|
+
state.loops = { ...state.loops, [key]: (state.loops?.[key] ?? 0) + 1 };
|
|
335
|
+
}
|
|
336
|
+
return cursor;
|
|
337
|
+
}
|
|
338
|
+
/** What a decision about the run reads: the person's answers and what the steps made so far. */
|
|
339
|
+
function runSummary(run, state) {
|
|
340
|
+
const results = {};
|
|
341
|
+
for (const step of run.definition.steps) {
|
|
342
|
+
const out = state.outputs[step.id];
|
|
343
|
+
const text = out?.text ?? (out?.json === undefined ? "" : JSON.stringify(out.json));
|
|
344
|
+
if (text) {
|
|
345
|
+
results[step.title] = text.slice(0, 6000);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
answers: answersAsText({ def: run.definition, state, brand: {} }),
|
|
350
|
+
results,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The decision over a step's open `ask` rules: one choice among their statements and "none".
|
|
355
|
+
* Kept in the run; a pick that does not lead clearly (where there are probabilities) is none.
|
|
356
|
+
*/
|
|
357
|
+
async function decideBranch(run, step, state, signal) {
|
|
358
|
+
const outcome = followRules(run.definition, step.id, {}, state.loops);
|
|
359
|
+
const rules = outcome.kind === "decide" ? outcome.rules : [];
|
|
360
|
+
const criteria = {};
|
|
361
|
+
for (const i of rules) {
|
|
362
|
+
criteria[`r${i}`] = step.next?.[i]?.ask ?? "";
|
|
363
|
+
}
|
|
364
|
+
criteria.none = "None of these clearly holds: go on with the next step.";
|
|
365
|
+
await emitEvent(run.id, step.id, "info", { code: "deciding" });
|
|
366
|
+
const decision = await decide({
|
|
367
|
+
state: runSummary(run, state),
|
|
368
|
+
questions: {
|
|
369
|
+
next: {
|
|
370
|
+
type: "choice",
|
|
371
|
+
instructions: "Which statement is true of this run? Answer none unless one clearly holds.",
|
|
372
|
+
criteria,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
call: { runId: run.id, stepId: step.id },
|
|
376
|
+
signal,
|
|
377
|
+
charge: (usd) => addLocalCost(run.id, step.id, usd),
|
|
378
|
+
});
|
|
379
|
+
const answer = decision.answers.next;
|
|
380
|
+
const margin = marginOf(answer);
|
|
381
|
+
const pick = margin !== null && margin < BRANCH_MARGIN ? "none" : answer.choice;
|
|
382
|
+
const rule = pick === "none" ? null : Number(pick.slice(1));
|
|
383
|
+
state.decisions = {
|
|
384
|
+
...state.decisions,
|
|
385
|
+
[step.id]: {
|
|
386
|
+
rule,
|
|
387
|
+
source: decision.source,
|
|
388
|
+
...(answer.probabilities ? { probabilities: answer.probabilities } : {}),
|
|
389
|
+
at: new Date().toISOString(),
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
delete state.deciding;
|
|
393
|
+
if (rule === null) {
|
|
394
|
+
const otherwise = otherwiseOf(run.definition, step.id, state.loops);
|
|
395
|
+
return taken(run.definition, step.id, otherwise.rule, otherwise.cursor, state);
|
|
396
|
+
}
|
|
397
|
+
const goto = step.next?.[rule]?.goto ?? "end";
|
|
398
|
+
return taken(run.definition, step.id, rule, goto === "end" ? null : goto, state);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The decided fields (`ask`, `group`) of a page a decision keeps, in one call as the run reaches
|
|
402
|
+
* the page. A field whose condition on earlier data already fails is not asked about. With the
|
|
403
|
+
* fields always shown, at most MAX_SHOWN_FIELDS appear.
|
|
404
|
+
*/
|
|
405
|
+
async function decidePageFields(run, page, state, signal) {
|
|
406
|
+
const values = await flowValues(run, state);
|
|
407
|
+
const own = new Set(page.fields.map((f) => f.id));
|
|
408
|
+
const possible = page.fields.filter((f) => isDecidedField(f) &&
|
|
409
|
+
whenHolds(conditionsOf(f.when).filter((c) => !own.has(c.field)), values));
|
|
410
|
+
const questions = {};
|
|
411
|
+
for (const f of possible.filter((f) => f.ask)) {
|
|
412
|
+
questions[`f_${f.id}`] = {
|
|
413
|
+
type: "noul",
|
|
414
|
+
instructions: `Should the page ask "${f.label}"? It is asked when this holds: ${f.ask}`,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
for (const g of page.groups ?? []) {
|
|
418
|
+
const members = possible.filter((f) => f.group === g.id);
|
|
419
|
+
if (members.length) {
|
|
420
|
+
questions[`g_${g.id}`] = {
|
|
421
|
+
type: "choice",
|
|
422
|
+
instructions: g.instructions,
|
|
423
|
+
criteria: {
|
|
424
|
+
...Object.fromEntries(members.map((f) => [f.id, f.help ? `${f.label} — ${f.help}` : f.label])),
|
|
425
|
+
...(g.optional ? { none: "None of these." } : {}),
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (!Object.keys(questions).length) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
await emitEvent(run.id, page.id, "info", { code: "choosingFields" });
|
|
434
|
+
const decision = await decide({
|
|
435
|
+
state: runSummary(run, state),
|
|
436
|
+
questions,
|
|
437
|
+
call: { runId: run.id, stepId: page.id },
|
|
438
|
+
signal,
|
|
439
|
+
charge: (usd) => addLocalCost(run.id, page.id, usd),
|
|
440
|
+
});
|
|
441
|
+
const kept = new Set();
|
|
442
|
+
for (const [name, answer] of Object.entries(decision.answers)) {
|
|
443
|
+
if (answer.type === "noul" && answer.noul >= 0.5) {
|
|
444
|
+
kept.add(name.slice(2));
|
|
445
|
+
}
|
|
446
|
+
else if (answer.type === "choice" && answer.choice !== "none") {
|
|
447
|
+
kept.add(answer.choice);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const room = MAX_SHOWN_FIELDS - page.fields.filter((f) => !isDecidedField(f)).length;
|
|
451
|
+
return page.fields
|
|
452
|
+
.filter((f) => isDecidedField(f) && kept.has(f.id))
|
|
453
|
+
.slice(0, Math.max(0, room))
|
|
454
|
+
.map((f) => f.id);
|
|
455
|
+
}
|
|
274
456
|
async function drive(runId, signal) {
|
|
275
457
|
await clearStaleAsk(runId);
|
|
276
458
|
while (!signal.aborted) {
|
|
@@ -279,11 +461,59 @@ async function drive(runId, signal) {
|
|
|
279
461
|
return;
|
|
280
462
|
}
|
|
281
463
|
const def = run.definition;
|
|
464
|
+
// A step's `ask` branches wait on a decision before the run goes on.
|
|
465
|
+
const deciding = run.state.deciding ? stepOf(def, run.state.deciding) : undefined;
|
|
466
|
+
if (deciding) {
|
|
467
|
+
const state = structuredClone(run.state);
|
|
468
|
+
try {
|
|
469
|
+
const cursor = await decideBranch(run, deciding, state, signal);
|
|
470
|
+
if (signal.aborted) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
await syncRunCost(runId);
|
|
474
|
+
await updateRun(runId, { state, cursor, status: cursor ? "running" : "done", error: null });
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
if (signal.aborted) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
console.error(`[run ${runId} deciding ${deciding.id}]`, err);
|
|
481
|
+
const message = friendly(err);
|
|
482
|
+
await emitEvent(runId, deciding.id, "error", message);
|
|
483
|
+
await updateRun(runId, { status: "failed", error: message });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
282
488
|
const step = stepOf(def, run.cursor);
|
|
283
489
|
if (!step) {
|
|
284
490
|
await updateRun(runId, { status: "done" });
|
|
285
491
|
return;
|
|
286
492
|
}
|
|
493
|
+
// A page whose fields a decision picks: picked once, as the run reaches it.
|
|
494
|
+
if (step.type === "page" && step.fields.some(isDecidedField) && !run.state.pages?.[step.id]) {
|
|
495
|
+
const state = structuredClone(run.state);
|
|
496
|
+
try {
|
|
497
|
+
const shown = await decidePageFields(run, step, state, signal);
|
|
498
|
+
if (signal.aborted) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
state.pages = { ...state.pages, [step.id]: { shown } };
|
|
502
|
+
await syncRunCost(runId);
|
|
503
|
+
await updateRun(runId, { state, error: null });
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
if (signal.aborted) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
console.error(`[run ${runId} fields of ${step.id}]`, err);
|
|
510
|
+
const message = friendly(err);
|
|
511
|
+
await emitEvent(runId, step.id, "error", message);
|
|
512
|
+
await updateRun(runId, { status: "failed", error: message });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
287
517
|
if (step.type === "result") {
|
|
288
518
|
await updateRun(runId, { status: "done" });
|
|
289
519
|
return;
|
|
@@ -312,7 +542,7 @@ async function drive(runId, signal) {
|
|
|
312
542
|
state.outputs[step.id] = output;
|
|
313
543
|
delete state.notes[step.id];
|
|
314
544
|
delete state.redo?.[step.id];
|
|
315
|
-
const cursor =
|
|
545
|
+
const cursor = await advance(run, step.id, state);
|
|
316
546
|
await updateRun(runId, { state, cursor, status: cursor ? "running" : "done", error: null });
|
|
317
547
|
await emitEvent(runId, step.id, "step_done", step.title);
|
|
318
548
|
}
|
|
@@ -363,7 +593,7 @@ export async function submitPage(runId, stepId, input) {
|
|
|
363
593
|
if (step?.type !== "page") {
|
|
364
594
|
throw new RunConflict("Not a page");
|
|
365
595
|
}
|
|
366
|
-
const { values, errors } = readPageInput(step, input);
|
|
596
|
+
const { values, errors } = readPageInput(step, input, await pageContext(run, step));
|
|
367
597
|
// An answer that leads to a step without a model here would stop the run there, later.
|
|
368
598
|
const closed = await closedChoices(run.definition, step, run.state.values);
|
|
369
599
|
for (const [id, choice] of Object.entries(closed)) {
|
|
@@ -408,8 +638,13 @@ export async function submitPage(runId, stepId, input) {
|
|
|
408
638
|
delete state.values[field.id];
|
|
409
639
|
}
|
|
410
640
|
}
|
|
641
|
+
// Later pages decide their fields again on what is answered now.
|
|
642
|
+
const at = run.definition.steps.indexOf(step);
|
|
643
|
+
for (const later of run.definition.steps.slice(at + 1)) {
|
|
644
|
+
delete state.pages?.[later.id];
|
|
645
|
+
}
|
|
411
646
|
state.history.push(stepId);
|
|
412
|
-
const cursor =
|
|
647
|
+
const cursor = await advance(run, stepId, state);
|
|
413
648
|
await updateRun(runId, { state, cursor, status: "running", error: null });
|
|
414
649
|
kick(runId);
|
|
415
650
|
}
|
|
@@ -427,7 +662,7 @@ export async function reviewStep(runId, stepId, action) {
|
|
|
427
662
|
}
|
|
428
663
|
}
|
|
429
664
|
state.history.push(stepId);
|
|
430
|
-
const cursor =
|
|
665
|
+
const cursor = await advance(run, stepId, state);
|
|
431
666
|
await updateRun(runId, { state, cursor, status: "running", error: null });
|
|
432
667
|
kick(runId);
|
|
433
668
|
return;
|
|
@@ -458,6 +693,8 @@ export async function goBack(runId) {
|
|
|
458
693
|
if (!previous) {
|
|
459
694
|
throw new RunConflict("Kein vorheriger Schritt.");
|
|
460
695
|
}
|
|
696
|
+
// A decision that failed belongs to the step left behind.
|
|
697
|
+
delete state.deciding;
|
|
461
698
|
await updateRun(runId, { state, cursor: previous, status: "waiting_input", error: null });
|
|
462
699
|
}
|
|
463
700
|
export async function retry(runId) {
|
|
@@ -578,6 +815,15 @@ export async function runView(run, brand) {
|
|
|
578
815
|
step,
|
|
579
816
|
values: run.state.values,
|
|
580
817
|
prefill: step?.type === "page" ? prefillOf(step, run, closed) : {},
|
|
818
|
+
...(step?.type === "page" ? viewContext(await pageContext(run, step)) : {}),
|
|
819
|
+
...(run.mode === "test" && run.state.decisions
|
|
820
|
+
? {
|
|
821
|
+
decisions: Object.fromEntries(Object.entries(run.state.decisions).map(([id, d]) => [
|
|
822
|
+
id,
|
|
823
|
+
{ ...d, over: def.steps.find((s) => s.id === id)?.next ?? [] },
|
|
824
|
+
])),
|
|
825
|
+
}
|
|
826
|
+
: {}),
|
|
581
827
|
closed,
|
|
582
828
|
outputs: Object.fromEntries(shownIds.map((id) => [id, run.state.outputs[id]]).filter(([, o]) => o)),
|
|
583
829
|
shown,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { pOf } from "@engenty-wizards/shared/decision";
|
|
1
2
|
import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
|
+
import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
|
|
2
4
|
import { Agent } from "@mastra/core/agent";
|
|
3
5
|
import { generateText, Output } from "ai";
|
|
4
6
|
import { z } from "zod";
|
|
@@ -12,7 +14,9 @@ import { buildStepTools } from "../tools/index.js";
|
|
|
12
14
|
import { spaceContextOf } from "../tools/plugin.js";
|
|
13
15
|
import { personUploads } from "../tools/store.js";
|
|
14
16
|
import { runWidgetStep } from "../widgets/step.js";
|
|
17
|
+
import { decide } from "./decide.js";
|
|
15
18
|
import { prepareInputs } from "./prepare.js";
|
|
19
|
+
import { runSurfaceStep } from "./surface.js";
|
|
16
20
|
import { answersAsText, colorLine, factLines, renderTemplate, resolveRef, } from "./template.js";
|
|
17
21
|
import { StepError } from "./types.js";
|
|
18
22
|
/** Today's date as the wizard's people write it. */
|
|
@@ -51,7 +55,12 @@ async function previousResult(ctx, stepId) {
|
|
|
51
55
|
async function revisionBlock(ctx, stepId) {
|
|
52
56
|
const note = ctx.state.notes[stepId];
|
|
53
57
|
if (!note) {
|
|
54
|
-
|
|
58
|
+
// A branch back to this step (a loop) runs it again: it builds on what it made last time.
|
|
59
|
+
const looped = Object.entries(ctx.state.loops ?? {}).some(([key, n]) => n > 0 && key.endsWith(`→${stepId}`));
|
|
60
|
+
const prev = looped ? await previousResult(ctx, stepId) : "";
|
|
61
|
+
return prev
|
|
62
|
+
? `# EARLIER ATTEMPT\nA later step sent the run back to this step. What it made last time — improve on it with what is known now:\n"""\n${prev}\n"""`
|
|
63
|
+
: "";
|
|
55
64
|
}
|
|
56
65
|
const prev = await previousResult(ctx, stepId);
|
|
57
66
|
return `# REVISION REQUEST\nThe person reviewed the previous result and asked for this change:\n"""${note}"""\n\n${prev ? `Previous result:\n"""\n${prev}\n"""` : ""}`;
|
|
@@ -88,6 +97,11 @@ function outputSchema(step) {
|
|
|
88
97
|
case "choice":
|
|
89
98
|
shape[f.id] = z.enum(f.options).describe(d);
|
|
90
99
|
break;
|
|
100
|
+
case "score":
|
|
101
|
+
shape[f.id] = z
|
|
102
|
+
.enum(f.options)
|
|
103
|
+
.describe(`${d} (lowest to highest)`);
|
|
104
|
+
break;
|
|
91
105
|
case "table":
|
|
92
106
|
shape[f.id] = f.columns?.length
|
|
93
107
|
? z
|
|
@@ -148,7 +162,186 @@ async function seenPhotos(step, ctx, resolved, uploads) {
|
|
|
148
162
|
}
|
|
149
163
|
return out;
|
|
150
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* A decision step: its yes/no, choice and score fields are the questions, its prompt the state,
|
|
167
|
+
* answered in one call through decide() — with probabilities where a decision model answers.
|
|
168
|
+
*/
|
|
169
|
+
async function decideStep(step, prompt, ctx) {
|
|
170
|
+
const fields = step.output.fields ?? [];
|
|
171
|
+
const questions = {};
|
|
172
|
+
for (const f of fields) {
|
|
173
|
+
const instructions = f.description ?? f.id;
|
|
174
|
+
if (f.kind === "yesno") {
|
|
175
|
+
questions[f.id] = { type: "noul", instructions };
|
|
176
|
+
}
|
|
177
|
+
else if (f.kind === "choice") {
|
|
178
|
+
questions[f.id] = {
|
|
179
|
+
type: "choice",
|
|
180
|
+
instructions,
|
|
181
|
+
criteria: Object.fromEntries((f.options ?? []).map((o) => [o, null])),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
else if (f.kind === "score") {
|
|
185
|
+
questions[f.id] = { type: "score", instructions, criteria: f.options ?? [] };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const decision = await decide({
|
|
189
|
+
state: prompt,
|
|
190
|
+
questions,
|
|
191
|
+
call: ctx.call,
|
|
192
|
+
signal: ctx.signal,
|
|
193
|
+
charge: ctx.chargeUsd,
|
|
194
|
+
});
|
|
195
|
+
const json = {};
|
|
196
|
+
const decided = {};
|
|
197
|
+
for (const [id, answer] of Object.entries(decision.answers)) {
|
|
198
|
+
json[id] =
|
|
199
|
+
answer.type === "noul"
|
|
200
|
+
? answer.noul >= 0.5
|
|
201
|
+
: answer.type === "choice"
|
|
202
|
+
? answer.choice
|
|
203
|
+
: answer.label;
|
|
204
|
+
decided[id] = Math.round(pOf(answer) * 1000) / 1000;
|
|
205
|
+
}
|
|
206
|
+
const text = fields
|
|
207
|
+
.map((f) => {
|
|
208
|
+
const value = json[f.id];
|
|
209
|
+
return `${f.description ?? f.id}: ${value === true ? "ja" : value === false ? "nein" : String(value)}`;
|
|
210
|
+
})
|
|
211
|
+
.join("\n");
|
|
212
|
+
return { text, json, decided, at: new Date().toISOString() };
|
|
213
|
+
}
|
|
214
|
+
/** The JSON array in a model's answer, fenced or not. */
|
|
215
|
+
function jsonArrayIn(text) {
|
|
216
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1] ?? text;
|
|
217
|
+
const start = fenced.indexOf("[");
|
|
218
|
+
const end = fenced.lastIndexOf("]");
|
|
219
|
+
if (start < 0 || end <= start) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(fenced.slice(start, end + 1));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The view an agent step composes of its result: the standard class writes components from the
|
|
231
|
+
* surface catalog bound to the step's fields, the catalog's validator checks them, and one try
|
|
232
|
+
* with the issues follows. Without a valid view the plain result shows: the step never fails
|
|
233
|
+
* for its view.
|
|
234
|
+
*/
|
|
235
|
+
async function composeSurface(step, json, ctx) {
|
|
236
|
+
const keys = Object.keys(json);
|
|
237
|
+
const fields = (step.output.fields ?? [])
|
|
238
|
+
.map((f) => `- /${f.id} (${f.kind}${f.columns ? `: ${f.columns.join(", ")}` : ""}): ${f.description ?? ""}`)
|
|
239
|
+
.join("\n");
|
|
240
|
+
const sample = JSON.stringify(json).slice(0, 4000);
|
|
241
|
+
try {
|
|
242
|
+
const writer = await textModel("standard", ctx.call);
|
|
243
|
+
let issues = [];
|
|
244
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
245
|
+
const result = await generateText({
|
|
246
|
+
model: writer.model,
|
|
247
|
+
abortSignal: ctx.signal,
|
|
248
|
+
prompt: [
|
|
249
|
+
`Compose a view of this result for the person who ran "${ctx.def.title}". Answer with the JSON array of components only.`,
|
|
250
|
+
SURFACE_GUIDE,
|
|
251
|
+
`# THE DATA (bind by path, never copy values)\n${fields}\n\nSample: ${sample}`,
|
|
252
|
+
issues.length
|
|
253
|
+
? `# YOUR LAST ANSWER HAD THESE PROBLEMS — fix them\n${issues.join("\n")}`
|
|
254
|
+
: "",
|
|
255
|
+
]
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.join("\n\n"),
|
|
258
|
+
});
|
|
259
|
+
await ctx.chargeUsd(costOf(writer, result.usage));
|
|
260
|
+
const components = jsonArrayIn(result.text);
|
|
261
|
+
issues = validateSurface(components, keys).map((i) => i.message);
|
|
262
|
+
if (!issues.length) {
|
|
263
|
+
return { components: components, data: json };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
console.warn(`[step ${step.id}] no valid view:`, issues.join("; "));
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
if (ctx.signal.aborted) {
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
console.warn(`[step ${step.id}] view not composed:`, err.message);
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
/** An agent step runs this many times at most with `each`; more entries are left out. */
|
|
277
|
+
const MAX_AGENT_ENTRIES = 20;
|
|
278
|
+
/** The entries an agent step with `each` runs for: rows of a table, entries of a list, list rows. */
|
|
279
|
+
function agentEntries(step, ctx) {
|
|
280
|
+
const ref = dataRef(step.each ?? "");
|
|
281
|
+
const list = ref.match(/^lists\.([a-zA-Z][a-zA-Z0-9_]*)$/)?.[1];
|
|
282
|
+
const value = list
|
|
283
|
+
? (ctx.scope.lists?.[list]?.rows ?? []).map((r) => r.cells)
|
|
284
|
+
: resolveRef(ref, ctx.scope);
|
|
285
|
+
return (Array.isArray(value) ? value : []).slice(0, MAX_AGENT_ENTRIES);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* An agent step with `each`: the step once per entry, one after the other, each reading its
|
|
289
|
+
* entry as {{item}}. The results become one table — the entry's columns and the step's output
|
|
290
|
+
* fields — and one text with a heading per entry.
|
|
291
|
+
*/
|
|
292
|
+
async function runAgentEach(step, ctx) {
|
|
293
|
+
const entries = agentEntries(step, ctx);
|
|
294
|
+
if (!entries.length) {
|
|
295
|
+
throw new StepError("Es gibt keine Einträge, für die dieser Schritt arbeiten könnte.");
|
|
296
|
+
}
|
|
297
|
+
// The view, if any, is composed once over the table of all entries.
|
|
298
|
+
const single = {
|
|
299
|
+
...step,
|
|
300
|
+
each: undefined,
|
|
301
|
+
output: { ...step.output, surface: false },
|
|
302
|
+
};
|
|
303
|
+
const rows = [];
|
|
304
|
+
const texts = [];
|
|
305
|
+
const assets = [];
|
|
306
|
+
for (const [index, item] of entries.entries()) {
|
|
307
|
+
await ctx.emit("info", { code: "entry", params: { n: index + 1, total: entries.length } });
|
|
308
|
+
const scope = { ...ctx.scope, entry: { item, index, count: entries.length } };
|
|
309
|
+
const out = await runAgentStep(single, { ...ctx, scope });
|
|
310
|
+
const own = item && typeof item === "object" ? item : { item };
|
|
311
|
+
const json = out.json && typeof out.json === "object" ? out.json : {};
|
|
312
|
+
rows.push({ ...own, ...json });
|
|
313
|
+
const title = Object.values(own).find((v) => typeof v === "string") ?? `${index + 1}`;
|
|
314
|
+
texts.push(`## ${String(title)}\n\n${out.text ?? ""}`.trim());
|
|
315
|
+
assets.push(...(out.assets ?? []));
|
|
316
|
+
}
|
|
317
|
+
const json = { rows };
|
|
318
|
+
const surface = step.output.surface
|
|
319
|
+
? await composeSurface({
|
|
320
|
+
...step,
|
|
321
|
+
output: {
|
|
322
|
+
...step.output,
|
|
323
|
+
fields: [
|
|
324
|
+
{
|
|
325
|
+
id: "rows",
|
|
326
|
+
kind: "table",
|
|
327
|
+
description: "One row per entry: its columns and the step's fields",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
}, json, ctx)
|
|
332
|
+
: undefined;
|
|
333
|
+
return {
|
|
334
|
+
text: texts.join("\n\n"),
|
|
335
|
+
json,
|
|
336
|
+
...(surface ? { surface } : {}),
|
|
337
|
+
assets: assets.length ? assets : undefined,
|
|
338
|
+
at: new Date().toISOString(),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
151
341
|
export async function runAgentStep(step, ctx) {
|
|
342
|
+
if (step.each) {
|
|
343
|
+
return runAgentEach(step, ctx);
|
|
344
|
+
}
|
|
152
345
|
const resolved = await textModel(step.model ?? "high", { ...ctx.call, effort: step.effort });
|
|
153
346
|
const uploads = await personUploads(ctx);
|
|
154
347
|
const { tools, assets, close } = await buildStepTools(step, ctx, resolved, uploads);
|
|
@@ -192,22 +385,7 @@ export async function runAgentStep(step, ctx) {
|
|
|
192
385
|
.join("\n\n");
|
|
193
386
|
// A decision needs no agent: its questions go to the classifier class in one call.
|
|
194
387
|
if (isDecisionStep(step) && uploads.length === 0) {
|
|
195
|
-
|
|
196
|
-
const decision = await generateText({
|
|
197
|
-
model: resolved.model,
|
|
198
|
-
abortSignal: ctx.signal,
|
|
199
|
-
output: Output.object({ schema }),
|
|
200
|
-
prompt,
|
|
201
|
-
});
|
|
202
|
-
await ctx.chargeUsd(costOf(resolved, decision.usage));
|
|
203
|
-
const json = decision.output;
|
|
204
|
-
const text = fields
|
|
205
|
-
.map((f) => {
|
|
206
|
-
const value = json[f.id];
|
|
207
|
-
return `${f.description ?? f.id}: ${value === true ? "ja" : value === false ? "nein" : String(value)}`;
|
|
208
|
-
})
|
|
209
|
-
.join("\n");
|
|
210
|
-
return { text, json, at: new Date().toISOString() };
|
|
388
|
+
return decideStep(step, prompt, ctx);
|
|
211
389
|
}
|
|
212
390
|
const agent = new Agent({
|
|
213
391
|
id: `step-${step.id}`,
|
|
@@ -260,7 +438,14 @@ export async function runAgentStep(step, ctx) {
|
|
|
260
438
|
});
|
|
261
439
|
await ctx.chargeUsd(costOf(structurer, structured.usage));
|
|
262
440
|
const json = tablesToRecords(structured.output, fields);
|
|
263
|
-
|
|
441
|
+
const surface = step.output.surface ? await composeSurface(step, json, ctx) : undefined;
|
|
442
|
+
return {
|
|
443
|
+
text,
|
|
444
|
+
json,
|
|
445
|
+
...(surface ? { surface } : {}),
|
|
446
|
+
assets: assets.length ? assets : undefined,
|
|
447
|
+
at: new Date().toISOString(),
|
|
448
|
+
};
|
|
264
449
|
}
|
|
265
450
|
finally {
|
|
266
451
|
await close();
|
|
@@ -604,5 +789,8 @@ export async function runAutomaticStep(step, ctx) {
|
|
|
604
789
|
if (step.type === "film") {
|
|
605
790
|
return runFilmStep(step, ctx);
|
|
606
791
|
}
|
|
792
|
+
if (step.type === "surface") {
|
|
793
|
+
return runSurfaceStep(step, ctx);
|
|
794
|
+
}
|
|
607
795
|
throw new StepError(`Step ${step.id} is not automatic.`);
|
|
608
796
|
}
|