@osolmaz/pi-workflows 0.13.1 → 0.13.3
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 +9 -7
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +69 -61
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +11 -5
- package/dist/builtins/autoplan.workflow.js +68 -32
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +4 -4
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.js +35 -24
- package/dist/builtins/plain-summary.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +25 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
- package/dist/controllers/index.d.ts +2 -2
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.js +88 -61
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/types.d.ts +34 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
- package/dist/controllers/workflow-engine-scheduler.js +83 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +8 -1
- package/dist/controllers/workflows.js +36 -0
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.d.ts +2 -1
- package/dist/extension/controller-host.js +1 -1
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.js +45 -7
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/follow-up-coordinator.d.ts +27 -0
- package/dist/extension/follow-up-coordinator.js +131 -0
- package/dist/extension/follow-up-coordinator.js.map +1 -0
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +462 -76
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +8 -8
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -3
- package/dist/extension/session-events.js +11 -10
- package/dist/extension/session-events.js.map +1 -1
- package/dist/extension/widget.js +9 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/state/database.d.ts +2 -1
- package/dist/state/database.js +11 -13
- package/dist/state/database.js.map +1 -1
- package/dist/state/json.js +6 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/mutation.d.ts +1 -1
- package/dist/state/mutation.js.map +1 -1
- package/dist/state/prune.d.ts +18 -0
- package/dist/state/prune.js +373 -0
- package/dist/state/prune.js.map +1 -0
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +130 -13
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/cli.d.ts +3 -1
- package/dist/viewer/cli.js +55 -4
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +0 -1
- package/dist/viewer/session-reducer.js +2 -24
- package/dist/viewer/session-reducer.js.map +1 -1
- package/dist/workflows/composition.d.ts +2 -0
- package/dist/workflows/composition.js +15 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -3
- package/dist/workflows/definition.js +3 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +303 -36
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +1 -0
- package/dist/workflows/human-decision.js +25 -9
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/json-patch.d.ts +47 -0
- package/dist/workflows/json-patch.js +298 -0
- package/dist/workflows/json-patch.js.map +1 -0
- package/dist/workflows/schema.js +29 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +128 -0
- package/dist/workflows/settings.js +199 -0
- package/dist/workflows/settings.js.map +1 -0
- package/dist/workflows/store.d.ts +81 -3
- package/dist/workflows/store.js +1563 -50
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +22 -0
- package/dist/workflows/tool-input.js +43 -0
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +41 -6
- package/docs/2026-08-25-workflow-follow-ups.md +132 -0
- package/docs/2026-08-25-workflow-settings.md +169 -0
- package/docs/DEFERRED_TURNS.md +6 -0
- package/docs/DESIGN_PHILOSOPHY.md +2 -0
- package/docs/SQLITE_STATE.md +36 -17
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
- package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
- package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
- package/docs/session-event-journal.md +2 -3
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +58 -10
- package/examples/workflows/live-settings.workflow.ts +93 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +4 -4
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +132 -72
- package/src/controllers/types.ts +40 -0
- package/src/controllers/workflow-engine-scheduler.ts +103 -1
- package/src/controllers/workflows.ts +68 -0
- package/src/extension/controller-host.ts +6 -1
- package/src/extension/decision-channels.ts +47 -7
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +540 -89
- package/src/extension/recorder.ts +10 -8
- package/src/extension/session-events.ts +15 -13
- package/src/extension/widget.ts +8 -0
- package/src/state/database.ts +12 -12
- package/src/state/json.ts +6 -1
- package/src/state/mutation.ts +4 -1
- package/src/state/prune.ts +502 -0
- package/src/state/schema.ts +130 -13
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/viewer/session-reducer.ts +2 -29
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +365 -36
- package/src/workflows/human-decision.ts +41 -11
- package/src/workflows/index.ts +43 -0
- package/src/workflows/json-patch.ts +375 -0
- package/src/workflows/schema.ts +31 -1
- package/src/workflows/settings.ts +430 -0
- package/src/workflows/store.ts +2268 -121
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -5
package/src/workflows/engine.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { canonicalJson } from "../state/json.js";
|
|
4
|
+
import { StaleResourceError } from "../state/mutation.js";
|
|
3
5
|
import {
|
|
4
6
|
compileWorkflowDefinition,
|
|
5
7
|
compositionMetadata,
|
|
@@ -22,6 +24,11 @@ import {
|
|
|
22
24
|
validateHumanDecisionResponse,
|
|
23
25
|
} from "./human-decision.js";
|
|
24
26
|
import { extractJsonValue } from "./json.js";
|
|
27
|
+
import {
|
|
28
|
+
parseWorkflowSettingsValue,
|
|
29
|
+
resolveInitialWorkflowSettings,
|
|
30
|
+
type WorkflowSettingsScopeRecord,
|
|
31
|
+
} from "./settings.js";
|
|
25
32
|
import { runShellAction, shellResultFromError } from "./shell.js";
|
|
26
33
|
import {
|
|
27
34
|
RUN_STATE_SCHEMA,
|
|
@@ -67,6 +74,7 @@ const TITLE_TIMEOUT_MS = 30_000;
|
|
|
67
74
|
const TIMEOUT_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
68
75
|
// Covers the shell SIGTERM → SIGKILL escalation (1s) plus stdio close.
|
|
69
76
|
const ABORT_CLEANUP_GRACE_MS = 2_000;
|
|
77
|
+
const MAX_SETTINGS_ROUTE_RETRIES = 16;
|
|
70
78
|
|
|
71
79
|
type NodeExecution = {
|
|
72
80
|
output: unknown;
|
|
@@ -88,6 +96,7 @@ type NodeExecutionMeta = {
|
|
|
88
96
|
type NodeAttempt = {
|
|
89
97
|
result: WorkflowNodeResult;
|
|
90
98
|
execution: NodeExecution | null;
|
|
99
|
+
settings?: WorkflowSettingsScopeRecord;
|
|
91
100
|
error?: unknown;
|
|
92
101
|
};
|
|
93
102
|
|
|
@@ -95,6 +104,7 @@ type ResumedNodeAttempt = {
|
|
|
95
104
|
nodeId: string;
|
|
96
105
|
attemptId: string;
|
|
97
106
|
startedAt: string;
|
|
107
|
+
settings?: WorkflowSettingsScopeRecord;
|
|
98
108
|
};
|
|
99
109
|
|
|
100
110
|
/**
|
|
@@ -119,6 +129,7 @@ export class WorkflowEngine {
|
|
|
119
129
|
state: WorkflowRunState;
|
|
120
130
|
nodeId: string;
|
|
121
131
|
attemptId: string;
|
|
132
|
+
settingsScopeId?: string;
|
|
122
133
|
signal: AbortSignal;
|
|
123
134
|
}
|
|
124
135
|
| undefined;
|
|
@@ -127,6 +138,7 @@ export class WorkflowEngine {
|
|
|
127
138
|
private cancelled = false;
|
|
128
139
|
private parked = false;
|
|
129
140
|
private paused = false;
|
|
141
|
+
private presentationRequired = false;
|
|
130
142
|
private wakePause: (() => void) | null = null;
|
|
131
143
|
|
|
132
144
|
constructor(options: WorkflowEngineOptions) {
|
|
@@ -236,6 +248,10 @@ export class WorkflowEngine {
|
|
|
236
248
|
return this.paused;
|
|
237
249
|
}
|
|
238
250
|
|
|
251
|
+
get activeSettingsScopeId(): string | undefined {
|
|
252
|
+
return this.activeAttempt?.settingsScopeId;
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
async run(
|
|
240
256
|
workflow: WorkflowDefinition,
|
|
241
257
|
input: unknown,
|
|
@@ -243,6 +259,7 @@ export class WorkflowEngine {
|
|
|
243
259
|
): Promise<WorkflowRunResult> {
|
|
244
260
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
245
261
|
validateWorkflowDefinition(workflow);
|
|
262
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
246
263
|
// Fail before any run row exists so bad input cannot leave partial state.
|
|
247
264
|
const suppliedInput = input === undefined ? null : input;
|
|
248
265
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -260,7 +277,18 @@ export class WorkflowEngine {
|
|
|
260
277
|
options.workflowSource,
|
|
261
278
|
options.runId,
|
|
262
279
|
);
|
|
263
|
-
const
|
|
280
|
+
const initialSettings =
|
|
281
|
+
workflow.settings === undefined
|
|
282
|
+
? []
|
|
283
|
+
: [
|
|
284
|
+
{
|
|
285
|
+
mountPath: "",
|
|
286
|
+
invocation: 1,
|
|
287
|
+
settings: (await resolveInitialWorkflowSettings(workflow.settings, normalizedInput))
|
|
288
|
+
.json,
|
|
289
|
+
},
|
|
290
|
+
];
|
|
291
|
+
const runId = await this.store.initializeRun(workflow, state, { initialSettings });
|
|
264
292
|
await this.persist(runId, state, {
|
|
265
293
|
scope: "run",
|
|
266
294
|
type: "run_started",
|
|
@@ -299,6 +327,7 @@ export class WorkflowEngine {
|
|
|
299
327
|
): Promise<WorkflowRunResult> {
|
|
300
328
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
301
329
|
validateWorkflowDefinition(workflow);
|
|
330
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
302
331
|
// Reset before any await: a park or cancel landing during preparation
|
|
303
332
|
// must survive, or a host drain would hang while the run executes.
|
|
304
333
|
this.cancelled = false;
|
|
@@ -314,18 +343,22 @@ export class WorkflowEngine {
|
|
|
314
343
|
const point = this.resumePointFor(workflow, state, "wait");
|
|
315
344
|
const interruptedNode =
|
|
316
345
|
state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
317
|
-
|
|
346
|
+
let resumedAttempt: ResumedNodeAttempt | undefined;
|
|
347
|
+
if (
|
|
318
348
|
state.currentNode !== undefined &&
|
|
319
349
|
state.currentAttemptId !== undefined &&
|
|
320
350
|
state.currentNodeStartedAt !== undefined &&
|
|
321
351
|
interruptedNode?.nodeType === "agent" &&
|
|
322
352
|
assistantMessageConfig(interruptedNode) !== undefined
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
:
|
|
353
|
+
) {
|
|
354
|
+
const resumedSettings = this.persistedSettingsBinding(state);
|
|
355
|
+
resumedAttempt = {
|
|
356
|
+
nodeId: state.currentNode,
|
|
357
|
+
attemptId: state.currentAttemptId,
|
|
358
|
+
startedAt: state.currentNodeStartedAt,
|
|
359
|
+
...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
329
362
|
// A resumed run starts unpaused. Submitted and non-agent nodes discard
|
|
330
363
|
// stale in-flight markers and start a new attempt. Assistant-message
|
|
331
364
|
// nodes keep their attempt id so the origin session can adopt an already
|
|
@@ -335,6 +368,9 @@ export class WorkflowEngine {
|
|
|
335
368
|
delete state.currentNode;
|
|
336
369
|
delete state.currentAttemptId;
|
|
337
370
|
delete state.currentNodeStartedAt;
|
|
371
|
+
delete state.currentSettingsScopeId;
|
|
372
|
+
delete state.currentSettingsChangeNumber;
|
|
373
|
+
delete state.currentSettingsHash;
|
|
338
374
|
delete state.statusDetail;
|
|
339
375
|
}
|
|
340
376
|
await this.persist(runId, state, {
|
|
@@ -407,6 +443,7 @@ export class WorkflowEngine {
|
|
|
407
443
|
): Promise<WorkflowRunResult> {
|
|
408
444
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
409
445
|
validateWorkflowDefinition(workflow);
|
|
446
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
410
447
|
this.cancelled = false;
|
|
411
448
|
this.paused = false;
|
|
412
449
|
this.parked = false;
|
|
@@ -686,6 +723,7 @@ export class WorkflowEngine {
|
|
|
686
723
|
let currentNodeId: string | null = startNodeId;
|
|
687
724
|
let executedSteps = executedStepsBase;
|
|
688
725
|
let lastOutput: unknown = initialLastOutput;
|
|
726
|
+
const settingsRouteRetries = new Map<string, number>();
|
|
689
727
|
|
|
690
728
|
while (currentNodeId !== null) {
|
|
691
729
|
await this.holdWhilePaused(state, runId);
|
|
@@ -722,28 +760,84 @@ export class WorkflowEngine {
|
|
|
722
760
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
723
761
|
throw new RunParkedError();
|
|
724
762
|
}
|
|
763
|
+
const beforeRecord = structuredClone(state);
|
|
725
764
|
this.recordAttempt(workflow, state, attempt);
|
|
726
|
-
// The
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
765
|
+
// The durable step row owns the output. The trace keeps the terminal fact,
|
|
766
|
+
// settings binding, and compact execution metadata.
|
|
767
|
+
try {
|
|
768
|
+
await this.persist(runId, state, {
|
|
769
|
+
scope: "node",
|
|
770
|
+
type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
|
|
771
|
+
nodeId: attempt.result.nodeId,
|
|
772
|
+
attemptId: attempt.result.attemptId,
|
|
773
|
+
payload: {
|
|
774
|
+
outcome: attempt.result.outcome,
|
|
775
|
+
durationMs: attempt.result.durationMs,
|
|
776
|
+
...(attempt.result.outcome === "ok" ? { output: attempt.result.output ?? null } : {}),
|
|
777
|
+
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
778
|
+
...(attempt.execution?.action !== undefined
|
|
779
|
+
? { action: attempt.execution.action }
|
|
780
|
+
: {}),
|
|
781
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
782
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
783
|
+
: {}),
|
|
784
|
+
...(attempt.execution?.conversation !== undefined
|
|
785
|
+
? { conversation: attempt.execution.conversation }
|
|
786
|
+
: {}),
|
|
787
|
+
...(attempt.settings !== undefined
|
|
788
|
+
? {
|
|
789
|
+
settingsScopeId: attempt.settings.scopeId,
|
|
790
|
+
settingsChangeNumber: attempt.settings.changeNumber,
|
|
791
|
+
settingsHash: attempt.settings.settingsHash,
|
|
792
|
+
}
|
|
793
|
+
: {}),
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (
|
|
798
|
+
!(error instanceof StaleResourceError) ||
|
|
799
|
+
node.nodeType !== "compute" ||
|
|
800
|
+
node.settingsRoute !== true ||
|
|
801
|
+
attempt.settings === undefined
|
|
802
|
+
) {
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
restoreRunState(state, beforeRecord);
|
|
806
|
+
const retryCount = (settingsRouteRetries.get(currentNodeId) ?? 0) + 1;
|
|
807
|
+
if (retryCount > MAX_SETTINGS_ROUTE_RETRIES) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
`Settings route ${currentNodeId} changed more than ${MAX_SETTINGS_ROUTE_RETRIES} times before settlement`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
settingsRouteRetries.set(currentNodeId, retryCount);
|
|
813
|
+
const latest = await this.captureSettingsBinding(workflow, state, currentNodeId);
|
|
814
|
+
if (latest === undefined) {
|
|
815
|
+
throw new Error(`Settings route ${currentNodeId} lost its settings scope`);
|
|
816
|
+
}
|
|
817
|
+
state.currentSettingsScopeId = latest.scopeId;
|
|
818
|
+
state.currentSettingsChangeNumber = latest.changeNumber;
|
|
819
|
+
state.currentSettingsHash = latest.settingsHash;
|
|
820
|
+
await this.persist(runId, state, {
|
|
821
|
+
scope: "run",
|
|
822
|
+
type: "settings_route_retried",
|
|
823
|
+
nodeId: currentNodeId,
|
|
824
|
+
attemptId: attempt.result.attemptId,
|
|
825
|
+
payload: {
|
|
826
|
+
scopeId: latest.scopeId,
|
|
827
|
+
previousChangeNumber: attempt.settings.changeNumber,
|
|
828
|
+
changeNumber: latest.changeNumber,
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
resumedAttempt = {
|
|
832
|
+
nodeId: currentNodeId,
|
|
833
|
+
attemptId: attempt.result.attemptId,
|
|
834
|
+
startedAt: attempt.result.startedAt,
|
|
835
|
+
settings: latest,
|
|
836
|
+
};
|
|
837
|
+
executedSteps -= 1;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
settingsRouteRetries.delete(currentNodeId);
|
|
747
841
|
|
|
748
842
|
if (attempt.result.outcome !== "ok") {
|
|
749
843
|
currentNodeId = this.routeAfterFailure(workflow, state, attempt);
|
|
@@ -886,11 +980,21 @@ export class WorkflowEngine {
|
|
|
886
980
|
...(attempt.execution?.conversation !== undefined
|
|
887
981
|
? { conversation: attempt.execution.conversation }
|
|
888
982
|
: {}),
|
|
983
|
+
...(attempt.settings !== undefined
|
|
984
|
+
? {
|
|
985
|
+
settingsScopeId: attempt.settings.scopeId,
|
|
986
|
+
settingsChangeNumber: attempt.settings.changeNumber,
|
|
987
|
+
settingsHash: attempt.settings.settingsHash,
|
|
988
|
+
}
|
|
989
|
+
: {}),
|
|
889
990
|
};
|
|
890
991
|
state.steps.push(step);
|
|
891
992
|
delete state.currentNode;
|
|
892
993
|
delete state.currentAttemptId;
|
|
893
994
|
delete state.currentNodeStartedAt;
|
|
995
|
+
delete state.currentSettingsScopeId;
|
|
996
|
+
delete state.currentSettingsChangeNumber;
|
|
997
|
+
delete state.currentSettingsHash;
|
|
894
998
|
delete state.statusDetail;
|
|
895
999
|
}
|
|
896
1000
|
|
|
@@ -904,9 +1008,20 @@ export class WorkflowEngine {
|
|
|
904
1008
|
): Promise<NodeAttempt> {
|
|
905
1009
|
const attemptId = resumedAttempt?.attemptId ?? randomUUID();
|
|
906
1010
|
const startedAt = resumedAttempt?.startedAt ?? new Date().toISOString();
|
|
1011
|
+
const settings =
|
|
1012
|
+
resumedAttempt?.settings ?? (await this.captureSettingsBinding(workflow, state, nodeId));
|
|
907
1013
|
state.currentNode = nodeId;
|
|
908
1014
|
state.currentAttemptId = attemptId;
|
|
909
1015
|
state.currentNodeStartedAt = startedAt;
|
|
1016
|
+
if (settings !== undefined) {
|
|
1017
|
+
state.currentSettingsScopeId = settings.scopeId;
|
|
1018
|
+
state.currentSettingsChangeNumber = settings.changeNumber;
|
|
1019
|
+
state.currentSettingsHash = settings.settingsHash;
|
|
1020
|
+
} else {
|
|
1021
|
+
delete state.currentSettingsScopeId;
|
|
1022
|
+
delete state.currentSettingsChangeNumber;
|
|
1023
|
+
delete state.currentSettingsHash;
|
|
1024
|
+
}
|
|
910
1025
|
if (node.statusDetail !== undefined) {
|
|
911
1026
|
state.statusDetail = node.statusDetail;
|
|
912
1027
|
}
|
|
@@ -916,7 +1031,16 @@ export class WorkflowEngine {
|
|
|
916
1031
|
type: "node_started",
|
|
917
1032
|
nodeId,
|
|
918
1033
|
attemptId,
|
|
919
|
-
payload: {
|
|
1034
|
+
payload: {
|
|
1035
|
+
nodeType: node.nodeType,
|
|
1036
|
+
...(settings !== undefined
|
|
1037
|
+
? {
|
|
1038
|
+
settingsScopeId: settings.scopeId,
|
|
1039
|
+
settingsChangeNumber: settings.changeNumber,
|
|
1040
|
+
settingsHash: settings.settingsHash,
|
|
1041
|
+
}
|
|
1042
|
+
: {}),
|
|
1043
|
+
},
|
|
920
1044
|
});
|
|
921
1045
|
}
|
|
922
1046
|
|
|
@@ -930,10 +1054,12 @@ export class WorkflowEngine {
|
|
|
930
1054
|
attemptId,
|
|
931
1055
|
node,
|
|
932
1056
|
meta,
|
|
1057
|
+
settings,
|
|
933
1058
|
);
|
|
934
1059
|
return {
|
|
935
1060
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
936
1061
|
execution,
|
|
1062
|
+
...(settings !== undefined ? { settings } : {}),
|
|
937
1063
|
};
|
|
938
1064
|
} catch (error) {
|
|
939
1065
|
const outcome = this.outcomeForError(error);
|
|
@@ -949,6 +1075,7 @@ export class WorkflowEngine {
|
|
|
949
1075
|
promptText: meta.promptText,
|
|
950
1076
|
...(meta.action !== undefined ? { action: meta.action } : {}),
|
|
951
1077
|
},
|
|
1078
|
+
...(settings !== undefined ? { settings } : {}),
|
|
952
1079
|
error,
|
|
953
1080
|
};
|
|
954
1081
|
}
|
|
@@ -993,9 +1120,10 @@ export class WorkflowEngine {
|
|
|
993
1120
|
attemptId: string,
|
|
994
1121
|
node: WorkflowNodeDefinition,
|
|
995
1122
|
meta: NodeExecutionMeta,
|
|
1123
|
+
settings?: WorkflowSettingsScopeRecord,
|
|
996
1124
|
): Promise<NodeExecution> {
|
|
997
1125
|
const abort = new AbortController();
|
|
998
|
-
const context = this.createNodeContext(state, abort.signal);
|
|
1126
|
+
const context = this.createNodeContext(state, abort.signal, settings);
|
|
999
1127
|
let timer: NodeJS.Timeout | undefined;
|
|
1000
1128
|
let dispatchSettled: Promise<void> | undefined;
|
|
1001
1129
|
this.activeAbort = abort;
|
|
@@ -1018,7 +1146,14 @@ export class WorkflowEngine {
|
|
|
1018
1146
|
abort.abort(new TimeoutError(timeoutMs));
|
|
1019
1147
|
}, timeoutMs);
|
|
1020
1148
|
}
|
|
1021
|
-
this.activeAttempt = {
|
|
1149
|
+
this.activeAttempt = {
|
|
1150
|
+
runId,
|
|
1151
|
+
state,
|
|
1152
|
+
nodeId,
|
|
1153
|
+
attemptId,
|
|
1154
|
+
...(settings !== undefined ? { settingsScopeId: settings.scopeId } : {}),
|
|
1155
|
+
signal: abort.signal,
|
|
1156
|
+
};
|
|
1022
1157
|
const dispatched = this.dispatchNode(
|
|
1023
1158
|
workflow,
|
|
1024
1159
|
state,
|
|
@@ -1161,12 +1296,89 @@ export class WorkflowEngine {
|
|
|
1161
1296
|
}
|
|
1162
1297
|
}
|
|
1163
1298
|
|
|
1164
|
-
private
|
|
1299
|
+
private async captureSettingsBinding(
|
|
1300
|
+
workflow: WorkflowDefinition,
|
|
1301
|
+
state: WorkflowRunState,
|
|
1302
|
+
nodeId: string,
|
|
1303
|
+
): Promise<WorkflowSettingsScopeRecord | undefined> {
|
|
1304
|
+
const metadata = compositionMetadata(workflow);
|
|
1305
|
+
let mountPath = "";
|
|
1306
|
+
let definition = workflow.settings;
|
|
1307
|
+
if (metadata !== undefined) {
|
|
1308
|
+
const entry = metadata.entries[nodeId];
|
|
1309
|
+
const exit = metadata.exits[nodeId];
|
|
1310
|
+
const scope =
|
|
1311
|
+
entry !== undefined
|
|
1312
|
+
? metadata.scopes[entry.parentPath]
|
|
1313
|
+
: exit !== undefined
|
|
1314
|
+
? metadata.scopes[exit.mountPath]
|
|
1315
|
+
: Object.values(metadata.scopes).find((candidate) =>
|
|
1316
|
+
Object.values(candidate.authoredNodes).includes(nodeId),
|
|
1317
|
+
);
|
|
1318
|
+
if (scope === undefined) {
|
|
1319
|
+
throw new Error(`Workflow settings scope is missing for node ${nodeId}`);
|
|
1320
|
+
}
|
|
1321
|
+
mountPath = scope.path;
|
|
1322
|
+
definition = scope.settings;
|
|
1323
|
+
}
|
|
1324
|
+
if (definition === undefined) return undefined;
|
|
1325
|
+
const invocation = mountPath === "" ? 1 : currentMountInvocation(state, mountPath);
|
|
1326
|
+
const existing = this.store.findSettingsScope(state.runId, mountPath, invocation);
|
|
1327
|
+
if (existing !== undefined) return existing;
|
|
1328
|
+
const input = mountPath === "" ? state.input : currentMountInput(state, mountPath);
|
|
1329
|
+
const mappedSettings =
|
|
1330
|
+
mountPath === "" ? { present: false as const } : currentMountSettings(state, mountPath);
|
|
1331
|
+
const initial = mappedSettings.present
|
|
1332
|
+
? await parseWorkflowSettingsValue(definition, mappedSettings.value)
|
|
1333
|
+
: await resolveInitialWorkflowSettings(definition, input);
|
|
1334
|
+
return this.store.ensureSettingsScope({
|
|
1335
|
+
runId: state.runId,
|
|
1336
|
+
mountPath,
|
|
1337
|
+
invocation,
|
|
1338
|
+
settings: initial.json,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private persistedSettingsBinding(
|
|
1343
|
+
state: WorkflowRunState,
|
|
1344
|
+
): WorkflowSettingsScopeRecord | undefined {
|
|
1345
|
+
if (state.currentSettingsScopeId === undefined) return undefined;
|
|
1346
|
+
if (
|
|
1347
|
+
state.currentSettingsChangeNumber === undefined ||
|
|
1348
|
+
state.currentSettingsHash === undefined
|
|
1349
|
+
) {
|
|
1350
|
+
throw new Error("Saved workflow settings binding is incomplete");
|
|
1351
|
+
}
|
|
1352
|
+
const scope = this.store.getSettingsScopeAtChange(
|
|
1353
|
+
state.currentSettingsScopeId,
|
|
1354
|
+
state.currentSettingsChangeNumber,
|
|
1355
|
+
);
|
|
1356
|
+
if (scope === undefined) {
|
|
1357
|
+
throw new Error(`Saved workflow settings scope is missing: ${state.currentSettingsScopeId}`);
|
|
1358
|
+
}
|
|
1359
|
+
if (scope.settingsHash !== state.currentSettingsHash) {
|
|
1360
|
+
throw new Error("Saved workflow settings binding does not match its saved change");
|
|
1361
|
+
}
|
|
1362
|
+
return scope;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private createNodeContext(
|
|
1366
|
+
state: WorkflowRunState,
|
|
1367
|
+
signal: AbortSignal,
|
|
1368
|
+
settings?: WorkflowSettingsScopeRecord,
|
|
1369
|
+
): WorkflowNodeContext {
|
|
1165
1370
|
return {
|
|
1166
1371
|
input: state.input,
|
|
1167
1372
|
outputs: state.outputs,
|
|
1168
1373
|
results: state.results,
|
|
1169
1374
|
state,
|
|
1375
|
+
...(settings !== undefined
|
|
1376
|
+
? {
|
|
1377
|
+
settings: deepFreezeJson(structuredClone(settings.settings)),
|
|
1378
|
+
settingsScopeId: settings.scopeId,
|
|
1379
|
+
settingsChangeNumber: settings.changeNumber,
|
|
1380
|
+
}
|
|
1381
|
+
: {}),
|
|
1170
1382
|
signal,
|
|
1171
1383
|
};
|
|
1172
1384
|
}
|
|
@@ -1201,7 +1413,8 @@ export class WorkflowEngine {
|
|
|
1201
1413
|
}
|
|
1202
1414
|
}
|
|
1203
1415
|
|
|
1204
|
-
const
|
|
1416
|
+
const authoredPrompt = await node.prompt(context);
|
|
1417
|
+
const basePrompt = appendLiveControlInstructions(authoredPrompt, workflow, nodeId, context);
|
|
1205
1418
|
if (signal.aborted) {
|
|
1206
1419
|
// The node timed out or the run was cancelled while the async prompt
|
|
1207
1420
|
// builder ran; a late continuation must not write into a run that
|
|
@@ -1351,6 +1564,9 @@ export class WorkflowEngine {
|
|
|
1351
1564
|
delete state.currentNode;
|
|
1352
1565
|
delete state.currentAttemptId;
|
|
1353
1566
|
delete state.currentNodeStartedAt;
|
|
1567
|
+
delete state.currentSettingsScopeId;
|
|
1568
|
+
delete state.currentSettingsChangeNumber;
|
|
1569
|
+
delete state.currentSettingsHash;
|
|
1354
1570
|
await this.persist(runId, state, {
|
|
1355
1571
|
scope: "run",
|
|
1356
1572
|
type: `run_${status}`,
|
|
@@ -1359,6 +1575,7 @@ export class WorkflowEngine {
|
|
|
1359
1575
|
...(fields.error !== undefined ? { error: fields.error } : {}),
|
|
1360
1576
|
...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
|
|
1361
1577
|
...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
|
|
1578
|
+
presentationRequired: status === "completed" && this.presentationRequired,
|
|
1362
1579
|
},
|
|
1363
1580
|
});
|
|
1364
1581
|
}
|
|
@@ -1477,6 +1694,58 @@ function abortRejection(signal: AbortSignal): Promise<never> {
|
|
|
1477
1694
|
});
|
|
1478
1695
|
}
|
|
1479
1696
|
|
|
1697
|
+
function restoreRunState(target: WorkflowRunState, saved: WorkflowRunState): void {
|
|
1698
|
+
for (const key of Object.keys(target) as Array<keyof WorkflowRunState>) {
|
|
1699
|
+
delete target[key];
|
|
1700
|
+
}
|
|
1701
|
+
Object.assign(target, saved);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function currentMountInvocation(state: WorkflowRunState, mountPath: string): number {
|
|
1705
|
+
for (let index = state.steps.length - 1; index >= 0; index -= 1) {
|
|
1706
|
+
const step = state.steps[index];
|
|
1707
|
+
if (step?.nodeId !== mountPath || step.outcome !== "ok") continue;
|
|
1708
|
+
const output = step.output as { invocation?: unknown };
|
|
1709
|
+
if (typeof output.invocation === "number" && Number.isInteger(output.invocation)) {
|
|
1710
|
+
return output.invocation;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
throw new Error(`Workflow include entry is missing for settings scope ${mountPath}`);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function currentMountInput(state: WorkflowRunState, mountPath: string): unknown {
|
|
1717
|
+
for (let index = state.steps.length - 1; index >= 0; index -= 1) {
|
|
1718
|
+
const step = state.steps[index];
|
|
1719
|
+
if (step?.nodeId !== mountPath || step.outcome !== "ok") continue;
|
|
1720
|
+
const output = step.output as { input?: unknown };
|
|
1721
|
+
return output.input ?? null;
|
|
1722
|
+
}
|
|
1723
|
+
throw new Error(`Workflow include input is missing for settings scope ${mountPath}`);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function currentMountSettings(
|
|
1727
|
+
state: WorkflowRunState,
|
|
1728
|
+
mountPath: string,
|
|
1729
|
+
): { present: true; value: unknown } | { present: false } {
|
|
1730
|
+
for (let index = state.steps.length - 1; index >= 0; index -= 1) {
|
|
1731
|
+
const step = state.steps[index];
|
|
1732
|
+
if (step?.nodeId !== mountPath || step.outcome !== "ok") continue;
|
|
1733
|
+
const output = step.output as { settings?: unknown };
|
|
1734
|
+
return Object.hasOwn(output, "settings")
|
|
1735
|
+
? { present: true, value: output.settings }
|
|
1736
|
+
: { present: false };
|
|
1737
|
+
}
|
|
1738
|
+
throw new Error(`Workflow include entry is missing for settings scope ${mountPath}`);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function deepFreezeJson<T>(value: T): T {
|
|
1742
|
+
if (value !== null && typeof value === "object") {
|
|
1743
|
+
Object.freeze(value);
|
|
1744
|
+
for (const item of Object.values(value)) deepFreezeJson(item);
|
|
1745
|
+
}
|
|
1746
|
+
return value;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1480
1749
|
function countExecutableSteps(workflow: WorkflowDefinition, steps: WorkflowStepRecord[]): number {
|
|
1481
1750
|
const metadata = compositionMetadata(workflow);
|
|
1482
1751
|
if (metadata === undefined) return steps.length;
|
|
@@ -1546,7 +1815,7 @@ function workflowSourceMismatch(
|
|
|
1546
1815
|
);
|
|
1547
1816
|
}
|
|
1548
1817
|
|
|
1549
|
-
function workflowIdentityMismatch(
|
|
1818
|
+
export function workflowIdentityMismatch(
|
|
1550
1819
|
state: WorkflowRunState,
|
|
1551
1820
|
workflow: WorkflowDefinition,
|
|
1552
1821
|
source: WorkflowSource | undefined,
|
|
@@ -1561,7 +1830,7 @@ function workflowIdentityMismatch(
|
|
|
1561
1830
|
|
|
1562
1831
|
function definitionDigest(workflow: WorkflowDefinition): string {
|
|
1563
1832
|
return `sha256:${createHash("sha256")
|
|
1564
|
-
.update(
|
|
1833
|
+
.update(canonicalJson(createDefinitionSnapshot(workflow)))
|
|
1565
1834
|
.digest("hex")}`;
|
|
1566
1835
|
}
|
|
1567
1836
|
|
|
@@ -1607,6 +1876,66 @@ function assistantMessageConfig(node: AgentNodeDefinition): AssistantMessageOutp
|
|
|
1607
1876
|
return assistantMessageOutput(node.expectedOutput);
|
|
1608
1877
|
}
|
|
1609
1878
|
|
|
1879
|
+
function appendLiveControlInstructions(
|
|
1880
|
+
prompt: string,
|
|
1881
|
+
workflow: WorkflowDefinition,
|
|
1882
|
+
nodeId: string,
|
|
1883
|
+
context: WorkflowNodeContext,
|
|
1884
|
+
): string {
|
|
1885
|
+
const lines = [prompt.trimEnd(), "", "Workflow changes during this run:"];
|
|
1886
|
+
if (
|
|
1887
|
+
context.settingsScopeId !== undefined &&
|
|
1888
|
+
context.settingsChangeNumber !== undefined &&
|
|
1889
|
+
context.settings !== undefined
|
|
1890
|
+
) {
|
|
1891
|
+
const definition = settingsDefinitionForNode(workflow, nodeId);
|
|
1892
|
+
const value = canonicalJson(context.settings);
|
|
1893
|
+
const summary =
|
|
1894
|
+
value.length <= 8_192 ? value : `${value.slice(0, 8_192)}… [settings truncated]`;
|
|
1895
|
+
const allowed = (definition?.paths ?? [])
|
|
1896
|
+
.flatMap((rule) =>
|
|
1897
|
+
Object.entries(rule.permissions)
|
|
1898
|
+
.filter(([, actors]) => actors?.includes("session") === true)
|
|
1899
|
+
.map(([permission]) => `${permission} ${rule.path || "/"}`),
|
|
1900
|
+
)
|
|
1901
|
+
.join(", ");
|
|
1902
|
+
lines.push(
|
|
1903
|
+
`- Active settings scope: ${context.settingsScopeId}`,
|
|
1904
|
+
`- Current settings change number: ${context.settingsChangeNumber}`,
|
|
1905
|
+
...(definition?.description !== undefined
|
|
1906
|
+
? [`- Settings purpose: ${definition.description}`]
|
|
1907
|
+
: []),
|
|
1908
|
+
`- Current settings: ${summary}`,
|
|
1909
|
+
`- Changes allowed from this model step: ${allowed || "none"}`,
|
|
1910
|
+
"- Treat the current settings as authoritative for future behavior in this workflow scope.",
|
|
1911
|
+
"- To change future workflow settings, use the workflow tool with action change-settings, this scopeId, an RFC 6902 patch, and optionally expectedChangeNumber.",
|
|
1912
|
+
);
|
|
1913
|
+
} else {
|
|
1914
|
+
lines.push("- This workflow scope has no editable settings.");
|
|
1915
|
+
}
|
|
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
|
+
return lines.join("\n");
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function settingsDefinitionForNode(
|
|
1925
|
+
workflow: WorkflowDefinition,
|
|
1926
|
+
nodeId: string,
|
|
1927
|
+
): WorkflowDefinition["settings"] {
|
|
1928
|
+
const metadata = compositionMetadata(workflow);
|
|
1929
|
+
if (metadata === undefined) return workflow.settings;
|
|
1930
|
+
const entry = metadata.entries[nodeId];
|
|
1931
|
+
if (entry !== undefined) return metadata.scopes[entry.parentPath]?.settings;
|
|
1932
|
+
const exit = metadata.exits[nodeId];
|
|
1933
|
+
if (exit !== undefined) return metadata.scopes[exit.mountPath]?.settings;
|
|
1934
|
+
return Object.values(metadata.scopes).find((scope) =>
|
|
1935
|
+
Object.values(scope.authoredNodes).includes(nodeId),
|
|
1936
|
+
)?.settings;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1610
1939
|
/**
|
|
1611
1940
|
* The step contract appended to every agent-node prompt. This is the
|
|
1612
1941
|
* documented standard for how the model completes a workflow step.
|