@krapht/ork 0.4.0 → 1.0.0-alpha.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/README.md +16 -12
- package/dist/{__build_info__-CtSVVuZE.mjs → __build_info__-DdTVd2p5.mjs} +36 -112
- package/dist/bin.mjs +663 -300
- package/dist/{clean-Dcr807be.mjs → clean-NnboOuz6.mjs} +1 -1
- package/dist/completion-snnYGjgK.mjs +405 -0
- package/dist/{context-C_bksb0C.mjs → context-DO3sBy9v.mjs} +11 -88
- package/dist/decide-B81wCZQg.mjs +113 -0
- package/dist/{find-DptmbhGd.mjs → find-BKW-uGey.mjs} +1 -1
- package/dist/{format-J-a21Yvn.mjs → format-CmTlTOjT.mjs} +71 -37
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
- package/dist/{limits-BDZCmGEu.mjs → limits-JCve2QpI.mjs} +1 -1
- package/dist/{list-ZvY2OcLq.mjs → list-C7UEgmtd.mjs} +1 -1
- package/dist/{mcp-CHVKKg3B.mjs → mcp-CKPyNaKD.mjs} +7 -7
- package/dist/{output-CXSJO00X.mjs → output-B1V6ZJSx.mjs} +67 -17
- package/dist/{pause-BPSmg4yz.mjs → pause-B_SL_yDz.mjs} +4 -4
- package/dist/{relay-Do-aNbSD.mjs → relay-o-Luee1c.mjs} +2 -2
- package/dist/{remove-AwaMMaHf.mjs → remove-DAZ7ZCzt.mjs} +3 -3
- package/dist/{send-DhGBkowy.mjs → send-3B64fBNB.mjs} +3 -3
- package/dist/{src-CpDKIulI.mjs → src-BJTegEzY.mjs} +23 -13
- package/dist/{src-UWkk1sl9.mjs → src-C38BPzit.mjs} +251 -18
- package/dist/{src-CGqMati3.mjs → src-DHvWU4tX.mjs} +636 -217
- package/dist/{src-BHnxpJyH.mjs → src-Dyz_Gy1q.mjs} +35 -8
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
- package/dist/ui/assets/app-CH7YC3in.css +1 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-MQNNgsyT.js → index-D83F2OKZ.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/{ui-DhutUMgQ.mjs → ui-CY4xxJUu.mjs} +5 -5
- package/dist/{usage-CT7Z-yjr.mjs → usage-CJnytoe4.mjs} +6 -6
- package/dist/{watch-0Qr2UfCQ.mjs → watch-BJB82ZJ7.mjs} +7 -7
- package/dist/workflows-CiM7S3l0.mjs +88 -0
- package/dist/{worktrees-DHl80k96.mjs → worktrees-86ySXxR6.mjs} +5 -5
- package/package.json +3 -3
- package/dist/ui/assets/app-BgVDC_5l.js +0 -67
- package/dist/ui/assets/app-DVf68fj5.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $n as timeoutMs, $t as parseTemplate, At as runScope, F as declaredProblem, Jn as beforeOf, Mt as envReadsOf, Ot as heldTimeout, Pt as declaredOutputOf, Qt as evaluate, Vn as markValue, Xn as isToolNode, Zn as programsRunBy, en as renderTemplate, er as INTEGRATIONS, ft as sessionDriver, jt as calledWorkflows, mt as reportSchemaFor, nr as integrationFor, s as planReason, tr as TOOL_PREFIX, ut as planNodeSession } from "./src-DHvWU4tX.mjs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -123,6 +123,196 @@ const resultOf = (outcome, declared) => {
|
|
|
123
123
|
if (!result.ok) result.error = report.summary;
|
|
124
124
|
return result;
|
|
125
125
|
};
|
|
126
|
+
/** The only built-in tools a fork has: it reads, and reports. */
|
|
127
|
+
const FORK_TOOLS = [
|
|
128
|
+
"Read",
|
|
129
|
+
"Grep",
|
|
130
|
+
"Glob"
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* The option that passes a question on: `ask-user`, or the first free
|
|
134
|
+
* `ask-user-<n>`.
|
|
135
|
+
*/
|
|
136
|
+
const passOnId = (options) => {
|
|
137
|
+
let id = "ask-user";
|
|
138
|
+
for (let n = 2; options.some((option) => option.id === id); n += 1) id = `ask-user-${String(n)}`;
|
|
139
|
+
return id;
|
|
140
|
+
};
|
|
141
|
+
const optionLine = (option) => [
|
|
142
|
+
`- \`${option.id}\``,
|
|
143
|
+
option.label === void 0 ? "" : `: ${option.label}`,
|
|
144
|
+
option.description === void 0 ? "" : ` (${option.description})`
|
|
145
|
+
].join("");
|
|
146
|
+
/** What the fork is asked. */
|
|
147
|
+
const forkPrompt = (question, passOn) => [
|
|
148
|
+
`[ork] \`${question.nodeId}\`, which you own, asks a question (ask_user). You take its questions before the user does.`,
|
|
149
|
+
"",
|
|
150
|
+
`Question: ${question.question}`,
|
|
151
|
+
...question.context === void 0 ? [] : [`Context: ${question.context}`],
|
|
152
|
+
"Options:",
|
|
153
|
+
...question.options.map(optionLine),
|
|
154
|
+
...question.recommended === void 0 ? [] : [`It recommends \`${question.recommended}\`.`],
|
|
155
|
+
"",
|
|
156
|
+
`If your task, and what was agreed with the user, already settle it, report that option as \`choice\`. If they don't, or it is the user's to decide, report \`${passOn}\` to pass it on to the user. Either way, say why in \`note\`, in a sentence or two: it goes to \`${question.nodeId}\` with your answer, or to the user with the question. This is a side conversation: change nothing, and your own session goes on as it was.`
|
|
157
|
+
].join("\n");
|
|
158
|
+
/** The fork's report: one of the options or the pass-on, and a note. */
|
|
159
|
+
const forkReportSchema = (ids) => {
|
|
160
|
+
const node = {
|
|
161
|
+
type: "claude.worker",
|
|
162
|
+
prompt: "",
|
|
163
|
+
output: { schema: {
|
|
164
|
+
choice: [...ids],
|
|
165
|
+
note: "string"
|
|
166
|
+
} }
|
|
167
|
+
};
|
|
168
|
+
return reportSchemaFor(node);
|
|
169
|
+
};
|
|
170
|
+
/** A fork can't ask anyone: it answers or passes the question on. */
|
|
171
|
+
const NO_ONE = {
|
|
172
|
+
permit: () => Promise.resolve({
|
|
173
|
+
allow: false,
|
|
174
|
+
message: "A fork answering a question can't ask for permission."
|
|
175
|
+
}),
|
|
176
|
+
ask: () => Promise.reject(/* @__PURE__ */ new Error("a fork can't ask the user"))
|
|
177
|
+
};
|
|
178
|
+
/** The fork's session spec, from the leader's own plan; `null` if it can't. */
|
|
179
|
+
const forkSpec = (context, owner, question, passOn, deps) => {
|
|
180
|
+
const session = owner.session();
|
|
181
|
+
const left = owner.budgetLeftUsd();
|
|
182
|
+
if (session === void 0 || left !== void 0 && left <= 0) return null;
|
|
183
|
+
const plan = planNodeSession(context.workflow, owner.id, {
|
|
184
|
+
cwd: session.cwd,
|
|
185
|
+
inputs: context.inputs,
|
|
186
|
+
outputs: context.outputs,
|
|
187
|
+
inPlace: deps.inPlace,
|
|
188
|
+
workflowDir: context.workflowDir,
|
|
189
|
+
projectRoot: context.projectRoot,
|
|
190
|
+
...context.orkDir === void 0 ? {} : { orkDir: context.orkDir },
|
|
191
|
+
...left === void 0 ? {} : { budgetLeftUsd: left }
|
|
192
|
+
});
|
|
193
|
+
if (!plan.ok) return null;
|
|
194
|
+
const driver = session.driver ?? sessionDriver({
|
|
195
|
+
override: deps.driver,
|
|
196
|
+
named: plan.driver,
|
|
197
|
+
ready: deps.readyDriver
|
|
198
|
+
});
|
|
199
|
+
if (driver === void 0) return null;
|
|
200
|
+
const ids = [...question.options.map((option) => option.id), passOn];
|
|
201
|
+
const spec = {
|
|
202
|
+
...plan.spec,
|
|
203
|
+
cwd: session.cwd,
|
|
204
|
+
prompt: forkPrompt(question, passOn),
|
|
205
|
+
resume: session.sessionId,
|
|
206
|
+
fork: true,
|
|
207
|
+
maxTurns: 3,
|
|
208
|
+
tools: {
|
|
209
|
+
allow: [...FORK_TOOLS],
|
|
210
|
+
deny: [...plan.spec.tools.deny, "mcp__ork__ask_user"],
|
|
211
|
+
only: [...FORK_TOOLS],
|
|
212
|
+
strictMcp: true
|
|
213
|
+
},
|
|
214
|
+
reportSchema: forkReportSchema(ids)
|
|
215
|
+
};
|
|
216
|
+
delete spec.promptCache;
|
|
217
|
+
delete spec.agents;
|
|
218
|
+
delete spec.plugins;
|
|
219
|
+
return {
|
|
220
|
+
driver,
|
|
221
|
+
spec
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
/** The choice a fork's outcome gives, if it is a valid one. */
|
|
225
|
+
const answerOf = (outcome, ids) => {
|
|
226
|
+
const { choice, note } = outcome?.report?.fields ?? {};
|
|
227
|
+
if (outcome?.subtype !== "success" || typeof choice !== "string") return null;
|
|
228
|
+
if (!ids.includes(choice)) return null;
|
|
229
|
+
return typeof note === "string" && note.trim() !== "" ? {
|
|
230
|
+
choice,
|
|
231
|
+
note: note.trim()
|
|
232
|
+
} : { choice };
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Asks a fork of the owner's latest session; `null` when it can't answer: no
|
|
236
|
+
* session yet, nothing left to spend, the node stopped, or a fork that fails,
|
|
237
|
+
* runs out of time or gives no valid choice. A paused run or node starts no
|
|
238
|
+
* fork until it goes on; one already started isn't paused, as it is bounded by
|
|
239
|
+
* its turns and `timeoutMs`. Its events are the owner's, each marked as a fork,
|
|
240
|
+
* and its spend the run's.
|
|
241
|
+
*/
|
|
242
|
+
const askFork = async (context, owner, question, deps) => {
|
|
243
|
+
const passOn = passOnId(question.options);
|
|
244
|
+
await context.hold?.wait("start", context.signal);
|
|
245
|
+
if (context.signal.aborted) return null;
|
|
246
|
+
const planned = forkSpec(context, owner, question, passOn, deps);
|
|
247
|
+
if (planned === null) return null;
|
|
248
|
+
const signal = AbortSignal.any([context.signal, AbortSignal.timeout(deps.timeoutMs ?? 3e5)]);
|
|
249
|
+
let outcome = null;
|
|
250
|
+
try {
|
|
251
|
+
const session = (await deps.runtimeFor(planned.driver)).start(planned.spec, { human: NO_ONE });
|
|
252
|
+
const stop = () => {
|
|
253
|
+
session.interrupt();
|
|
254
|
+
};
|
|
255
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
256
|
+
if (signal.aborted) stop();
|
|
257
|
+
try {
|
|
258
|
+
for await (const event of session.events) {
|
|
259
|
+
if (event.type === "session.finished") {
|
|
260
|
+
({outcome} = event);
|
|
261
|
+
owner.charge(outcome.sessionId, outcome.usage);
|
|
262
|
+
}
|
|
263
|
+
owner.emit({
|
|
264
|
+
...event,
|
|
265
|
+
fork: true
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
} finally {
|
|
269
|
+
signal.removeEventListener("abort", stop);
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (signal.aborted) return null;
|
|
275
|
+
return answerOf(outcome, [...question.options.map((option) => option.id), passOn]);
|
|
276
|
+
};
|
|
277
|
+
/** What the person sees of a question its asker's owner passed on. */
|
|
278
|
+
const passedOnContext = (context, owner, note) => [...context === void 0 ? [] : [context, ""], note === void 0 ? `${owner} passed this on.` : `${owner} passed this on: ${note}`].join("\n");
|
|
279
|
+
/**
|
|
280
|
+
* The child's channel when its owner takes its questions first: a question goes
|
|
281
|
+
* to a fork of the owner's session, then to the person if the fork passes it on
|
|
282
|
+
* or can't answer. Permission prompts go to the person as before.
|
|
283
|
+
*/
|
|
284
|
+
const ownerFirst = (context, owner, deps) => {
|
|
285
|
+
const { human } = context;
|
|
286
|
+
return {
|
|
287
|
+
permit: (request) => human.permit(request),
|
|
288
|
+
ask: async (question) => {
|
|
289
|
+
const answered = await askFork(context, owner, question, deps);
|
|
290
|
+
if (context.signal.aborted) throw new Error(`${context.id} was stopped`);
|
|
291
|
+
const signal = question.signal === void 0 ? context.signal : AbortSignal.any([question.signal, context.signal]);
|
|
292
|
+
if (answered === null) return human.ask({
|
|
293
|
+
...question,
|
|
294
|
+
signal
|
|
295
|
+
});
|
|
296
|
+
if (answered.choice === passOnId(question.options)) return human.ask({
|
|
297
|
+
...question,
|
|
298
|
+
signal,
|
|
299
|
+
owner: owner.id,
|
|
300
|
+
context: passedOnContext(question.context, owner.id, answered.note)
|
|
301
|
+
});
|
|
302
|
+
const { note } = answered;
|
|
303
|
+
human.settled?.({
|
|
304
|
+
...question,
|
|
305
|
+
answer: answered.choice,
|
|
306
|
+
owner: owner.id,
|
|
307
|
+
...note === void 0 ? {} : { note }
|
|
308
|
+
});
|
|
309
|
+
question.onOwner?.(owner.id);
|
|
310
|
+
if (note !== void 0) question.onNote?.(note);
|
|
311
|
+
return answered.choice;
|
|
312
|
+
},
|
|
313
|
+
...human.settled === void 0 ? {} : { settled: human.settled }
|
|
314
|
+
};
|
|
315
|
+
};
|
|
126
316
|
// ../nodes/src/session-turns.ts
|
|
127
317
|
/**
|
|
128
318
|
* @file One session over several processes: the outcome after another turn, and
|
|
@@ -235,7 +425,8 @@ const notStarted = (reason, limited) => ({
|
|
|
235
425
|
* result that cannot be read fails the node rather than turning into an empty
|
|
236
426
|
* value, and one too large to keep in an event is never cut. What a program
|
|
237
427
|
* wrote is not yet scrubbed, so these files live in a private folder that a
|
|
238
|
-
* run clears when it starts and ends.
|
|
428
|
+
* run clears when it starts and ends. A long field is kept in the run's
|
|
429
|
+
* files, named for the node, its round, and its step and try.
|
|
239
430
|
*/
|
|
240
431
|
/** The run's private folder for results not yet read. */
|
|
241
432
|
const OUTPUT_FOLDER = ".output";
|
|
@@ -278,8 +469,14 @@ const prepareOutputFile = (runDir, id, round) => {
|
|
|
278
469
|
removeOutputFile(file);
|
|
279
470
|
return file;
|
|
280
471
|
};
|
|
472
|
+
/**
|
|
473
|
+
* A node's activation, and the step and its try when it is one, as a file
|
|
474
|
+
* name's start: `review.1`, `review.1.before-1`, `review.1.verify-2`. Two
|
|
475
|
+
* steps, or two tries of a check, never share a file.
|
|
476
|
+
*/
|
|
477
|
+
const keptStem = ({ id, round, step }) => `${id}.${String(round)}${step === void 0 ? "" : `.${step.name}-${String(step.try)}`}`;
|
|
281
478
|
/** Where a long field of a node's output is kept. */
|
|
282
|
-
const keptFile = (context, field) => path.join(context.runDir, `${
|
|
479
|
+
const keptFile = (context, field) => path.join(context.runDir, `${keptStem(context)}.${field}.txt`);
|
|
283
480
|
/** A way to write a field of a node's output to the run's files, when it's long. */
|
|
284
481
|
const keepFile = (context, field, text) => () => {
|
|
285
482
|
mkdirSync(context.runDir, { recursive: true });
|
|
@@ -1168,11 +1365,15 @@ const TAIL_LIMIT = 8192;
|
|
|
1168
1365
|
*/
|
|
1169
1366
|
const isAcceptance = (outcome) => outcome.report !== null && outcome.report.status !== "fail" && outcome.report.status !== "changes_requested";
|
|
1170
1367
|
/** Runs a step as a one-off tool node, in the node's checkout, as the node. */
|
|
1171
|
-
const runStep = async (tool,
|
|
1368
|
+
const runStep = async (tool, given, { timeout, ...fields }, step) => {
|
|
1172
1369
|
const node = {
|
|
1173
1370
|
type: "tool.shell",
|
|
1174
1371
|
...fields
|
|
1175
1372
|
};
|
|
1373
|
+
const context = {
|
|
1374
|
+
...given,
|
|
1375
|
+
step
|
|
1376
|
+
};
|
|
1176
1377
|
const ms = timeoutMs(timeout);
|
|
1177
1378
|
if (ms === void 0) return tool({
|
|
1178
1379
|
...context,
|
|
@@ -1225,7 +1426,7 @@ const tailOf = (check, context) => {
|
|
|
1225
1426
|
if (said === "") return "It printed nothing.";
|
|
1226
1427
|
if (said.length <= TAIL_LIMIT) return `\`\`\`\n${said}\n\`\`\``;
|
|
1227
1428
|
mkdirSync(context.runDir, { recursive: true });
|
|
1228
|
-
const file = path.join(context.runDir, `${
|
|
1429
|
+
const file = path.join(context.runDir, `${keptStem(context)}.txt`);
|
|
1229
1430
|
writeFileSync(file, said);
|
|
1230
1431
|
return `The end of what it printed, with all of it in ${file}:\n\n\`\`\`\n${said.slice(-8192)}\n\`\`\``;
|
|
1231
1432
|
};
|
|
@@ -1362,7 +1563,10 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1362
1563
|
if (context.claims !== void 0) planOptions.claims = context.claims;
|
|
1363
1564
|
const before = beforeOf(context.node, context.workflow.defaults);
|
|
1364
1565
|
if (before !== void 0) {
|
|
1365
|
-
const ran = await runStep(options.steps ?? noSteps, context, before
|
|
1566
|
+
const ran = await runStep(options.steps ?? noSteps, context, before, {
|
|
1567
|
+
name: "before",
|
|
1568
|
+
try: 1
|
|
1569
|
+
});
|
|
1366
1570
|
if (!ran.ok) return beforeFailed(ran);
|
|
1367
1571
|
await context.hold?.wait("start", context.signal);
|
|
1368
1572
|
if (context.signal.aborted) return beforeFailed({
|
|
@@ -1386,8 +1590,19 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1386
1590
|
error: plan.error
|
|
1387
1591
|
};
|
|
1388
1592
|
const spec = specFor(plan.spec, context);
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1593
|
+
const driver = sessionDriver({
|
|
1594
|
+
override: options.driver,
|
|
1595
|
+
named: plan.driver,
|
|
1596
|
+
ready: options.readyDriver
|
|
1597
|
+
});
|
|
1598
|
+
if (driver === void 0) return notStarted("no Claude driver is ready; see `ork doctor`");
|
|
1599
|
+
const runtime = await options.runtimeFor(driver);
|
|
1600
|
+
const ports = { human: context.owner === void 0 ? context.human : ownerFirst(context, context.owner, {
|
|
1601
|
+
runtimeFor: options.runtimeFor,
|
|
1602
|
+
driver: options.driver,
|
|
1603
|
+
readyDriver: options.readyDriver,
|
|
1604
|
+
inPlace: options.inPlace
|
|
1605
|
+
}) };
|
|
1391
1606
|
if (context.hold !== void 0) ports.hold = context.hold;
|
|
1392
1607
|
if (context.ownership !== void 0) ports.ownership = context.ownership;
|
|
1393
1608
|
if (context.mailbox !== void 0) ports.mailbox = context.mailbox;
|
|
@@ -1447,26 +1662,38 @@ const claudeExecutor = (options) => async (context) => {
|
|
|
1447
1662
|
const { node } = context;
|
|
1448
1663
|
const verify = "verify" in node ? node.verify : void 0;
|
|
1449
1664
|
if (outcome === null || verify === void 0 || !isAcceptance(outcome)) return watch.result(resultOf(outcome, declaredOutputOf(node)));
|
|
1665
|
+
let tries = 0;
|
|
1666
|
+
const step = () => ({
|
|
1667
|
+
name: "verify",
|
|
1668
|
+
try: tries
|
|
1669
|
+
});
|
|
1450
1670
|
const checked = await checkReport({
|
|
1451
1671
|
verify,
|
|
1452
1672
|
first: outcome,
|
|
1453
1673
|
signal: context.signal,
|
|
1454
|
-
check: () =>
|
|
1674
|
+
check: () => {
|
|
1675
|
+
tries += 1;
|
|
1676
|
+
return runStep(options.steps ?? noSteps, context, verify, step());
|
|
1677
|
+
},
|
|
1455
1678
|
late: lateTurn,
|
|
1456
1679
|
resume: async (current, check) => {
|
|
1457
1680
|
const limits = limitsAfter(current);
|
|
1458
1681
|
if (limits === null) return null;
|
|
1459
1682
|
await context.hold?.wait("start", context.signal);
|
|
1460
|
-
return context.signal.aborted ? null : resumeWith(current, checkFailedMessage(check,
|
|
1683
|
+
return context.signal.aborted ? null : resumeWith(current, checkFailedMessage(check, {
|
|
1684
|
+
...context,
|
|
1685
|
+
step: step()
|
|
1686
|
+
}), limits);
|
|
1461
1687
|
}
|
|
1462
1688
|
});
|
|
1463
1689
|
return watch.result(checkedResult(resultOf(checked.outcome, declaredOutputOf(node)), checked));
|
|
1464
1690
|
};
|
|
1465
1691
|
// ../nodes/src/gate.ts
|
|
1466
1692
|
/**
|
|
1467
|
-
* @file The `human.gate` node type: asks the human the rendered question,
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1693
|
+
* @file The `human.gate` node type: asks the human the rendered question,
|
|
1694
|
+
* outputs their choice with the note they gave, and answers with the
|
|
1695
|
+
* `default` when the `timeout` passes. The timeout counts only while the run
|
|
1696
|
+
* and the gate are not paused (ADR-041).
|
|
1470
1697
|
*/
|
|
1471
1698
|
/** A promise that never settles: the losing side of a race. */
|
|
1472
1699
|
const NEVER = new Promise(() => {});
|
|
@@ -1478,8 +1705,8 @@ const choiceOf = (option) => {
|
|
|
1478
1705
|
return choice;
|
|
1479
1706
|
};
|
|
1480
1707
|
/**
|
|
1481
|
-
* Asks the human and outputs `choice` and `
|
|
1482
|
-
* `default` answers when no one has.
|
|
1708
|
+
* Asks the human and outputs `choice`, `answeredBy` and `note` (empty when they
|
|
1709
|
+
* said nothing). With `timeout`, the `default` answers when no one has.
|
|
1483
1710
|
*/
|
|
1484
1711
|
const gateExecutor = async (context) => {
|
|
1485
1712
|
const { node } = context;
|
|
@@ -1495,16 +1722,21 @@ const gateExecutor = async (context) => {
|
|
|
1495
1722
|
context.signal.addEventListener("abort", () => {
|
|
1496
1723
|
withdraw.abort();
|
|
1497
1724
|
});
|
|
1725
|
+
let note = "";
|
|
1498
1726
|
const asked = {
|
|
1499
1727
|
nodeId: context.id,
|
|
1500
1728
|
question,
|
|
1501
1729
|
options,
|
|
1502
|
-
signal: withdraw.signal
|
|
1730
|
+
signal: withdraw.signal,
|
|
1731
|
+
onNote: (given) => {
|
|
1732
|
+
note = given;
|
|
1733
|
+
}
|
|
1503
1734
|
};
|
|
1504
1735
|
if (node.recommended !== void 0) asked.recommended = node.recommended;
|
|
1505
1736
|
const answer = context.human.ask(asked).then((choice) => ({
|
|
1506
1737
|
choice,
|
|
1507
|
-
answeredBy: "user"
|
|
1738
|
+
answeredBy: "user",
|
|
1739
|
+
note
|
|
1508
1740
|
}));
|
|
1509
1741
|
const ms = timeoutMs(node.timeout);
|
|
1510
1742
|
const answered = new AbortController();
|
|
@@ -1513,7 +1745,8 @@ const gateExecutor = async (context) => {
|
|
|
1513
1745
|
withdraw.abort();
|
|
1514
1746
|
return {
|
|
1515
1747
|
choice: node.default ?? "",
|
|
1516
|
-
answeredBy: "timeout"
|
|
1748
|
+
answeredBy: "timeout",
|
|
1749
|
+
note: ""
|
|
1517
1750
|
};
|
|
1518
1751
|
})];
|
|
1519
1752
|
try {
|