@osolmaz/pi-workflows 0.14.0 → 0.15.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 +78 -110
- package/dist/builtins/autoimplement.workflow.js +3 -1
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/change-verification.workflow.js +7 -2
- package/dist/builtins/change-verification.workflow.js.map +1 -1
- package/dist/builtins/metadata.d.ts +22 -0
- package/dist/builtins/metadata.js +10 -0
- package/dist/builtins/metadata.js.map +1 -0
- package/dist/builtins/monitor.workflow.js +4 -1
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.js +4 -1
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.js +3 -1
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -1
- package/dist/controllers/index.d.ts +0 -1
- package/dist/controllers/index.js +0 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +51 -0
- package/dist/controllers/sqlite.js +433 -57
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +1 -0
- package/dist/extension/controller-command.d.ts +22 -0
- package/dist/extension/controller-command.js +32 -0
- package/dist/extension/controller-command.js.map +1 -0
- package/dist/extension/index.d.ts +2 -9
- package/dist/extension/index.js +876 -3447
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/message-card.d.ts +13 -0
- package/dist/extension/message-card.js +46 -0
- package/dist/extension/message-card.js.map +1 -0
- package/dist/extension/step-message.d.ts +5 -6
- package/dist/extension/step-message.js +41 -52
- package/dist/extension/step-message.js.map +1 -1
- package/dist/host/child-worker-supervisor.d.ts +50 -0
- package/dist/host/child-worker-supervisor.js +159 -0
- package/dist/host/child-worker-supervisor.js.map +1 -0
- package/dist/host/client.d.ts +48 -0
- package/dist/host/client.js +216 -0
- package/dist/host/client.js.map +1 -0
- package/dist/host/controller-worker-entry.d.ts +2 -0
- package/dist/host/controller-worker-entry.js +244 -0
- package/dist/host/controller-worker-entry.js.map +1 -0
- package/dist/host/controller-worker-protocol.d.ts +36 -0
- package/dist/host/controller-worker-protocol.js +49 -0
- package/dist/host/controller-worker-protocol.js.map +1 -0
- package/dist/host/controller-worker-supervisor.d.ts +21 -0
- package/dist/host/controller-worker-supervisor.js +54 -0
- package/dist/host/controller-worker-supervisor.js.map +1 -0
- package/dist/host/host-entry.d.ts +2 -0
- package/dist/host/host-entry.js +23 -0
- package/dist/host/host-entry.js.map +1 -0
- package/dist/host/processes.d.ts +17 -12
- package/dist/host/processes.js +154 -50
- package/dist/host/processes.js.map +1 -1
- package/dist/host/protocol.d.ts +38 -0
- package/dist/host/protocol.js +156 -0
- package/dist/host/protocol.js.map +1 -0
- package/dist/host/resolver-entry.d.ts +49 -0
- package/dist/host/resolver-entry.js +149 -0
- package/dist/host/resolver-entry.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +14 -3
- package/dist/host/rpc-executor.js +63 -30
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +95 -29
- package/dist/host/runner.js +2311 -348
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +174 -0
- package/dist/host/state.js +689 -0
- package/dist/host/state.js.map +1 -0
- package/dist/host/worker-entry.d.ts +10 -0
- package/dist/host/worker-entry.js +446 -0
- package/dist/host/worker-entry.js.map +1 -0
- package/dist/host/worker-protocol.d.ts +31 -0
- package/dist/host/worker-protocol.js +122 -0
- package/dist/host/worker-protocol.js.map +1 -0
- package/dist/host/worker-store.d.ts +80 -0
- package/dist/host/worker-store.js +91 -0
- package/dist/host/worker-store.js.map +1 -0
- package/dist/host/worker-supervisor.d.ts +22 -0
- package/dist/host/worker-supervisor.js +54 -0
- package/dist/host/worker-supervisor.js.map +1 -0
- package/dist/render/canvas.d.ts +2 -1
- package/dist/render/canvas.js +40 -15
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +7 -3
- package/dist/render/graph-render.js +125 -74
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.js +3 -1
- package/dist/state/database.js.map +1 -1
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +171 -3
- package/dist/state/schema.js.map +1 -1
- package/dist/state/viewer.d.ts +46 -0
- package/dist/state/viewer.js +249 -0
- package/dist/state/viewer.js.map +1 -0
- package/dist/viewer/cli.d.ts +1 -1
- package/dist/viewer/cli.js +47 -18
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/composition.js +25 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +3 -1
- package/dist/workflows/definition.js +25 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +1 -0
- package/dist/workflows/engine.js +157 -42
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -1
- package/dist/workflows/errors.js +4 -7
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +3 -0
- package/dist/workflows/human-decision.js +31 -0
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -6
- package/dist/workflows/schema.js.map +1 -1
- package/dist/{viewer → workflows}/session-reducer.d.ts +3 -1
- package/dist/{viewer → workflows}/session-reducer.js +4 -0
- package/dist/workflows/session-reducer.js.map +1 -0
- package/dist/workflows/store.d.ts +87 -1
- package/dist/workflows/store.js +892 -93
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +0 -26
- package/dist/workflows/tool-input.js +2 -50
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +36 -4
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +353 -0
- package/docs/CONTROLLERS.md +11 -11
- package/docs/DEFERRED_TURNS.md +23 -2
- package/docs/SQLITE_STATE.md +41 -31
- package/docs/WORKFLOW_HOST.md +452 -0
- package/docs/development.md +46 -30
- package/docs/live-replay-protocol.md +129 -100
- package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +42 -0
- package/docs/plans/2026-08-28-piw-incremental-viewer-plan.md +293 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +11 -1
- package/docs/plans/tui-viewer-implementation-plan.md +5 -0
- package/docs/tui-viewer.md +18 -3
- package/docs/workflows.md +162 -210
- package/examples/workflows/command-batch.workflow.ts +2 -0
- package/examples/workflows/shell.workflow.ts +2 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +3 -0
- package/src/builtins/change-verification.workflow.ts +7 -2
- package/src/builtins/metadata.ts +9 -0
- package/src/builtins/monitor.workflow.ts +4 -0
- package/src/builtins/sanity-check.workflow.ts +4 -0
- package/src/builtins/workspace-preparation.workflow.ts +3 -1
- package/src/controllers/index.ts +0 -5
- package/src/controllers/sqlite.ts +709 -76
- package/src/controllers/store.ts +1 -0
- package/src/extension/controller-command.ts +45 -0
- package/src/extension/index.ts +944 -4163
- package/src/extension/message-card.ts +61 -0
- package/src/extension/step-message.ts +58 -63
- package/src/host/child-worker-supervisor.ts +183 -0
- package/src/host/client.ts +293 -0
- package/src/host/controller-worker-entry.ts +311 -0
- package/src/host/controller-worker-protocol.ts +104 -0
- package/src/host/controller-worker-supervisor.ts +79 -0
- package/src/host/host-entry.ts +23 -0
- package/src/host/processes.ts +171 -54
- package/src/host/protocol.ts +196 -0
- package/src/host/resolver-entry.ts +241 -0
- package/src/host/rpc-executor.ts +76 -34
- package/src/host/runner.ts +2813 -422
- package/src/host/state.ts +1160 -0
- package/src/host/worker-entry.ts +533 -0
- package/src/host/worker-protocol.ts +165 -0
- package/src/host/worker-store.ts +229 -0
- package/src/host/worker-supervisor.ts +74 -0
- package/src/render/canvas.ts +44 -10
- package/src/render/graph-render.ts +145 -90
- package/src/state/database.ts +2 -1
- package/src/state/index.ts +14 -0
- package/src/state/schema.ts +171 -3
- package/src/state/viewer.ts +356 -0
- package/src/viewer/cli.ts +49 -17
- package/src/workflows/composition.ts +36 -2
- package/src/workflows/definition.ts +32 -0
- package/src/workflows/engine.ts +157 -54
- package/src/workflows/errors.ts +11 -2
- package/src/workflows/human-decision.ts +49 -0
- package/src/workflows/index.ts +2 -0
- package/src/workflows/schema.ts +19 -6
- package/src/{viewer → workflows}/session-reducer.ts +11 -1
- package/src/workflows/store.ts +1316 -108
- package/src/workflows/tool-input.ts +3 -69
- package/src/workflows/types.ts +32 -4
- package/dist/controllers/workflow-engine-scheduler.d.ts +0 -29
- package/dist/controllers/workflow-engine-scheduler.js +0 -175
- package/dist/controllers/workflow-engine-scheduler.js.map +0 -1
- package/dist/extension/controller-host.d.ts +0 -48
- package/dist/extension/controller-host.js +0 -110
- package/dist/extension/controller-host.js.map +0 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +0 -32
- package/dist/extension/deferred-turn-coordinator.js +0 -143
- package/dist/extension/deferred-turn-coordinator.js.map +0 -1
- package/dist/extension/deferred-turn.d.ts +0 -44
- package/dist/extension/deferred-turn.js +0 -110
- package/dist/extension/deferred-turn.js.map +0 -1
- package/dist/extension/executor.d.ts +0 -86
- package/dist/extension/executor.js +0 -311
- package/dist/extension/executor.js.map +0 -1
- package/dist/extension/follow-up-coordinator.d.ts +0 -27
- package/dist/extension/follow-up-coordinator.js +0 -131
- package/dist/extension/follow-up-coordinator.js.map +0 -1
- package/dist/extension/recorder.d.ts +0 -84
- package/dist/extension/recorder.js +0 -528
- package/dist/extension/recorder.js.map +0 -1
- package/dist/extension/restart-policy.d.ts +0 -38
- package/dist/extension/restart-policy.js +0 -116
- package/dist/extension/restart-policy.js.map +0 -1
- package/dist/extension/session-events.d.ts +0 -133
- package/dist/extension/session-events.js +0 -61
- package/dist/extension/session-events.js.map +0 -1
- package/dist/extension/terminal-decision.d.ts +0 -51
- package/dist/extension/terminal-decision.js +0 -110
- package/dist/extension/terminal-decision.js.map +0 -1
- package/dist/viewer/session-reducer.js.map +0 -1
- package/src/controllers/workflow-engine-scheduler.ts +0 -246
- package/src/extension/controller-host.ts +0 -167
- package/src/extension/deferred-turn-coordinator.ts +0 -171
- package/src/extension/deferred-turn.ts +0 -166
- package/src/extension/executor.ts +0 -411
- package/src/extension/follow-up-coordinator.ts +0 -151
- package/src/extension/recorder.ts +0 -655
- package/src/extension/restart-policy.ts +0 -163
- package/src/extension/session-events.ts +0 -121
- package/src/extension/terminal-decision.ts +0 -172
package/src/viewer/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { SqliteControllerStore } from "../controllers/sqlite.js";
|
|
6
6
|
import { syncHerdrPlugin } from "../herdr/setup.js";
|
|
7
|
+
import { WorkflowHostClient } from "../host/client.js";
|
|
7
8
|
import { sanitizeText } from "../render/ansi.js";
|
|
8
9
|
import { StateDatabase } from "../state/database.js";
|
|
9
10
|
import { pruneState } from "../state/prune.js";
|
|
@@ -28,7 +29,10 @@ Usage:
|
|
|
28
29
|
pi-workflows state backup <destination>
|
|
29
30
|
pi-workflows state prune --before <timestamp> --dry-run
|
|
30
31
|
pi-workflows state prune --before <timestamp> --backup <absolute-path> --apply
|
|
31
|
-
pi-workflows host
|
|
32
|
+
pi-workflows host start
|
|
33
|
+
pi-workflows host status
|
|
34
|
+
pi-workflows host stop
|
|
35
|
+
pi-workflows host run [-- <extra pi args>]
|
|
32
36
|
pi-workflows herdr sync [--json]
|
|
33
37
|
pi-workflows herdr setup [--json]
|
|
34
38
|
|
|
@@ -41,13 +45,13 @@ export type CliArgs = {
|
|
|
41
45
|
controllerName?: string;
|
|
42
46
|
resourceKey?: string;
|
|
43
47
|
herdrAction?: string;
|
|
48
|
+
hostAction?: "start" | "status" | "stop" | "run";
|
|
44
49
|
stateAction?: "status" | "verify" | "backup" | "prune";
|
|
45
50
|
backupDestination?: string;
|
|
46
51
|
pruneBefore?: string;
|
|
47
52
|
pruneApply?: boolean;
|
|
48
53
|
once: boolean;
|
|
49
54
|
json: boolean;
|
|
50
|
-
project?: string | undefined;
|
|
51
55
|
piArgs?: string[] | undefined;
|
|
52
56
|
};
|
|
53
57
|
|
|
@@ -80,7 +84,18 @@ export function parseCliArgs(argv: string[]): CliArgs {
|
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
if (command !== "herdr" && json) throw new Error("--json is available only for herdr sync");
|
|
83
|
-
if (command === "host")
|
|
87
|
+
if (command === "host") {
|
|
88
|
+
if (project !== undefined) throw new Error("The global host does not accept --project");
|
|
89
|
+
const action = positionals[0];
|
|
90
|
+
if (action !== "start" && action !== "status" && action !== "stop" && action !== "run") {
|
|
91
|
+
throw new Error("host requires start, status, stop, or run");
|
|
92
|
+
}
|
|
93
|
+
if (positionals.length !== 1) throw new Error("host accepts one lifecycle action");
|
|
94
|
+
if (piArgs.length > 0 && action !== "run") {
|
|
95
|
+
throw new Error("Extra Pi arguments are available only for host run");
|
|
96
|
+
}
|
|
97
|
+
return { command, hostAction: action, once, json, piArgs };
|
|
98
|
+
}
|
|
84
99
|
if (command === "controller") {
|
|
85
100
|
if (positionals.length !== 2) throw new Error("controller requires <controller> and <key>");
|
|
86
101
|
return {
|
|
@@ -266,7 +281,9 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
266
281
|
await runStateCommand(databasePath, args);
|
|
267
282
|
return 0;
|
|
268
283
|
}
|
|
269
|
-
if (args.command === "host")
|
|
284
|
+
if (args.command === "host") {
|
|
285
|
+
return await runHost(args.hostAction as "start" | "status" | "stop" | "run", args.piArgs);
|
|
286
|
+
}
|
|
270
287
|
if (args.command === "herdr") {
|
|
271
288
|
const result = syncHerdrPlugin(packageRoot());
|
|
272
289
|
process.stdout.write(args.json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
|
|
@@ -333,20 +350,35 @@ async function runStateCommand(databasePath: string, args: CliArgs): Promise<voi
|
|
|
333
350
|
}
|
|
334
351
|
}
|
|
335
352
|
|
|
336
|
-
async function runHost(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
353
|
+
async function runHost(
|
|
354
|
+
action: "start" | "status" | "stop" | "run",
|
|
355
|
+
piArgs: string[] | undefined,
|
|
356
|
+
): Promise<number> {
|
|
357
|
+
if (action === "run") {
|
|
358
|
+
const { WorkflowHost } = await import("../host/runner.js");
|
|
359
|
+
const host = new WorkflowHost({
|
|
360
|
+
piArgs: piArgs ?? [],
|
|
361
|
+
onLog: (message) => process.stdout.write(`[host] ${message}\n`),
|
|
362
|
+
});
|
|
363
|
+
await host.start();
|
|
364
|
+
await new Promise<void>((resolve) => {
|
|
365
|
+
const shutdown = () => void host.stop().then(resolve);
|
|
366
|
+
process.once("SIGTERM", shutdown);
|
|
367
|
+
process.once("SIGINT", shutdown);
|
|
368
|
+
});
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
const client = new WorkflowHostClient();
|
|
372
|
+
if (action === "start") {
|
|
373
|
+
const response = await client.ensureRunning();
|
|
374
|
+
process.stdout.write(`${JSON.stringify(response.receipt ?? {})}\n`);
|
|
375
|
+
return response.outcome === "accepted" || response.outcome === "adopted" ? 0 : 1;
|
|
376
|
+
}
|
|
377
|
+
const response = await client.request({
|
|
378
|
+
operation: action === "status" ? "host.status" : "host.stop",
|
|
348
379
|
});
|
|
349
|
-
|
|
380
|
+
process.stdout.write(`${JSON.stringify(response.receipt ?? {})}\n`);
|
|
381
|
+
return response.outcome === "accepted" || response.outcome === "adopted" ? 0 : 1;
|
|
350
382
|
}
|
|
351
383
|
|
|
352
384
|
function openControllerStore(databasePath: string): SqliteControllerStore | undefined {
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
WorkflowEdge,
|
|
15
15
|
WorkflowExitDefinition,
|
|
16
16
|
WorkflowIncludeDefinition,
|
|
17
|
+
WorkflowManagedEffect,
|
|
17
18
|
WorkflowMountedSource,
|
|
18
19
|
WorkflowMountSnapshot,
|
|
19
20
|
WorkflowNodeContext,
|
|
@@ -539,10 +540,18 @@ function wrapNode(
|
|
|
539
540
|
return {
|
|
540
541
|
...common,
|
|
541
542
|
nodeType: "action",
|
|
542
|
-
|
|
543
|
+
...(wrappedEffect(shell, project) === undefined
|
|
544
|
+
? {}
|
|
545
|
+
: { effect: wrappedEffect(shell, project) }),
|
|
546
|
+
exec: (context) =>
|
|
547
|
+
shell.exec(projectActionContext(context, scopePath, scopes, entries, exits)),
|
|
543
548
|
...(shell.parse !== undefined
|
|
544
549
|
? {
|
|
545
|
-
parse: (result, context) =>
|
|
550
|
+
parse: (result, context) =>
|
|
551
|
+
shell.parse?.(
|
|
552
|
+
result,
|
|
553
|
+
projectActionContext(context, scopePath, scopes, entries, exits),
|
|
554
|
+
),
|
|
546
555
|
}
|
|
547
556
|
: {}),
|
|
548
557
|
...(shell.updates !== undefined
|
|
@@ -564,6 +573,9 @@ function wrapNode(
|
|
|
564
573
|
return {
|
|
565
574
|
...common,
|
|
566
575
|
nodeType: "action",
|
|
576
|
+
...(wrappedEffect(node, project) === undefined
|
|
577
|
+
? {}
|
|
578
|
+
: { effect: wrappedEffect(node, project) }),
|
|
567
579
|
run: (context) =>
|
|
568
580
|
(node as FunctionActionNodeDefinition).run(
|
|
569
581
|
projectActionContext(context, scopePath, scopes, entries, exits),
|
|
@@ -572,6 +584,27 @@ function wrapNode(
|
|
|
572
584
|
}
|
|
573
585
|
}
|
|
574
586
|
|
|
587
|
+
function wrappedEffect(
|
|
588
|
+
node: ActionNodeDefinition,
|
|
589
|
+
project: (context: WorkflowNodeContext) => WorkflowNodeContext,
|
|
590
|
+
): WorkflowManagedEffect | undefined {
|
|
591
|
+
if (node.effect === undefined) return undefined;
|
|
592
|
+
const idempotencyKey = node.effect.idempotencyKey;
|
|
593
|
+
const request = node.effect.request;
|
|
594
|
+
return {
|
|
595
|
+
type: node.effect.type,
|
|
596
|
+
recovery: node.effect.recovery,
|
|
597
|
+
idempotencyKey:
|
|
598
|
+
typeof idempotencyKey === "function"
|
|
599
|
+
? (context: WorkflowNodeContext) => idempotencyKey(project(context))
|
|
600
|
+
: idempotencyKey,
|
|
601
|
+
request:
|
|
602
|
+
typeof request === "function"
|
|
603
|
+
? (context: WorkflowNodeContext) => request(project(context))
|
|
604
|
+
: request,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
575
608
|
function projectActionContext(
|
|
576
609
|
context: WorkflowActionContext,
|
|
577
610
|
scopePath: string,
|
|
@@ -581,6 +614,7 @@ function projectActionContext(
|
|
|
581
614
|
): WorkflowActionContext {
|
|
582
615
|
return {
|
|
583
616
|
...projectWorkflowContext(context, scopePath, scopes, entries, exits),
|
|
617
|
+
effect: context.effect,
|
|
584
618
|
publishUpdate: context.publishUpdate,
|
|
585
619
|
};
|
|
586
620
|
}
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
WorkflowIncludedResult,
|
|
26
26
|
WorkflowIncludeMap,
|
|
27
27
|
WorkflowInputOf,
|
|
28
|
+
WorkflowManagedEffect,
|
|
28
29
|
WorkflowNodeContext,
|
|
29
30
|
WorkflowNodeDefinition,
|
|
30
31
|
WorkflowSettingsOf,
|
|
@@ -171,6 +172,37 @@ export function isWorkflowDefinition(value: unknown): value is WorkflowDefinitio
|
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
export function manualEffect(type: string): WorkflowManagedEffect {
|
|
176
|
+
return workflowEffect(type, "manual");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function idempotentEffect(type: string): WorkflowManagedEffect {
|
|
180
|
+
return workflowEffect(type, "idempotent");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function workflowEffect(
|
|
184
|
+
type: string,
|
|
185
|
+
recovery: WorkflowManagedEffect["recovery"],
|
|
186
|
+
): WorkflowManagedEffect {
|
|
187
|
+
const normalized = type.trim();
|
|
188
|
+
if (normalized.length === 0 || normalized.length > 256) {
|
|
189
|
+
throw new Error("Managed effect type must be nonempty text of at most 256 characters");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
type: normalized,
|
|
193
|
+
recovery,
|
|
194
|
+
idempotencyKey: ({ state }: WorkflowNodeContext) => {
|
|
195
|
+
const nodeId = state.currentNode;
|
|
196
|
+
if (nodeId === undefined) {
|
|
197
|
+
throw new Error("Managed effect resolution requires an active workflow node");
|
|
198
|
+
}
|
|
199
|
+
const nodeInvocation = state.steps.filter((step) => step.nodeId === nodeId).length + 1;
|
|
200
|
+
return `${state.runId}:${normalized}:${nodeId}:${nodeInvocation}`;
|
|
201
|
+
},
|
|
202
|
+
request: ({ input, outputs }: WorkflowNodeContext) => ({ input, outputs }),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
174
206
|
export function assistantMessage(options: { maxChars?: number } = {}): AssistantMessageOutput {
|
|
175
207
|
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
176
208
|
throw new Error("Invalid workflow definition: assistantMessage options must be an object");
|
package/src/workflows/engine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { canonicalJson } from "../state/json.js";
|
|
3
|
+
import { canonicalJson, type JsonValue } from "../state/json.js";
|
|
4
4
|
import { StaleResourceError } from "../state/mutation.js";
|
|
5
5
|
import {
|
|
6
6
|
compileWorkflowDefinition,
|
|
@@ -18,11 +18,7 @@ import {
|
|
|
18
18
|
WorkflowSourceChangedError,
|
|
19
19
|
} from "./errors.js";
|
|
20
20
|
import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
21
|
-
import {
|
|
22
|
-
HumanDecisionStore,
|
|
23
|
-
createHumanDecisionRequest,
|
|
24
|
-
validateHumanDecisionResponse,
|
|
25
|
-
} from "./human-decision.js";
|
|
21
|
+
import { createHumanDecisionRequest, validateHumanDecisionResponse } from "./human-decision.js";
|
|
26
22
|
import { extractJsonValue } from "./json.js";
|
|
27
23
|
import {
|
|
28
24
|
parseWorkflowSettingsValue,
|
|
@@ -35,6 +31,7 @@ import {
|
|
|
35
31
|
WorkflowRunStore,
|
|
36
32
|
createDefinitionSnapshot,
|
|
37
33
|
createRunId,
|
|
34
|
+
type WorkflowExecutionStore,
|
|
38
35
|
} from "./store.js";
|
|
39
36
|
import type {
|
|
40
37
|
ResolvedHumanDecision,
|
|
@@ -104,6 +101,7 @@ type ResumedNodeAttempt = {
|
|
|
104
101
|
nodeId: string;
|
|
105
102
|
attemptId: string;
|
|
106
103
|
startedAt: string;
|
|
104
|
+
deadlineAt?: string | null;
|
|
107
105
|
settings?: WorkflowSettingsScopeRecord;
|
|
108
106
|
};
|
|
109
107
|
|
|
@@ -116,7 +114,7 @@ type ResumedNodeAttempt = {
|
|
|
116
114
|
export class WorkflowEngine {
|
|
117
115
|
private readonly executor: AgentStepExecutor;
|
|
118
116
|
private readonly notificationSink: WorkflowNotificationSink | undefined;
|
|
119
|
-
private readonly store:
|
|
117
|
+
private readonly store: WorkflowExecutionStore;
|
|
120
118
|
private readonly defaultNodeTimeoutMs: number;
|
|
121
119
|
private readonly maxSteps: number;
|
|
122
120
|
private readonly onEvent?: WorkflowEngineOptions["onEvent"];
|
|
@@ -306,6 +304,7 @@ export class WorkflowEngine {
|
|
|
306
304
|
try {
|
|
307
305
|
await this.executeGraph(workflow, state, runId);
|
|
308
306
|
} catch (error) {
|
|
307
|
+
if (isClaimLostError(error)) throw error;
|
|
309
308
|
if (isRunParkedError(error) || this.parked) {
|
|
310
309
|
return { runId, state };
|
|
311
310
|
}
|
|
@@ -323,7 +322,11 @@ export class WorkflowEngine {
|
|
|
323
322
|
async resumeRun(
|
|
324
323
|
workflow: WorkflowDefinition,
|
|
325
324
|
runId: string,
|
|
326
|
-
options: {
|
|
325
|
+
options: {
|
|
326
|
+
workflowSource?: WorkflowSource;
|
|
327
|
+
force?: boolean;
|
|
328
|
+
resumeInteractionAttemptId?: string;
|
|
329
|
+
} = {},
|
|
327
330
|
): Promise<WorkflowRunResult> {
|
|
328
331
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
329
332
|
validateWorkflowDefinition(workflow);
|
|
@@ -349,25 +352,30 @@ export class WorkflowEngine {
|
|
|
349
352
|
state.currentAttemptId !== undefined &&
|
|
350
353
|
state.currentNodeStartedAt !== undefined &&
|
|
351
354
|
interruptedNode?.nodeType === "agent" &&
|
|
352
|
-
assistantMessageConfig(interruptedNode) !== undefined
|
|
355
|
+
(assistantMessageConfig(interruptedNode) !== undefined ||
|
|
356
|
+
options.resumeInteractionAttemptId === state.currentAttemptId)
|
|
353
357
|
) {
|
|
354
|
-
const resumedSettings = this.persistedSettingsBinding(state);
|
|
358
|
+
const resumedSettings = await this.persistedSettingsBinding(state);
|
|
355
359
|
resumedAttempt = {
|
|
356
360
|
nodeId: state.currentNode,
|
|
357
361
|
attemptId: state.currentAttemptId,
|
|
358
362
|
startedAt: state.currentNodeStartedAt,
|
|
363
|
+
...(state.currentNodeDeadlineAt === undefined
|
|
364
|
+
? {}
|
|
365
|
+
: { deadlineAt: state.currentNodeDeadlineAt }),
|
|
359
366
|
...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
|
|
360
367
|
};
|
|
361
368
|
}
|
|
362
|
-
// A resumed run starts unpaused. Submitted and non-agent nodes
|
|
363
|
-
// stale in-flight markers and start a new attempt.
|
|
364
|
-
//
|
|
365
|
-
//
|
|
369
|
+
// A resumed run starts unpaused. Submitted and non-agent nodes normally
|
|
370
|
+
// discard stale in-flight markers and start a new attempt. A submitted
|
|
371
|
+
// interaction and an assistant-message node keep their exact attempt id
|
|
372
|
+
// so durable origin-session work is never detached from its contract.
|
|
366
373
|
delete state.paused;
|
|
367
374
|
if (resumedAttempt === undefined) {
|
|
368
375
|
delete state.currentNode;
|
|
369
376
|
delete state.currentAttemptId;
|
|
370
377
|
delete state.currentNodeStartedAt;
|
|
378
|
+
delete state.currentNodeDeadlineAt;
|
|
371
379
|
delete state.currentSettingsScopeId;
|
|
372
380
|
delete state.currentSettingsChangeNumber;
|
|
373
381
|
delete state.currentSettingsHash;
|
|
@@ -416,6 +424,7 @@ export class WorkflowEngine {
|
|
|
416
424
|
resumedAttempt,
|
|
417
425
|
);
|
|
418
426
|
} catch (error) {
|
|
427
|
+
if (isClaimLostError(error)) throw error;
|
|
419
428
|
if (isRunParkedError(error) || this.parked) {
|
|
420
429
|
return { runId, state };
|
|
421
430
|
}
|
|
@@ -447,7 +456,7 @@ export class WorkflowEngine {
|
|
|
447
456
|
this.cancelled = false;
|
|
448
457
|
this.paused = false;
|
|
449
458
|
this.parked = false;
|
|
450
|
-
const parent = this.store.readRun(parentRunId);
|
|
459
|
+
const parent = await this.store.readRun(parentRunId);
|
|
451
460
|
if (parent === null) {
|
|
452
461
|
throw new Error(`Cannot continue from unreadable workflow run: ${parentRunId}`);
|
|
453
462
|
}
|
|
@@ -480,9 +489,7 @@ export class WorkflowEngine {
|
|
|
480
489
|
) {
|
|
481
490
|
throw new Error("Accepted human decision does not match the waiting request");
|
|
482
491
|
}
|
|
483
|
-
const durableDecision = await
|
|
484
|
-
state: this.store.state,
|
|
485
|
-
}).readResolved(request.decisionId);
|
|
492
|
+
const durableDecision = await this.store.readResolvedHumanDecision(request.decisionId);
|
|
486
493
|
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
487
494
|
throw new Error("Accepted human decision does not match the durable decision record");
|
|
488
495
|
}
|
|
@@ -572,6 +579,7 @@ export class WorkflowEngine {
|
|
|
572
579
|
point.lastOutput,
|
|
573
580
|
);
|
|
574
581
|
} catch (error) {
|
|
582
|
+
if (isClaimLostError(error)) throw error;
|
|
575
583
|
if (isRunParkedError(error) || this.parked) {
|
|
576
584
|
return { runId, state };
|
|
577
585
|
}
|
|
@@ -992,6 +1000,7 @@ export class WorkflowEngine {
|
|
|
992
1000
|
delete state.currentNode;
|
|
993
1001
|
delete state.currentAttemptId;
|
|
994
1002
|
delete state.currentNodeStartedAt;
|
|
1003
|
+
delete state.currentNodeDeadlineAt;
|
|
995
1004
|
delete state.currentSettingsScopeId;
|
|
996
1005
|
delete state.currentSettingsChangeNumber;
|
|
997
1006
|
delete state.currentSettingsHash;
|
|
@@ -1055,6 +1064,7 @@ export class WorkflowEngine {
|
|
|
1055
1064
|
node,
|
|
1056
1065
|
meta,
|
|
1057
1066
|
settings,
|
|
1067
|
+
resumedAttempt?.deadlineAt,
|
|
1058
1068
|
);
|
|
1059
1069
|
return {
|
|
1060
1070
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
@@ -1062,6 +1072,7 @@ export class WorkflowEngine {
|
|
|
1062
1072
|
...(settings !== undefined ? { settings } : {}),
|
|
1063
1073
|
};
|
|
1064
1074
|
} catch (error) {
|
|
1075
|
+
if (isRunParkedError(error) || isClaimLostError(error)) throw error;
|
|
1065
1076
|
const outcome = this.outcomeForError(error);
|
|
1066
1077
|
return {
|
|
1067
1078
|
result: {
|
|
@@ -1121,6 +1132,7 @@ export class WorkflowEngine {
|
|
|
1121
1132
|
node: WorkflowNodeDefinition,
|
|
1122
1133
|
meta: NodeExecutionMeta,
|
|
1123
1134
|
settings?: WorkflowSettingsScopeRecord,
|
|
1135
|
+
persistedDeadlineAt?: string | null,
|
|
1124
1136
|
): Promise<NodeExecution> {
|
|
1125
1137
|
const abort = new AbortController();
|
|
1126
1138
|
const context = this.createNodeContext(state, abort.signal, settings);
|
|
@@ -1137,13 +1149,49 @@ export class WorkflowEngine {
|
|
|
1137
1149
|
throw new CancelledError();
|
|
1138
1150
|
}
|
|
1139
1151
|
|
|
1140
|
-
|
|
1152
|
+
let timeoutMs: number | null;
|
|
1153
|
+
let timeoutErrorMs: number | null;
|
|
1154
|
+
if (persistedDeadlineAt === undefined) {
|
|
1155
|
+
timeoutMs = await this.resolveNodeTimeout(node, context, abort);
|
|
1156
|
+
timeoutErrorMs = timeoutMs;
|
|
1157
|
+
if (node.nodeType === "agent" && this.executor.preservesDeadlineWhileParked === true) {
|
|
1158
|
+
state.currentNodeDeadlineAt =
|
|
1159
|
+
timeoutMs === null ? null : new Date(Date.now() + timeoutMs).toISOString();
|
|
1160
|
+
await this.persist(runId, state, {
|
|
1161
|
+
scope: "node",
|
|
1162
|
+
type: "node_deadline_set",
|
|
1163
|
+
nodeId,
|
|
1164
|
+
attemptId,
|
|
1165
|
+
payload: { deadlineAt: state.currentNodeDeadlineAt },
|
|
1166
|
+
});
|
|
1167
|
+
if (state.currentNodeDeadlineAt !== null && timeoutErrorMs !== null) {
|
|
1168
|
+
timeoutMs = Date.parse(state.currentNodeDeadlineAt) - Date.now();
|
|
1169
|
+
if (timeoutMs <= 0) throw new TimeoutError(timeoutErrorMs);
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
delete state.currentNodeDeadlineAt;
|
|
1173
|
+
}
|
|
1174
|
+
} else if (persistedDeadlineAt === null) {
|
|
1175
|
+
state.currentNodeDeadlineAt = null;
|
|
1176
|
+
timeoutMs = null;
|
|
1177
|
+
timeoutErrorMs = null;
|
|
1178
|
+
} else {
|
|
1179
|
+
const deadline = Date.parse(persistedDeadlineAt);
|
|
1180
|
+
const started = Date.parse(state.currentNodeStartedAt ?? persistedDeadlineAt);
|
|
1181
|
+
if (!Number.isFinite(deadline) || !Number.isFinite(started)) {
|
|
1182
|
+
throw new Error("Persisted node deadline is invalid");
|
|
1183
|
+
}
|
|
1184
|
+
state.currentNodeDeadlineAt = persistedDeadlineAt;
|
|
1185
|
+
timeoutMs = deadline - Date.now();
|
|
1186
|
+
timeoutErrorMs = Math.max(1, deadline - started);
|
|
1187
|
+
if (timeoutMs <= 0) throw new TimeoutError(timeoutErrorMs);
|
|
1188
|
+
}
|
|
1141
1189
|
if (abort.signal.aborted) {
|
|
1142
1190
|
throw abortError(abort.signal);
|
|
1143
1191
|
}
|
|
1144
|
-
if (timeoutMs !== null) {
|
|
1192
|
+
if (timeoutMs !== null && timeoutErrorMs !== null) {
|
|
1145
1193
|
timer = setTimeout(() => {
|
|
1146
|
-
abort.abort(new TimeoutError(
|
|
1194
|
+
abort.abort(new TimeoutError(timeoutErrorMs));
|
|
1147
1195
|
}, timeoutMs);
|
|
1148
1196
|
}
|
|
1149
1197
|
this.activeAttempt = {
|
|
@@ -1323,7 +1371,7 @@ export class WorkflowEngine {
|
|
|
1323
1371
|
}
|
|
1324
1372
|
if (definition === undefined) return undefined;
|
|
1325
1373
|
const invocation = mountPath === "" ? 1 : currentMountInvocation(state, mountPath);
|
|
1326
|
-
const existing = this.store.findSettingsScope(state.runId, mountPath, invocation);
|
|
1374
|
+
const existing = await this.store.findSettingsScope(state.runId, mountPath, invocation);
|
|
1327
1375
|
if (existing !== undefined) return existing;
|
|
1328
1376
|
const input = mountPath === "" ? state.input : currentMountInput(state, mountPath);
|
|
1329
1377
|
const mappedSettings =
|
|
@@ -1331,7 +1379,7 @@ export class WorkflowEngine {
|
|
|
1331
1379
|
const initial = mappedSettings.present
|
|
1332
1380
|
? await parseWorkflowSettingsValue(definition, mappedSettings.value)
|
|
1333
1381
|
: await resolveInitialWorkflowSettings(definition, input);
|
|
1334
|
-
return this.store.ensureSettingsScope({
|
|
1382
|
+
return await this.store.ensureSettingsScope({
|
|
1335
1383
|
runId: state.runId,
|
|
1336
1384
|
mountPath,
|
|
1337
1385
|
invocation,
|
|
@@ -1339,9 +1387,9 @@ export class WorkflowEngine {
|
|
|
1339
1387
|
});
|
|
1340
1388
|
}
|
|
1341
1389
|
|
|
1342
|
-
private persistedSettingsBinding(
|
|
1390
|
+
private async persistedSettingsBinding(
|
|
1343
1391
|
state: WorkflowRunState,
|
|
1344
|
-
): WorkflowSettingsScopeRecord | undefined {
|
|
1392
|
+
): Promise<WorkflowSettingsScopeRecord | undefined> {
|
|
1345
1393
|
if (state.currentSettingsScopeId === undefined) return undefined;
|
|
1346
1394
|
if (
|
|
1347
1395
|
state.currentSettingsChangeNumber === undefined ||
|
|
@@ -1349,7 +1397,7 @@ export class WorkflowEngine {
|
|
|
1349
1397
|
) {
|
|
1350
1398
|
throw new Error("Saved workflow settings binding is incomplete");
|
|
1351
1399
|
}
|
|
1352
|
-
const scope = this.store.getSettingsScopeAtChange(
|
|
1400
|
+
const scope = await this.store.getSettingsScopeAtChange(
|
|
1353
1401
|
state.currentSettingsScopeId,
|
|
1354
1402
|
state.currentSettingsChangeNumber,
|
|
1355
1403
|
);
|
|
@@ -1508,16 +1556,78 @@ export class WorkflowEngine {
|
|
|
1508
1556
|
signal: AbortSignal,
|
|
1509
1557
|
meta: NodeExecutionMeta,
|
|
1510
1558
|
): Promise<NodeExecution> {
|
|
1559
|
+
/* istanbul ignore if -- graph validation rejects action nodes without managed effects */
|
|
1560
|
+
if (node.effect === undefined) {
|
|
1561
|
+
throw new Error(
|
|
1562
|
+
`Action node ${nodeId} must declare a managed effect with an explicit recovery policy`,
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
const effectType = node.effect.type.trim();
|
|
1566
|
+
const idempotencyKey =
|
|
1567
|
+
typeof node.effect.idempotencyKey === "function"
|
|
1568
|
+
? await node.effect.idempotencyKey(context)
|
|
1569
|
+
: node.effect.idempotencyKey;
|
|
1570
|
+
const request =
|
|
1571
|
+
typeof node.effect.request === "function"
|
|
1572
|
+
? await node.effect.request(context)
|
|
1573
|
+
: node.effect.request;
|
|
1574
|
+
/* istanbul ignore if -- definition validation and effect helpers reject empty fields */
|
|
1575
|
+
if (effectType.length === 0 || idempotencyKey.trim().length === 0) {
|
|
1576
|
+
throw new Error("Managed effect type and idempotency key must be nonempty text");
|
|
1577
|
+
}
|
|
1578
|
+
canonicalJson(request);
|
|
1579
|
+
const reservation = await this.store.reserveEffect({
|
|
1580
|
+
runId: context.state.runId,
|
|
1581
|
+
attemptId,
|
|
1582
|
+
effectType,
|
|
1583
|
+
idempotencyKey,
|
|
1584
|
+
request: request as JsonValue,
|
|
1585
|
+
recovery: node.effect.recovery,
|
|
1586
|
+
});
|
|
1587
|
+
if (reservation.disposition === "ambiguous") throw new RunParkedError();
|
|
1588
|
+
if (reservation.disposition === "adopted") {
|
|
1589
|
+
const adopted = reservation.result;
|
|
1590
|
+
if (adopted === null || typeof adopted !== "object" || Array.isArray(adopted)) {
|
|
1591
|
+
throw new Error("Managed effect receipt is invalid");
|
|
1592
|
+
}
|
|
1593
|
+
const execution = adopted as NodeExecution;
|
|
1594
|
+
if (execution.action !== undefined) meta.action = execution.action;
|
|
1595
|
+
return execution;
|
|
1596
|
+
}
|
|
1511
1597
|
const actionContext: WorkflowActionContext = {
|
|
1512
1598
|
...context,
|
|
1599
|
+
effect: { type: effectType, idempotencyKey, recovery: node.effect.recovery },
|
|
1513
1600
|
publishUpdate: async (update) => await this.publishUpdate(nodeId, attemptId, update),
|
|
1514
1601
|
};
|
|
1515
|
-
|
|
1516
|
-
|
|
1602
|
+
try {
|
|
1603
|
+
let execution: NodeExecution;
|
|
1604
|
+
if ("exec" in node) {
|
|
1605
|
+
execution = await runShellActionNode(node, actionContext, signal, meta);
|
|
1606
|
+
} else {
|
|
1607
|
+
meta.action = { actionType: "function" };
|
|
1608
|
+
const output = await node.run(actionContext);
|
|
1609
|
+
execution = { output, promptText: null, action: { actionType: "function" } };
|
|
1610
|
+
}
|
|
1611
|
+
canonicalJson(execution);
|
|
1612
|
+
await this.store.settleEffect({
|
|
1613
|
+
runId: context.state.runId,
|
|
1614
|
+
effectId: reservation.effectId,
|
|
1615
|
+
attemptNumber: reservation.attemptNumber,
|
|
1616
|
+
outcome: "applied",
|
|
1617
|
+
result: execution as JsonValue,
|
|
1618
|
+
});
|
|
1619
|
+
return execution;
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
if (isRunParkedError(error) || isClaimLostError(error)) throw error;
|
|
1622
|
+
await this.store.settleEffect({
|
|
1623
|
+
runId: context.state.runId,
|
|
1624
|
+
effectId: reservation.effectId,
|
|
1625
|
+
attemptNumber: reservation.attemptNumber,
|
|
1626
|
+
outcome: "rejected",
|
|
1627
|
+
error: errorMessage(error),
|
|
1628
|
+
});
|
|
1629
|
+
throw error;
|
|
1517
1630
|
}
|
|
1518
|
-
meta.action = { actionType: "function" };
|
|
1519
|
-
const output = await node.run(actionContext);
|
|
1520
|
-
return { output, promptText: null, action: { actionType: "function" } };
|
|
1521
1631
|
}
|
|
1522
1632
|
|
|
1523
1633
|
private async persist(
|
|
@@ -1532,6 +1642,7 @@ export class WorkflowEngine {
|
|
|
1532
1642
|
// Observers (UI updates, loggers) must never determine workflow
|
|
1533
1643
|
// correctness; a throwing observer would otherwise fail the run.
|
|
1534
1644
|
}
|
|
1645
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1535
1646
|
}
|
|
1536
1647
|
|
|
1537
1648
|
private async finishRun(
|
|
@@ -1543,13 +1654,6 @@ export class WorkflowEngine {
|
|
|
1543
1654
|
if (status === "failed" && state.status === "timed_out") {
|
|
1544
1655
|
status = "timed_out";
|
|
1545
1656
|
}
|
|
1546
|
-
// Let observers (e.g. the session recorder) stop and drain before the
|
|
1547
|
-
// terminal event exists, so the terminal fact is immutable from that point on.
|
|
1548
|
-
try {
|
|
1549
|
-
await this.onRunFinishing?.(runId, state);
|
|
1550
|
-
} catch {
|
|
1551
|
-
// Finishing the run wins over observer failures.
|
|
1552
|
-
}
|
|
1553
1657
|
state.status = status;
|
|
1554
1658
|
state.finishedAt = new Date().toISOString();
|
|
1555
1659
|
if (fields.error !== undefined) {
|
|
@@ -1564,9 +1668,17 @@ export class WorkflowEngine {
|
|
|
1564
1668
|
delete state.currentNode;
|
|
1565
1669
|
delete state.currentAttemptId;
|
|
1566
1670
|
delete state.currentNodeStartedAt;
|
|
1671
|
+
delete state.currentNodeDeadlineAt;
|
|
1567
1672
|
delete state.currentSettingsScopeId;
|
|
1568
1673
|
delete state.currentSettingsChangeNumber;
|
|
1569
1674
|
delete state.currentSettingsHash;
|
|
1675
|
+
// Let observers stop, drain, and prepare terminal delivery from the final
|
|
1676
|
+
// in-memory state before the immutable terminal event is committed.
|
|
1677
|
+
try {
|
|
1678
|
+
await this.onRunFinishing?.(runId, state);
|
|
1679
|
+
} catch {
|
|
1680
|
+
// Finishing the run wins over observer failures.
|
|
1681
|
+
}
|
|
1570
1682
|
await this.persist(runId, state, {
|
|
1571
1683
|
scope: "run",
|
|
1572
1684
|
type: `run_${status}`,
|
|
@@ -1585,7 +1697,7 @@ async function runCheckpointNode(
|
|
|
1585
1697
|
node: CheckpointNodeDefinition,
|
|
1586
1698
|
context: WorkflowNodeContext,
|
|
1587
1699
|
execution: {
|
|
1588
|
-
store:
|
|
1700
|
+
store: WorkflowExecutionStore;
|
|
1589
1701
|
workflowName: string;
|
|
1590
1702
|
nodeId: string;
|
|
1591
1703
|
attemptId: string;
|
|
@@ -1610,9 +1722,7 @@ async function runCheckpointNode(
|
|
|
1610
1722
|
prompt,
|
|
1611
1723
|
...(timeout !== undefined ? { timeout } : {}),
|
|
1612
1724
|
});
|
|
1613
|
-
await
|
|
1614
|
-
state: execution.store.state,
|
|
1615
|
-
}).createRequest(request);
|
|
1725
|
+
await execution.store.createHumanDecisionRequest(request);
|
|
1616
1726
|
return { output: request, promptText: null };
|
|
1617
1727
|
}
|
|
1618
1728
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
@@ -1633,12 +1743,11 @@ function shellReceipt(result: ShellActionResult): WorkflowActionReceipt {
|
|
|
1633
1743
|
|
|
1634
1744
|
async function runShellActionNode(
|
|
1635
1745
|
node: ShellActionNodeDefinition,
|
|
1636
|
-
context: WorkflowNodeContext,
|
|
1637
1746
|
actionContext: WorkflowActionContext,
|
|
1638
1747
|
signal: AbortSignal,
|
|
1639
1748
|
meta: NodeExecutionMeta,
|
|
1640
1749
|
): Promise<NodeExecution> {
|
|
1641
|
-
const spec = await node.exec(
|
|
1750
|
+
const spec = await node.exec(actionContext);
|
|
1642
1751
|
const streams = new Set(node.updates?.streams ?? ["stdout"]);
|
|
1643
1752
|
let result: ShellActionResult;
|
|
1644
1753
|
try {
|
|
@@ -1664,7 +1773,7 @@ async function runShellActionNode(
|
|
|
1664
1773
|
throw error;
|
|
1665
1774
|
}
|
|
1666
1775
|
meta.action = shellReceipt(result);
|
|
1667
|
-
const output = node.parse ? await node.parse(result,
|
|
1776
|
+
const output = node.parse ? await node.parse(result, actionContext) : result;
|
|
1668
1777
|
return { output, promptText: null, action: shellReceipt(result) };
|
|
1669
1778
|
}
|
|
1670
1779
|
|
|
@@ -1882,7 +1991,7 @@ function appendLiveControlInstructions(
|
|
|
1882
1991
|
nodeId: string,
|
|
1883
1992
|
context: WorkflowNodeContext,
|
|
1884
1993
|
): string {
|
|
1885
|
-
const lines = [prompt.trimEnd(), "", "Workflow
|
|
1994
|
+
const lines = [prompt.trimEnd(), "", "Workflow context for this run:"];
|
|
1886
1995
|
if (
|
|
1887
1996
|
context.settingsScopeId !== undefined &&
|
|
1888
1997
|
context.settingsChangeNumber !== undefined &&
|
|
@@ -1906,18 +2015,12 @@ function appendLiveControlInstructions(
|
|
|
1906
2015
|
? [`- Settings purpose: ${definition.description}`]
|
|
1907
2016
|
: []),
|
|
1908
2017
|
`- Current settings: ${summary}`,
|
|
1909
|
-
`-
|
|
1910
|
-
"- Treat the current settings as authoritative for
|
|
1911
|
-
"- To change future workflow settings, use the workflow tool with action change-settings, this scopeId, an RFC 6902 patch, and optionally expectedChangeNumber.",
|
|
2018
|
+
`- Settings permissions: ${allowed || "none"}`,
|
|
2019
|
+
"- Treat the current settings as authoritative for this workflow scope.",
|
|
1912
2020
|
);
|
|
1913
2021
|
} else {
|
|
1914
2022
|
lines.push("- This workflow scope has no editable settings.");
|
|
1915
2023
|
}
|
|
1916
|
-
lines.push(
|
|
1917
|
-
"- To save normal user work for after successful completion, use workflow action queue-follow-up once for each prompt.",
|
|
1918
|
-
"- To remove an unsent prompt that this model queued, use workflow action remove-follow-up with its followUpId.",
|
|
1919
|
-
"- Setting changes affect only future node attempts and future settings routes. They do not change this running attempt or completed work.",
|
|
1920
|
-
);
|
|
1921
2024
|
return lines.join("\n");
|
|
1922
2025
|
}
|
|
1923
2026
|
|