wizards 0.2.24 → 0.2.26
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/authoring/guide.js +55 -6
- package/apps/runtime/dist/connectors/builtin.js +17 -1
- package/apps/runtime/dist/connectors/imap.js +46 -2
- package/apps/runtime/dist/connectors/index.js +52 -18
- package/apps/runtime/dist/connectors/via-account.js +63 -0
- package/apps/runtime/dist/credits/estimate.js +12 -2
- package/apps/runtime/dist/engine/decide.js +82 -0
- package/apps/runtime/dist/engine/input.js +12 -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/render/downloads.js +7 -0
- package/apps/runtime/dist/routes/runs.js +18 -5
- package/apps/runtime/dist/services/relevance.js +17 -49
- package/apps/runtime/dist/tools/pages.js +31 -6
- package/apps/web/dist/assets/{Account-thPW7H4A.js → Account-Cnksw5Ea.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-mCszGL5_.js → ChangelogDialog-CdtQWq0m.js} +1 -1
- package/apps/web/dist/assets/{CreditsDialog-Com2Y3g8.js → CreditsDialog-Ti43boCN.js} +1 -1
- package/apps/web/dist/assets/EditorPage-N7EzlRSj.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-CRsxtwhV.js → FlowPreview-IZcNrBJi.js} +1 -1
- package/apps/web/dist/assets/{Harness-CfgVRYpr.js → Harness-Cj-k02TA.js} +1 -1
- package/apps/web/dist/assets/{HomePage-DYKF3C1U.js → HomePage-bjdKkt84.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-BTp7XTsF.js → ImportWizard-DagFXYPM.js} +1 -1
- package/apps/web/dist/assets/{LocalRuntime-CYhPXwAv.js → LocalRuntime-Boi_zqQJ.js} +1 -1
- package/apps/web/dist/assets/NewWizardPage-DO5J8OpZ.js +2 -0
- package/apps/web/dist/assets/ProjectPage-DdG9wYsd.js +2 -0
- package/apps/web/dist/assets/{PublicRunner-ClB2-2EF.js → PublicRunner-B4ts9rT6.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-LnVfCiYO.js +4 -0
- package/apps/web/dist/assets/RunnerView-DwffK7AA.js +70 -0
- package/apps/web/dist/assets/{SettingsPage-qDqiZ7th.js → SettingsPage-Cyx_jTlp.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-BeBReYLP.js → SetupPage-CBDD4rpC.js} +1 -1
- package/apps/web/dist/assets/{SharePage-Cv8toByB.js → SharePage-70niRKaO.js} +1 -1
- package/apps/web/dist/assets/{api-rJBQEu1G.js → api-C4M6cbpW.js} +1 -1
- package/apps/web/dist/assets/changelog-COyL1B5X.js +1 -0
- package/apps/web/dist/assets/definition-DoCLtZZL.js +55 -0
- package/apps/web/dist/assets/{esm-DFNBMDKB.js → esm-CfW8Rmtq.js} +1 -1
- package/apps/web/dist/assets/index-CJ0YJ5HC.css +1 -0
- package/apps/web/dist/assets/{index-CoTWrikR.js → index-DMKXnSf-.js} +3 -3
- package/apps/web/dist/assets/meta-6jZygxJq.js +1 -0
- package/apps/web/dist/assets/{settings-menu-bsLDpw56.js → settings-menu-Dph4yXbO.js} +1 -1
- package/apps/web/dist/assets/{table-2-BdDuBcF2.js → star-UHmZXpjA.js} +1 -1
- package/apps/web/dist/assets/ui-BS348L4M.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-IEd4jkyn.js +1 -0
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/mcp/flow.html +60 -60
- 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 +1048 -50
- package/node_modules/@engenty-wizards/shared/dist/definition.js +516 -43
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +19 -2
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +1 -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/EditorPage-Dwg-K4UI.js +0 -2
- package/apps/web/dist/assets/NewWizardPage-BgnrGN8A.js +0 -2
- package/apps/web/dist/assets/ProjectPage-Cfco8MIG.js +0 -5
- package/apps/web/dist/assets/RunDrawer-B50sCUSw.js +0 -1
- package/apps/web/dist/assets/RunnerView-CFCmOmJI.js +0 -70
- package/apps/web/dist/assets/changelog-BmS3l433.js +0 -1
- package/apps/web/dist/assets/definition-BznNdlhf.js +0 -55
- package/apps/web/dist/assets/index-BZs1IxAk.css +0 -1
- package/apps/web/dist/assets/meta-DJta2rA3.js +0 -1
- package/apps/web/dist/assets/ui-DJOZSSKo.js +0 -1
- package/apps/web/dist/assets/wand-sparkles-Clj3VRB5.js +0 -1
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { isDecisionStep, MODEL_CLASSES, ruleMatches, } from "@engenty-wizards/shared/definition";
|
|
1
|
+
import { conditionMatches, conditionsOf, isDecidedField, isDecisionStep, isOtherwise, MODEL_CLASSES, ruleMatches, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { classProblem, filmClient } from "../models.js";
|
|
3
3
|
/**
|
|
4
4
|
* What a wizard needs of the models before a run starts: each class its steps call, and whether
|
|
5
5
|
* this runtime can serve it. A step every run passes through blocks the start when its class is
|
|
6
6
|
* missing; a step behind a branch only warns, since the person may choose around it.
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The classes a step calls, as ../engine/steps.ts and ../media/generate.ts call them — and the
|
|
10
|
+
* classifier where a decision picks its next step or a page's fields.
|
|
11
|
+
*/
|
|
9
12
|
export function stepClasses(step) {
|
|
13
|
+
const own = ownClasses(step);
|
|
14
|
+
const decides = step.next?.some((r) => r.ask) || (step.type === "page" && step.fields.some(isDecidedField));
|
|
15
|
+
return decides && !own.includes("classifier") ? [...own, "classifier"] : own;
|
|
16
|
+
}
|
|
17
|
+
function ownClasses(step) {
|
|
10
18
|
switch (step.type) {
|
|
11
19
|
case "agent": {
|
|
12
20
|
if (isDecisionStep(step)) {
|
|
@@ -19,6 +27,9 @@ export function stepClasses(step) {
|
|
|
19
27
|
if (step.output.format === "json") {
|
|
20
28
|
classes.push("classifier");
|
|
21
29
|
}
|
|
30
|
+
if (step.output.surface && !classes.includes("standard")) {
|
|
31
|
+
classes.push("standard");
|
|
32
|
+
}
|
|
22
33
|
return classes;
|
|
23
34
|
}
|
|
24
35
|
case "generate":
|
|
@@ -64,8 +75,11 @@ export function unavoidableFrom(def, from, known) {
|
|
|
64
75
|
const successors = (i) => {
|
|
65
76
|
const out = [];
|
|
66
77
|
for (const rule of def.steps[i].next ?? []) {
|
|
78
|
+
if (isOtherwise(rule)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
67
81
|
const target = rule.goto === "end" ? def.steps.length : (index.get(rule.goto) ?? -1);
|
|
68
|
-
if (!(rule.when.field in known)) {
|
|
82
|
+
if (rule.ask || !conditionsOf(rule.when).every((c) => c.field in known)) {
|
|
69
83
|
if (target >= 0) {
|
|
70
84
|
out.push(target);
|
|
71
85
|
}
|
|
@@ -76,8 +90,8 @@ export function unavoidableFrom(def, from, known) {
|
|
|
76
90
|
return target >= 0 ? [...out, target] : out;
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
|
-
// No rule matching goes on to the
|
|
80
|
-
out.push(i
|
|
93
|
+
// No rule matching goes on to the "otherwise" step, else the next (or ends after the last).
|
|
94
|
+
out.push(otherwiseIndex(def, i, index));
|
|
81
95
|
return out;
|
|
82
96
|
};
|
|
83
97
|
const reachesEnd = (without) => {
|
|
@@ -102,11 +116,36 @@ export function unavoidableFrom(def, from, known) {
|
|
|
102
116
|
};
|
|
103
117
|
return new Set(def.steps.filter((_, i) => !reachesEnd(i)).map((s) => s.id));
|
|
104
118
|
}
|
|
119
|
+
/** Where step `i` goes when none of its rules holds, as an index; the length = the end. */
|
|
120
|
+
function otherwiseIndex(def, i, index) {
|
|
121
|
+
const goto = def.steps[i].next?.find(isOtherwise)?.goto;
|
|
122
|
+
if (goto === undefined) {
|
|
123
|
+
return i + 1;
|
|
124
|
+
}
|
|
125
|
+
return goto === "end" ? def.steps.length : (index.get(goto) ?? i + 1);
|
|
126
|
+
}
|
|
105
127
|
/** Whether a rule matches given what is known: true, false, or null = it may go either way. */
|
|
106
128
|
function ruleHolds(rule, facts) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
// A decision may go either way.
|
|
130
|
+
if (rule.ask) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
let all = true;
|
|
134
|
+
for (const c of conditionsOf(rule.when)) {
|
|
135
|
+
const holds = conditionHolds(c, facts);
|
|
136
|
+
if (holds === false) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (holds === null) {
|
|
140
|
+
all = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return all;
|
|
144
|
+
}
|
|
145
|
+
function conditionHolds(c, facts) {
|
|
146
|
+
const k = facts[c.field];
|
|
147
|
+
const value = c.value;
|
|
148
|
+
switch (c.op) {
|
|
110
149
|
case "empty":
|
|
111
150
|
return k?.filled === undefined ? (k?.is !== undefined ? false : null) : !k.filled;
|
|
112
151
|
case "notEmpty":
|
|
@@ -130,15 +169,35 @@ function ruleHolds(rule, facts) {
|
|
|
130
169
|
}
|
|
131
170
|
return list.every((v) => k?.not.includes(v)) ? false : null;
|
|
132
171
|
}
|
|
172
|
+
case "gt":
|
|
173
|
+
case "lt":
|
|
174
|
+
case "contains":
|
|
175
|
+
if (k?.is !== undefined) {
|
|
176
|
+
return conditionMatches(c, { [c.field]: k.is });
|
|
177
|
+
}
|
|
178
|
+
return k?.filled === false ? false : null;
|
|
133
179
|
}
|
|
134
180
|
}
|
|
135
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* What is known once a rule did (`held`) or did not match. A rule of several conditions that
|
|
183
|
+
* did not match says nothing about any one of them.
|
|
184
|
+
*/
|
|
136
185
|
function learn(rule, held, facts) {
|
|
137
|
-
|
|
186
|
+
if (rule.ask) {
|
|
187
|
+
return facts;
|
|
188
|
+
}
|
|
189
|
+
const conditions = conditionsOf(rule.when);
|
|
190
|
+
if (held) {
|
|
191
|
+
return conditions.reduce((known, c) => learnCondition(c, true, known), facts);
|
|
192
|
+
}
|
|
193
|
+
return conditions.length === 1 ? learnCondition(conditions[0], false, facts) : facts;
|
|
194
|
+
}
|
|
195
|
+
function learnCondition(c, held, facts) {
|
|
196
|
+
const field = c.field;
|
|
138
197
|
const k = { ...(facts[field] ?? { not: [] }) };
|
|
139
198
|
k.not = [...k.not];
|
|
140
|
-
const value = String(
|
|
141
|
-
const op =
|
|
199
|
+
const value = String(c.value);
|
|
200
|
+
const op = c.op;
|
|
142
201
|
if ((op === "equals" && held) || (op === "notEquals" && !held)) {
|
|
143
202
|
k.is = value;
|
|
144
203
|
k.filled = true;
|
|
@@ -146,12 +205,15 @@ function learn(rule, held, facts) {
|
|
|
146
205
|
else if ((op === "equals" && !held) || (op === "notEquals" && held)) {
|
|
147
206
|
k.not.push(value);
|
|
148
207
|
}
|
|
149
|
-
else if (op === "in" && !held && Array.isArray(
|
|
150
|
-
k.not.push(...
|
|
208
|
+
else if (op === "in" && !held && Array.isArray(c.value)) {
|
|
209
|
+
k.not.push(...c.value.map(String));
|
|
151
210
|
}
|
|
152
211
|
else if (op === "empty" || op === "notEmpty") {
|
|
153
212
|
k.filled = (op === "notEmpty") === held;
|
|
154
213
|
}
|
|
214
|
+
else if ((op === "gt" || op === "lt" || op === "contains") && held) {
|
|
215
|
+
k.filled = true;
|
|
216
|
+
}
|
|
155
217
|
return { ...facts, [field]: k };
|
|
156
218
|
}
|
|
157
219
|
/**
|
|
@@ -185,6 +247,9 @@ export function mostAlongOnePath(def, cost, closed = new Set()) {
|
|
|
185
247
|
let known = facts;
|
|
186
248
|
let decided = false;
|
|
187
249
|
for (const rule of step.next ?? []) {
|
|
250
|
+
if (isOtherwise(rule)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
188
253
|
const holds = ruleHolds(rule, known);
|
|
189
254
|
if (holds === false) {
|
|
190
255
|
continue;
|
|
@@ -197,7 +262,7 @@ export function mostAlongOnePath(def, cost, closed = new Set()) {
|
|
|
197
262
|
known = learn(rule, false, known);
|
|
198
263
|
}
|
|
199
264
|
if (!decided) {
|
|
200
|
-
consider(i
|
|
265
|
+
consider(otherwiseIndex(def, i, index), known);
|
|
201
266
|
}
|
|
202
267
|
return most === null ? null : most + cost(step);
|
|
203
268
|
};
|
|
@@ -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,
|