@moxxy/cli 0.7.3 → 0.8.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/LICENSE +21 -0
- package/dist/bin.js +362 -20
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bin.js
CHANGED
|
@@ -88109,6 +88109,8 @@ var init_WorkflowsPanel = __esm({
|
|
|
88109
88109
|
const [loading, setLoading] = import_react55.default.useState(true);
|
|
88110
88110
|
const [busy, setBusy] = import_react55.default.useState(false);
|
|
88111
88111
|
const [status, setStatus] = import_react55.default.useState(null);
|
|
88112
|
+
const [pending, setPending] = import_react55.default.useState(null);
|
|
88113
|
+
const [reply2, setReply] = import_react55.default.useState("");
|
|
88112
88114
|
const reload = import_react55.default.useCallback(async () => {
|
|
88113
88115
|
if (!view) {
|
|
88114
88116
|
setLoading(false);
|
|
@@ -88126,7 +88128,7 @@ var init_WorkflowsPanel = __esm({
|
|
|
88126
88128
|
import_react55.default.useEffect(() => {
|
|
88127
88129
|
void reload();
|
|
88128
88130
|
}, [reload]);
|
|
88129
|
-
const active = !busy && !!view && rows.length > 0;
|
|
88131
|
+
const active = !busy && !pending && !!view && rows.length > 0;
|
|
88130
88132
|
const run2 = import_react55.default.useCallback(async (wf) => {
|
|
88131
88133
|
if (!view || busy)
|
|
88132
88134
|
return;
|
|
@@ -88138,6 +88140,17 @@ var init_WorkflowsPanel = __esm({
|
|
|
88138
88140
|
setStatus(`running "${wf.name}"\u2026`);
|
|
88139
88141
|
try {
|
|
88140
88142
|
const result = await view.run(wf.name);
|
|
88143
|
+
if (result.status === "paused" && result.runId) {
|
|
88144
|
+
const askStep = result.steps.find((s2) => s2.status === "awaiting_input");
|
|
88145
|
+
setPending({
|
|
88146
|
+
runId: result.runId,
|
|
88147
|
+
label: askStep?.id ?? wf.name,
|
|
88148
|
+
prompt: result.output || "The workflow is waiting for your input."
|
|
88149
|
+
});
|
|
88150
|
+
setReply("");
|
|
88151
|
+
setStatus(`\u23F8 "${wf.name}" is waiting for your reply \u2014 type it and press Enter (Esc cancels).`);
|
|
88152
|
+
return;
|
|
88153
|
+
}
|
|
88141
88154
|
const marks = result.steps.map((s2) => `${stepMark(s2.status)}${s2.id}`).join(" ");
|
|
88142
88155
|
setStatus(result.ok ? `\u2713 ${wf.name} completed \u2014 ${marks}` : `\u2717 ${wf.name} failed: ${result.error ?? ""} \u2014 ${marks}`);
|
|
88143
88156
|
} catch (err) {
|
|
@@ -88147,6 +88160,35 @@ var init_WorkflowsPanel = __esm({
|
|
|
88147
88160
|
void reload();
|
|
88148
88161
|
}
|
|
88149
88162
|
}, [view, busy, reload]);
|
|
88163
|
+
const submitReply = import_react55.default.useCallback(async (pendingRun, text) => {
|
|
88164
|
+
if (!view?.resume) {
|
|
88165
|
+
setStatus("resume not supported by this session");
|
|
88166
|
+
return;
|
|
88167
|
+
}
|
|
88168
|
+
const answer = text.trim();
|
|
88169
|
+
if (!answer)
|
|
88170
|
+
return;
|
|
88171
|
+
setPending(null);
|
|
88172
|
+
setReply("");
|
|
88173
|
+
setBusy(true);
|
|
88174
|
+
setStatus(`resuming "${pendingRun.label}"\u2026`);
|
|
88175
|
+
try {
|
|
88176
|
+
const result = await view.resume(pendingRun.runId, answer);
|
|
88177
|
+
if (result.status === "paused" && result.runId) {
|
|
88178
|
+
const askStep = result.steps.find((s2) => s2.status === "awaiting_input");
|
|
88179
|
+
setPending({ runId: result.runId, label: askStep?.id ?? pendingRun.label, prompt: result.output });
|
|
88180
|
+
setStatus("\u23F8 waiting again \u2014 type your reply and press Enter (Esc cancels).");
|
|
88181
|
+
} else {
|
|
88182
|
+
const marks = result.steps.map((s2) => `${stepMark(s2.status)}${s2.id}`).join(" ");
|
|
88183
|
+
setStatus(result.ok ? `\u2713 resumed & completed \u2014 ${marks}` : `\u2717 resume failed: ${result.error ?? ""} \u2014 ${marks}`);
|
|
88184
|
+
}
|
|
88185
|
+
} catch (err) {
|
|
88186
|
+
setStatus(`\u2717 resume errored: ${err instanceof Error ? err.message : String(err)}`);
|
|
88187
|
+
} finally {
|
|
88188
|
+
setBusy(false);
|
|
88189
|
+
void reload();
|
|
88190
|
+
}
|
|
88191
|
+
}, [view, reload]);
|
|
88150
88192
|
const scroll = useScrollableList({
|
|
88151
88193
|
total: rows.length,
|
|
88152
88194
|
windowSize: WINDOW4,
|
|
@@ -88172,6 +88214,26 @@ var init_WorkflowsPanel = __esm({
|
|
|
88172
88214
|
void run2(wf);
|
|
88173
88215
|
}
|
|
88174
88216
|
}, { isActive: active });
|
|
88217
|
+
use_input_default((input, key) => {
|
|
88218
|
+
if (!pending)
|
|
88219
|
+
return;
|
|
88220
|
+
if (key.escape) {
|
|
88221
|
+
setPending(null);
|
|
88222
|
+
setReply("");
|
|
88223
|
+
setStatus("reply cancelled \u2014 the run stays paused (re-run to resume).");
|
|
88224
|
+
return;
|
|
88225
|
+
}
|
|
88226
|
+
if (key.return) {
|
|
88227
|
+
void submitReply(pending, reply2);
|
|
88228
|
+
return;
|
|
88229
|
+
}
|
|
88230
|
+
if (key.backspace || key.delete) {
|
|
88231
|
+
setReply((r2) => r2.slice(0, -1));
|
|
88232
|
+
return;
|
|
88233
|
+
}
|
|
88234
|
+
if (input && !key.ctrl && !key.meta)
|
|
88235
|
+
setReply((r2) => r2 + input);
|
|
88236
|
+
}, { isActive: !busy && !!pending });
|
|
88175
88237
|
const termWidth = process.stdout.columns ?? 80;
|
|
88176
88238
|
const descWidth = Math.max(16, termWidth - NAME_COL3 - SCOPE_COL2 - TRIG_COL - 12);
|
|
88177
88239
|
const slice = rows.slice(scroll.visible.start, scroll.visible.end);
|
|
@@ -88181,7 +88243,7 @@ var init_WorkflowsPanel = __esm({
|
|
|
88181
88243
|
const absoluteIndex = scroll.visible.start + i2;
|
|
88182
88244
|
const focused = absoluteIndex === scroll.cursor;
|
|
88183
88245
|
return (0, import_jsx_runtime27.jsxs)(Box_default, { children: [(0, import_jsx_runtime27.jsx)(Text, { ...focused ? {} : { dimColor: true }, children: focused ? "\u203A " : " " }), (0, import_jsx_runtime27.jsx)(Text, { color: wf.enabled ? Colors.active : void 0, dimColor: !wf.enabled, children: wf.enabled ? "\u25CF " : "\u25CB " }), (0, import_jsx_runtime27.jsx)(Box_default, { width: NAME_COL3, children: (0, import_jsx_runtime27.jsx)(Text, { bold: focused, children: truncate8(wf.name, NAME_COL3 - 1) }) }), (0, import_jsx_runtime27.jsx)(Box_default, { width: SCOPE_COL2, children: (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: wf.scope }) }), (0, import_jsx_runtime27.jsx)(Box_default, { width: TRIG_COL, children: (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, wrap: "truncate", children: wf.triggers }) }), (0, import_jsx_runtime27.jsx)(Box_default, { width: descWidth, children: (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, wrap: "truncate", children: oneLine5(wf.description) }) })] }, wf.name);
|
|
88184
|
-
}), scroll.canScrollDown ? (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: ` \u2193 ${rows.length - scroll.visible.end} more below` }) : null, status ? (0, import_jsx_runtime27.jsx)(Box_default, { marginTop: 1, children: (0, import_jsx_runtime27.jsx)(Text, { wrap: "truncate-end", children: status }) }) : null] });
|
|
88246
|
+
}), scroll.canScrollDown ? (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: ` \u2193 ${rows.length - scroll.visible.end} more below` }) : null, pending ? (0, import_jsx_runtime27.jsxs)(Box_default, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: Colors.active, paddingX: 1, children: [(0, import_jsx_runtime27.jsxs)(Text, { color: Colors.active, children: ["\u23F8 Workflow waiting \xB7 ", pending.label] }), pending.prompt ? (0, import_jsx_runtime27.jsx)(Text, { wrap: "wrap", children: oneLine5(pending.prompt).slice(0, 280) }) : null, (0, import_jsx_runtime27.jsxs)(Box_default, { marginTop: 1, children: [(0, import_jsx_runtime27.jsx)(Text, { children: "reply \u203A " }), (0, import_jsx_runtime27.jsx)(Text, { children: reply2 || " " })] }), (0, import_jsx_runtime27.jsx)(Text, { dimColor: true, children: "Enter send \xB7 Esc cancel" })] }) : null, status ? (0, import_jsx_runtime27.jsx)(Box_default, { marginTop: 1, children: (0, import_jsx_runtime27.jsx)(Text, { wrap: "truncate-end", children: status }) }) : null] });
|
|
88185
88247
|
};
|
|
88186
88248
|
}
|
|
88187
88249
|
});
|
|
@@ -134642,7 +134704,8 @@ function isRunnerUp(socketPath = runnerSocketPath()) {
|
|
|
134642
134704
|
|
|
134643
134705
|
// ../runner/dist/server.js
|
|
134644
134706
|
init_dist();
|
|
134645
|
-
var RUNNER_PROTOCOL_VERSION =
|
|
134707
|
+
var RUNNER_PROTOCOL_VERSION = 5;
|
|
134708
|
+
var MIN_COMPATIBLE_PROTOCOL_VERSION = 1;
|
|
134646
134709
|
var RunnerMethod = {
|
|
134647
134710
|
/** client->server: handshake; returns the initial info snapshot. */
|
|
134648
134711
|
Attach: "attach",
|
|
@@ -134692,6 +134755,12 @@ var RunnerMethod = {
|
|
|
134692
134755
|
WorkflowSave: "workflow.save",
|
|
134693
134756
|
/** client->server: fetch one saved workflow as canonical YAML (builder). */
|
|
134694
134757
|
WorkflowGetRun: "workflow.getRun",
|
|
134758
|
+
/**
|
|
134759
|
+
* client->server: answer a paused workflow's `awaitInput` question and resume
|
|
134760
|
+
* the run (human-in-the-loop). v5 — the client gates this on the server's
|
|
134761
|
+
* reported version so an older runner returns an actionable error.
|
|
134762
|
+
*/
|
|
134763
|
+
WorkflowResume: "workflow.resume",
|
|
134695
134764
|
/** server->client: ask this client to decide a tool-call permission. */
|
|
134696
134765
|
PermissionCheck: "permission.check",
|
|
134697
134766
|
/** server->client: ask this client to confirm an approval checkpoint. */
|
|
@@ -134776,6 +134845,10 @@ var workflowSaveParamsSchema = z.object({
|
|
|
134776
134845
|
previousName: z.string().min(1).max(120).optional()
|
|
134777
134846
|
});
|
|
134778
134847
|
var workflowGetRunParamsSchema = z.object({ name: z.string().min(1).max(120) });
|
|
134848
|
+
var workflowResumeParamsSchema = z.object({
|
|
134849
|
+
runId: z.string().min(1).max(120),
|
|
134850
|
+
reply: z.string().min(1).max(1e5)
|
|
134851
|
+
});
|
|
134779
134852
|
|
|
134780
134853
|
// ../runner/dist/server.js
|
|
134781
134854
|
var RunnerServer = class {
|
|
@@ -134855,6 +134928,7 @@ var RunnerServer = class {
|
|
|
134855
134928
|
peer.handle(RunnerMethod.WorkflowValidateDraft, (raw) => this.handleWorkflowValidateDraft(raw));
|
|
134856
134929
|
peer.handle(RunnerMethod.WorkflowSave, (raw) => this.handleWorkflowSave(raw));
|
|
134857
134930
|
peer.handle(RunnerMethod.WorkflowGetRun, (raw) => this.handleWorkflowGetRun(raw));
|
|
134931
|
+
peer.handle(RunnerMethod.WorkflowResume, (raw) => this.handleWorkflowResume(raw));
|
|
134858
134932
|
peer.onClose(() => this.onDisconnect(client));
|
|
134859
134933
|
}
|
|
134860
134934
|
onDisconnect(client) {
|
|
@@ -134866,7 +134940,7 @@ var RunnerServer = class {
|
|
|
134866
134940
|
// --- request handlers ----------------------------------------------------
|
|
134867
134941
|
handleAttach(client, raw) {
|
|
134868
134942
|
const params = attachParamsSchema.parse(raw);
|
|
134869
|
-
if (params.protocolVersion
|
|
134943
|
+
if (params.protocolVersion < MIN_COMPATIBLE_PROTOCOL_VERSION) {
|
|
134870
134944
|
throw new Error(`runner protocol mismatch: server v${RUNNER_PROTOCOL_VERSION}, client v${params.protocolVersion}`);
|
|
134871
134945
|
}
|
|
134872
134946
|
client.role = params.role;
|
|
@@ -135109,6 +135183,16 @@ var RunnerServer = class {
|
|
|
135109
135183
|
throw new Error("workflows builder not supported on this runner");
|
|
135110
135184
|
return await view.getRun(params.name) ?? null;
|
|
135111
135185
|
}
|
|
135186
|
+
// --- Workflows human-in-the-loop (resume a paused awaitInput run) ---------
|
|
135187
|
+
// v5. Optional on the view (older hosts lack it), so feature-check and throw a
|
|
135188
|
+
// clear error rather than calling undefined.
|
|
135189
|
+
async handleWorkflowResume(raw) {
|
|
135190
|
+
const params = workflowResumeParamsSchema.parse(raw);
|
|
135191
|
+
const view = this.session.workflows;
|
|
135192
|
+
if (!view?.resume)
|
|
135193
|
+
throw new Error("workflow resume not supported on this runner");
|
|
135194
|
+
return view.resume(params.runId, params.reply);
|
|
135195
|
+
}
|
|
135112
135196
|
broadcastInfo() {
|
|
135113
135197
|
this.broadcast(RunnerNotification.InfoChanged, { info: this.session.getInfo() });
|
|
135114
135198
|
}
|
|
@@ -135251,6 +135335,14 @@ var RemoteSession = class {
|
|
|
135251
135335
|
permissionResolver = null;
|
|
135252
135336
|
approvalResolver = null;
|
|
135253
135337
|
info = null;
|
|
135338
|
+
/**
|
|
135339
|
+
* The protocol version the SERVER reported at attach. Defaults to our own
|
|
135340
|
+
* version until the handshake resolves. Version-specific client methods (the
|
|
135341
|
+
* v4 workflow *builder* family) gate on this so a newer client attached to an
|
|
135342
|
+
* older runner degrades with a clear, actionable error instead of a raw
|
|
135343
|
+
* JSON-RPC method-not-found. Null until attached.
|
|
135344
|
+
*/
|
|
135345
|
+
serverProtocolVersion = null;
|
|
135254
135346
|
constructor(transport) {
|
|
135255
135347
|
this.peer = new JsonRpcPeer(transport);
|
|
135256
135348
|
this.peer.on(RunnerNotification.Event, (params) => {
|
|
@@ -135319,6 +135411,28 @@ var RemoteSession = class {
|
|
|
135319
135411
|
sinceSeq
|
|
135320
135412
|
});
|
|
135321
135413
|
this.info = result.info;
|
|
135414
|
+
this.serverProtocolVersion = typeof result.protocolVersion === "number" ? result.protocolVersion : RUNNER_PROTOCOL_VERSION;
|
|
135415
|
+
}
|
|
135416
|
+
/**
|
|
135417
|
+
* The protocol version the attached runner speaks (its own, from the
|
|
135418
|
+
* handshake). Lets a capability-detecting caller (e.g. the desktop's visual
|
|
135419
|
+
* builder, see #146) decide whether a version-gated feature is available on
|
|
135420
|
+
* THIS runner before invoking it. Null until attached.
|
|
135421
|
+
*/
|
|
135422
|
+
get runnerProtocolVersion() {
|
|
135423
|
+
return this.serverProtocolVersion;
|
|
135424
|
+
}
|
|
135425
|
+
/**
|
|
135426
|
+
* Guard a method that only exists on a server at/after `minVersion`. Throws a
|
|
135427
|
+
* clear, actionable error (not a raw JSON-RPC method-not-found) when the
|
|
135428
|
+
* attached runner is older — the desktop case after a JS hot-update outran
|
|
135429
|
+
* its bundled CLI.
|
|
135430
|
+
*/
|
|
135431
|
+
requireServerProtocol(minVersion, feature) {
|
|
135432
|
+
const server = this.serverProtocolVersion;
|
|
135433
|
+
if (server !== null && server < minVersion) {
|
|
135434
|
+
throw new Error(`${feature} is not supported by this runner (runner protocol v${server}, needs v${minVersion}) \u2014 update the moxxy CLI to continue.`);
|
|
135435
|
+
}
|
|
135322
135436
|
}
|
|
135323
135437
|
get id() {
|
|
135324
135438
|
return this.requireInfo().sessionId;
|
|
@@ -135554,12 +135668,37 @@ var RemoteSession = class {
|
|
|
135554
135668
|
run: (name) => this.peer.request(RunnerMethod.WorkflowRun, { name }),
|
|
135555
135669
|
// Builder methods (protocol v4): forward to the runner so the desktop's
|
|
135556
135670
|
// RemoteSession-backed visual builder can validate/save/load drafts.
|
|
135557
|
-
|
|
135558
|
-
|
|
135559
|
-
|
|
135560
|
-
|
|
135561
|
-
|
|
135562
|
-
|
|
135671
|
+
// Gated on the SERVER's reported version so a v4 client on a v3 runner
|
|
135672
|
+
// (a desktop whose JS hot-update outran its bundled CLI) gets a clear
|
|
135673
|
+
// "update the CLI" error instead of a raw method-not-found.
|
|
135674
|
+
validateDraft: async (yaml) => {
|
|
135675
|
+
this.requireServerProtocol(4, "The workflows builder");
|
|
135676
|
+
return this.peer.request(RunnerMethod.WorkflowValidateDraft, {
|
|
135677
|
+
yaml
|
|
135678
|
+
});
|
|
135679
|
+
},
|
|
135680
|
+
save: async (yaml, previousName) => {
|
|
135681
|
+
this.requireServerProtocol(4, "Saving a workflow from the builder");
|
|
135682
|
+
return this.peer.request(RunnerMethod.WorkflowSave, {
|
|
135683
|
+
yaml,
|
|
135684
|
+
...previousName ? { previousName } : {}
|
|
135685
|
+
});
|
|
135686
|
+
},
|
|
135687
|
+
getRun: async (name) => {
|
|
135688
|
+
this.requireServerProtocol(4, "Loading a workflow into the builder");
|
|
135689
|
+
return this.peer.request(RunnerMethod.WorkflowGetRun, { name });
|
|
135690
|
+
},
|
|
135691
|
+
// Human-in-the-loop resume (protocol v5). Gated on the SERVER's reported
|
|
135692
|
+
// version so a v5 client attached to a v4 runner (a desktop whose JS
|
|
135693
|
+
// hot-update outran its bundled CLI) gets a clear "update the CLI" error
|
|
135694
|
+
// rather than a raw method-not-found.
|
|
135695
|
+
resume: async (runId, reply2) => {
|
|
135696
|
+
this.requireServerProtocol(5, "Resuming a paused workflow");
|
|
135697
|
+
return this.peer.request(RunnerMethod.WorkflowResume, {
|
|
135698
|
+
runId,
|
|
135699
|
+
reply: reply2
|
|
135700
|
+
});
|
|
135701
|
+
}
|
|
135563
135702
|
};
|
|
135564
135703
|
}
|
|
135565
135704
|
};
|
|
@@ -135626,11 +135765,14 @@ async function connectRemoteSession(opts = {}) {
|
|
|
135626
135765
|
throw err;
|
|
135627
135766
|
}
|
|
135628
135767
|
}
|
|
135768
|
+
function isProtocolMismatchError(err) {
|
|
135769
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135770
|
+
return /protocol mismatch/i.test(msg);
|
|
135771
|
+
}
|
|
135629
135772
|
async function maybeRecoverFromMismatch(err, socketPath, opts) {
|
|
135630
135773
|
if (opts.transport || opts.skipMismatchRecovery)
|
|
135631
135774
|
return;
|
|
135632
|
-
|
|
135633
|
-
if (!/protocol mismatch/i.test(msg))
|
|
135775
|
+
if (!isProtocolMismatchError(err))
|
|
135634
135776
|
return;
|
|
135635
135777
|
try {
|
|
135636
135778
|
await killAndUnlinkRunner(socketPath, [...DEFAULT_RUNNER_PORTS, ...opts.extraPortsToFree ?? []]);
|
|
@@ -135786,7 +135928,13 @@ var REMOTE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
135786
135928
|
// phone must not rewrite or re-enable the host's workflows.
|
|
135787
135929
|
"workflows.list",
|
|
135788
135930
|
"workflows.run",
|
|
135789
|
-
"workflows.getRun"
|
|
135931
|
+
"workflows.getRun",
|
|
135932
|
+
// Answer a paused workflow's awaitInput question. This is RESPOND-only — like
|
|
135933
|
+
// `ask.respond`, the operator answers a question the WORKFLOW asked (the reply
|
|
135934
|
+
// is fed into the paused step and the run continues); it cannot create or
|
|
135935
|
+
// rewrite a workflow. A mobile user answering "ship it" to their own pipeline
|
|
135936
|
+
// is the canonical human-in-the-loop case, so it belongs on the trust surface.
|
|
135937
|
+
"workflows.resume"
|
|
135790
135938
|
]);
|
|
135791
135939
|
var providerName = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/, "invalid provider name");
|
|
135792
135940
|
var httpUrl = z.string().refine((s2) => {
|
|
@@ -135877,6 +136025,12 @@ var ipcInputSchemas = {
|
|
|
135877
136025
|
previousName: workflowName.optional()
|
|
135878
136026
|
}),
|
|
135879
136027
|
"workflows.getRun": z.object({ name: workflowName }),
|
|
136028
|
+
// Human-in-the-loop resume: bound the run id + the operator reply (the reply
|
|
136029
|
+
// is forwarded into the paused step's child agent, so cap it to avoid OOM).
|
|
136030
|
+
"workflows.resume": z.object({
|
|
136031
|
+
runId: z.string().min(1).max(120),
|
|
136032
|
+
reply: z.string().min(1).max(1e5)
|
|
136033
|
+
}),
|
|
135880
136034
|
// Security-sensitive: this bypasses the approval sheet, so validate it at
|
|
135881
136035
|
// the boundary like the other dangerous commands.
|
|
135882
136036
|
"session.setAutoApprove": z.object({ workspaceId: optionalWorkspace, enabled: z.boolean() }),
|
|
@@ -136162,6 +136316,12 @@ var MobileSessionHost = class {
|
|
|
136162
136316
|
}
|
|
136163
136317
|
return await this.session.workflows.getRun(name);
|
|
136164
136318
|
});
|
|
136319
|
+
this.bus.handle("workflows.resume", async ({ runId, reply: reply2 }) => {
|
|
136320
|
+
if (!this.session.workflows?.resume) {
|
|
136321
|
+
throw new IpcError("not-supported", "workflow resume not supported on this session");
|
|
136322
|
+
}
|
|
136323
|
+
return await this.session.workflows.resume(runId, reply2);
|
|
136324
|
+
});
|
|
136165
136325
|
this.bus.handle("ask.respond", async ({ requestId, response }) => {
|
|
136166
136326
|
this.answerAsk(requestId, response);
|
|
136167
136327
|
});
|
|
@@ -142121,10 +142281,11 @@ var stepSchema = z.object({
|
|
|
142121
142281
|
format: z.enum(["json", "plain"]).optional(),
|
|
142122
142282
|
awaitInput: z.boolean().optional()
|
|
142123
142283
|
}).superRefine((step, ctx) => {
|
|
142124
|
-
|
|
142284
|
+
const isLogic = step.bridge != null || step.condition != null || step.switch != null;
|
|
142285
|
+
if (step.awaitInput && (step.tool != null || step.workflow != null || step.loop != null || isLogic)) {
|
|
142125
142286
|
ctx.addIssue({
|
|
142126
142287
|
code: z.ZodIssueCode.custom,
|
|
142127
|
-
message: `step "${step.id}": awaitInput
|
|
142288
|
+
message: `step "${step.id}": awaitInput is only allowed on prompt or skill steps`,
|
|
142128
142289
|
path: ["awaitInput"]
|
|
142129
142290
|
});
|
|
142130
142291
|
}
|
|
@@ -142356,6 +142517,13 @@ var workflowSchema = z.object({
|
|
|
142356
142517
|
path: ["steps"]
|
|
142357
142518
|
});
|
|
142358
142519
|
}
|
|
142520
|
+
if (body.awaitInput) {
|
|
142521
|
+
ctx.addIssue({
|
|
142522
|
+
code: z.ZodIssueCode.custom,
|
|
142523
|
+
message: `step "${step.id}" is a loop body of "${owner}" and cannot use awaitInput \u2014 a loop body cannot pause mid-iteration. Ask the operator before/after the loop instead.`,
|
|
142524
|
+
path: ["steps"]
|
|
142525
|
+
});
|
|
142526
|
+
}
|
|
142359
142527
|
if (body.when != null) {
|
|
142360
142528
|
ctx.addIssue({
|
|
142361
142529
|
code: z.ZodIssueCode.custom,
|
|
@@ -142800,6 +142968,7 @@ function stepsToSkipForBranch(step, selected) {
|
|
|
142800
142968
|
// ../plugin-workflows/dist/executor/dag.js
|
|
142801
142969
|
var DAG_EXECUTOR_NAME = "dag";
|
|
142802
142970
|
var MAX_NESTING_DEPTH = 5;
|
|
142971
|
+
var FINALIZE_REPLY_SUFFIX = "\n\nFinalize now: consolidate the operator's answers into a clear structured response. Include every field the step instructions require. Do not ask further questions.";
|
|
142803
142972
|
function collectLoopBodyIds(workflow) {
|
|
142804
142973
|
const ids = /* @__PURE__ */ new Set();
|
|
142805
142974
|
for (const step of workflow.steps) {
|
|
@@ -142876,6 +143045,19 @@ function serializeStates(states) {
|
|
|
142876
143045
|
}
|
|
142877
143046
|
return out;
|
|
142878
143047
|
}
|
|
143048
|
+
function restoreStates(raw) {
|
|
143049
|
+
const states = /* @__PURE__ */ new Map();
|
|
143050
|
+
for (const [id, st3] of Object.entries(raw)) {
|
|
143051
|
+
states.set(id, {
|
|
143052
|
+
status: st3.status,
|
|
143053
|
+
output: st3.output,
|
|
143054
|
+
...st3.error ? { error: st3.error } : {},
|
|
143055
|
+
startedAt: st3.startedAt,
|
|
143056
|
+
endedAt: st3.endedAt
|
|
143057
|
+
});
|
|
143058
|
+
}
|
|
143059
|
+
return states;
|
|
143060
|
+
}
|
|
142879
143061
|
function buildStepResults(workflow, states) {
|
|
142880
143062
|
return workflow.steps.map((step) => {
|
|
142881
143063
|
const st3 = states.get(step.id);
|
|
@@ -143005,7 +143187,13 @@ async function runExecutorLoop(ctx) {
|
|
|
143005
143187
|
await deps.emit?.("workflow_paused", {
|
|
143006
143188
|
runId,
|
|
143007
143189
|
stepId: step.id,
|
|
143008
|
-
childSessionId: outcome.interactionAgentId
|
|
143190
|
+
childSessionId: outcome.interactionAgentId,
|
|
143191
|
+
// Carry the human-facing question so the operator UI is self-contained
|
|
143192
|
+
// (no separate event correlation needed): the workflow name, the step
|
|
143193
|
+
// label, and the prompt/question the paused step asked.
|
|
143194
|
+
workflow: workflow.name,
|
|
143195
|
+
label: step.label ?? step.id,
|
|
143196
|
+
prompt: outcome.output.slice(0, 2e3)
|
|
143009
143197
|
});
|
|
143010
143198
|
return buildRunResult(ctx, "paused", true, {
|
|
143011
143199
|
runId,
|
|
@@ -143062,6 +143250,78 @@ async function runExecutor(workflow, deps) {
|
|
|
143062
143250
|
}
|
|
143063
143251
|
return runExecutorLoop(ctx);
|
|
143064
143252
|
}
|
|
143253
|
+
async function resumeWorkflowRun(runId, userMessage, deps, store = defaultWorkflowRunStore) {
|
|
143254
|
+
const checkpoint = await store.load(runId);
|
|
143255
|
+
if (!checkpoint) {
|
|
143256
|
+
return {
|
|
143257
|
+
ok: false,
|
|
143258
|
+
status: "failed",
|
|
143259
|
+
steps: [],
|
|
143260
|
+
output: "",
|
|
143261
|
+
error: `no paused workflow run "${runId}"`
|
|
143262
|
+
};
|
|
143263
|
+
}
|
|
143264
|
+
const depsWithStore = { ...deps, runStore: store };
|
|
143265
|
+
const step = checkpoint.workflow.steps.find((s2) => s2.id === checkpoint.pendingStepId);
|
|
143266
|
+
if (!step) {
|
|
143267
|
+
return {
|
|
143268
|
+
ok: false,
|
|
143269
|
+
status: "failed",
|
|
143270
|
+
steps: buildStepResults(checkpoint.workflow, restoreStates(checkpoint.states)),
|
|
143271
|
+
output: "",
|
|
143272
|
+
error: `paused step "${checkpoint.pendingStepId}" not found`
|
|
143273
|
+
};
|
|
143274
|
+
}
|
|
143275
|
+
const restoredVars = {};
|
|
143276
|
+
const ctx = {
|
|
143277
|
+
workflow: checkpoint.workflow,
|
|
143278
|
+
deps: depsWithStore,
|
|
143279
|
+
inputs: checkpoint.inputs,
|
|
143280
|
+
vars: restoredVars,
|
|
143281
|
+
states: restoreStates(checkpoint.states),
|
|
143282
|
+
now: nowFn(deps),
|
|
143283
|
+
loopBodyIds: collectLoopBodyIds(checkpoint.workflow)
|
|
143284
|
+
};
|
|
143285
|
+
mergeVars(ctx, checkpoint.vars);
|
|
143286
|
+
const st3 = ctx.states.get(step.id);
|
|
143287
|
+
await deps.emit?.("workflow_resumed", { runId, stepId: step.id });
|
|
143288
|
+
if (typeof deps.spawner.continue !== "function") {
|
|
143289
|
+
st3.status = "failed";
|
|
143290
|
+
st3.error = "subagent spawner does not support resume (continue)";
|
|
143291
|
+
st3.endedAt = ctx.now();
|
|
143292
|
+
await deps.emit?.("workflow_step_failed", { id: step.id, error: st3.error });
|
|
143293
|
+
await store.remove(runId);
|
|
143294
|
+
return buildRunResult(ctx, "failed", false, { error: st3.error });
|
|
143295
|
+
}
|
|
143296
|
+
const finalizePrompt = `Operator reply:
|
|
143297
|
+
${userMessage.trim()}${FINALIZE_REPLY_SUFFIX}`;
|
|
143298
|
+
try {
|
|
143299
|
+
const child = await deps.spawner.continue({
|
|
143300
|
+
childSessionId: checkpoint.interactionAgentId,
|
|
143301
|
+
prompt: finalizePrompt,
|
|
143302
|
+
label: step.label ?? step.id
|
|
143303
|
+
});
|
|
143304
|
+
if (child.error)
|
|
143305
|
+
throw new Error(child.error.message);
|
|
143306
|
+
st3.status = "completed";
|
|
143307
|
+
st3.output = child.text;
|
|
143308
|
+
st3.endedAt = ctx.now();
|
|
143309
|
+
await deps.emit?.("workflow_step_completed", {
|
|
143310
|
+
id: step.id,
|
|
143311
|
+
preview: child.text.slice(0, 280)
|
|
143312
|
+
});
|
|
143313
|
+
} catch (err) {
|
|
143314
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143315
|
+
st3.status = "failed";
|
|
143316
|
+
st3.error = message;
|
|
143317
|
+
st3.endedAt = ctx.now();
|
|
143318
|
+
await deps.emit?.("workflow_step_failed", { id: step.id, error: message });
|
|
143319
|
+
await store.remove(runId);
|
|
143320
|
+
return buildRunResult(ctx, "failed", false, { error: message });
|
|
143321
|
+
}
|
|
143322
|
+
await store.remove(runId);
|
|
143323
|
+
return runExecutorLoop(ctx);
|
|
143324
|
+
}
|
|
143065
143325
|
function sinkOutput(workflow, states) {
|
|
143066
143326
|
const needed = /* @__PURE__ */ new Set();
|
|
143067
143327
|
for (const step of workflow.steps)
|
|
@@ -143414,7 +143674,7 @@ A workflow is a DAG of steps. Schema:
|
|
|
143414
143674
|
- when (optional, legacy): simple guards only \u2014 '{{ steps.x.output }} is not empty'. Do NOT use when for semantic decisions (use condition/switch).
|
|
143415
143675
|
- onError (optional): fail | continue | retry ; retries (optional, 0-3)
|
|
143416
143676
|
|
|
143417
|
-
|
|
143677
|
+
Operator data \u2014 two ways: declare a value the operator can supply UP FRONT as an \`inputs\` field (filled in before Run). To PAUSE mid-run and ask a question whose answer depends on earlier steps, set \`awaitInput: true\` on a prompt or skill step: the workflow pauses, surfaces the step's prompt to the operator, and resumes with their reply once they answer. Prefer \`inputs\` for known-up-front values; use \`awaitInput\` only for genuinely mid-run questions.
|
|
143418
143678
|
|
|
143419
143679
|
Templating: {{ steps.<id>.output }}, {{ inputs.<name> }}, {{ vars.<name> }}, {{ trigger }}, {{ now }}.
|
|
143420
143680
|
|
|
@@ -143424,8 +143684,8 @@ Ordering: steps whose \`needs\` are all satisfied run in parallel \u2014 chain w
|
|
|
143424
143684
|
|
|
143425
143685
|
Authoring rules:
|
|
143426
143686
|
1. Decompose the intent into concrete steps. Multi-phase requests (collect \u2192 act \u2192 summarize \u2192 deliver) need at least 4 steps with a linear or fan-in \`needs\` chain.
|
|
143427
|
-
2. Values the operator must supply (search topic, recipient email, brief): declare each as an \`inputs\` field with a clear \`description\` (and a \`default\` when sensible). The operator fills them in before Run; reference them downstream via \`{{ inputs.<name> }}\`.
|
|
143428
|
-
3.
|
|
143687
|
+
2. Values the operator must supply (search topic, recipient email, brief): declare each as an \`inputs\` field with a clear \`description\` (and a \`default\` when sensible). The operator fills them in before Run; reference them downstream via \`{{ inputs.<name> }}\`.
|
|
143688
|
+
3. To ask the operator a mid-run question whose answer depends on earlier steps, set \`awaitInput: true\` on a prompt or skill step \u2014 the run pauses, shows that step's prompt to the operator, and continues with their reply (referenced downstream via \`{{ steps.<id>.output }}\`). awaitInput is ONLY valid on prompt/skill steps (never tool/logic/loop or a loop body). Prefer \`inputs\` for values known before Run.
|
|
143429
143689
|
4. Research + report + email intents: typical chain \u2014 \`web-research\` skill (over \`{{ inputs.topic }}\`) \u2192 \`write_report\` \u2192 \`send_email\` tool (to \`{{ inputs.recipient }}\`). Put \`topic\` and \`recipient\` in \`inputs\`.
|
|
143430
143690
|
5. Use ONLY skill/tool names from the catalogs below \u2014 never placeholders like "<< skill-name >>", "TBD", or empty skill/tool fields.
|
|
143431
143691
|
6. Prefer a listed skill when its description fits; otherwise use a detailed \`prompt\` step.
|
|
@@ -143541,6 +143801,31 @@ steps:
|
|
|
143541
143801
|
Improve the current draft (start from {{ steps.first_draft.output }} or {{ vars.draft }}).
|
|
143542
143802
|
Return JSON with vars.draft set to the improved text.
|
|
143543
143803
|
\`\`\`
|
|
143804
|
+
|
|
143805
|
+
Example shape for a mid-run question (awaitInput pause \u2192 operator reply \u2192 continue):
|
|
143806
|
+
\`\`\`yaml
|
|
143807
|
+
name: draft-with-approval
|
|
143808
|
+
description: Draft an announcement, ask the operator to approve or tweak it, then publish.
|
|
143809
|
+
enabled: true
|
|
143810
|
+
steps:
|
|
143811
|
+
- id: draft
|
|
143812
|
+
label: Draft announcement
|
|
143813
|
+
prompt: |
|
|
143814
|
+
Write a short product announcement.
|
|
143815
|
+
- id: approve
|
|
143816
|
+
needs: [draft]
|
|
143817
|
+
label: Approve or tweak
|
|
143818
|
+
awaitInput: true
|
|
143819
|
+
prompt: |
|
|
143820
|
+
Here is the draft announcement:
|
|
143821
|
+
{{ steps.draft.output }}
|
|
143822
|
+
Reply with "ship it" to approve, or describe any changes you want.
|
|
143823
|
+
- id: publish
|
|
143824
|
+
needs: [approve]
|
|
143825
|
+
label: Publish
|
|
143826
|
+
prompt: |
|
|
143827
|
+
Apply the operator's decision ({{ steps.approve.output }}) and produce the final announcement.
|
|
143828
|
+
\`\`\`
|
|
143544
143829
|
(Replace skill/tool names with ones from the catalog when drafting.)`;
|
|
143545
143830
|
}
|
|
143546
143831
|
function formatCatalog(entries, emptyLabel) {
|
|
@@ -144195,6 +144480,49 @@ function buildWorkflowsIntegration(args) {
|
|
|
144195
144480
|
inFlight.delete(input.name);
|
|
144196
144481
|
}
|
|
144197
144482
|
}
|
|
144483
|
+
async function resumeNow(runId, reply2) {
|
|
144484
|
+
const checkpoint = await defaultWorkflowRunStore.load(runId);
|
|
144485
|
+
const turnId = session.startTurn().turnId;
|
|
144486
|
+
const spawner = createSubagentSpawner({
|
|
144487
|
+
parentSession: session,
|
|
144488
|
+
parentTurnId: turnId,
|
|
144489
|
+
parentSignal: session.signal,
|
|
144490
|
+
parentModel: activeModel(session)
|
|
144491
|
+
});
|
|
144492
|
+
const result = await resumeWorkflowRun(
|
|
144493
|
+
runId,
|
|
144494
|
+
reply2,
|
|
144495
|
+
{
|
|
144496
|
+
spawner,
|
|
144497
|
+
tools: session.tools,
|
|
144498
|
+
lookup: {
|
|
144499
|
+
skill: (n2) => session.skills.byName(n2),
|
|
144500
|
+
workflow: (n2) => store.lookup(n2)
|
|
144501
|
+
},
|
|
144502
|
+
signal: session.signal,
|
|
144503
|
+
now: () => Date.now(),
|
|
144504
|
+
emit: (subtype, payload) => void session.log.append({
|
|
144505
|
+
type: "plugin_event",
|
|
144506
|
+
sessionId: session.id,
|
|
144507
|
+
turnId,
|
|
144508
|
+
source: "plugin",
|
|
144509
|
+
pluginId: PLUGIN_ID3,
|
|
144510
|
+
subtype,
|
|
144511
|
+
payload
|
|
144512
|
+
}),
|
|
144513
|
+
...logger ? { logger } : {}
|
|
144514
|
+
},
|
|
144515
|
+
defaultWorkflowRunStore
|
|
144516
|
+
);
|
|
144517
|
+
if (result.status === "paused") {
|
|
144518
|
+
logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
|
|
144519
|
+
runId: result.runId
|
|
144520
|
+
});
|
|
144521
|
+
return result;
|
|
144522
|
+
}
|
|
144523
|
+
if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
|
|
144524
|
+
return result;
|
|
144525
|
+
}
|
|
144198
144526
|
const view = {
|
|
144199
144527
|
list: async () => (await store.list()).map((w4) => ({
|
|
144200
144528
|
name: w4.workflow.name,
|
|
@@ -144214,7 +144542,9 @@ function buildWorkflowsIntegration(args) {
|
|
|
144214
144542
|
ok: r2.ok,
|
|
144215
144543
|
output: r2.output,
|
|
144216
144544
|
...r2.error ? { error: r2.error } : {},
|
|
144217
|
-
steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} }))
|
|
144545
|
+
steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} })),
|
|
144546
|
+
status: r2.status,
|
|
144547
|
+
...r2.runId ? { runId: r2.runId } : {}
|
|
144218
144548
|
};
|
|
144219
144549
|
},
|
|
144220
144550
|
// Builder-facing additions (phase 2 GUI): validate a draft YAML, persist a
|
|
@@ -144242,6 +144572,18 @@ function buildWorkflowsIntegration(args) {
|
|
|
144242
144572
|
path: entry.path,
|
|
144243
144573
|
yaml: serializeWorkflow(entry.workflow)
|
|
144244
144574
|
};
|
|
144575
|
+
},
|
|
144576
|
+
// Human-in-the-loop: answer a paused run's awaitInput question and resume.
|
|
144577
|
+
resume: async (runId, reply2) => {
|
|
144578
|
+
const r2 = await resumeNow(runId, reply2);
|
|
144579
|
+
return {
|
|
144580
|
+
ok: r2.ok,
|
|
144581
|
+
output: r2.output,
|
|
144582
|
+
...r2.error ? { error: r2.error } : {},
|
|
144583
|
+
steps: r2.steps.map((s2) => ({ id: s2.id, status: s2.status, ...s2.error ? { error: s2.error } : {} })),
|
|
144584
|
+
status: r2.status,
|
|
144585
|
+
...r2.runId ? { runId: r2.runId } : {}
|
|
144586
|
+
};
|
|
144245
144587
|
}
|
|
144246
144588
|
};
|
|
144247
144589
|
async function syncSchedules() {
|