@osolmaz/pi-workflows 0.13.2 → 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 +5 -3
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +65 -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 +5 -5
- package/dist/builtins/catalog.js +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +23 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- 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 +85 -23
- 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/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 +460 -75
- package/dist/extension/index.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 +116 -18
- 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/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 +301 -35
- 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 +75 -2
- package/dist/workflows/store.js +1349 -89
- 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 +40 -5
- 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 +31 -18
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- 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/tui-viewer.md +7 -6
- package/docs/workflows.md +22 -1
- 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/catalog.ts +1 -1
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +129 -35
- 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/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +538 -88
- 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 +116 -18
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +363 -35
- 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 +1977 -184
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -4
package/src/workflows/engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { canonicalJson } from "../state/json.js";
|
|
4
|
+
import { StaleResourceError } from "../state/mutation.js";
|
|
4
5
|
import {
|
|
5
6
|
compileWorkflowDefinition,
|
|
6
7
|
compositionMetadata,
|
|
@@ -23,6 +24,11 @@ import {
|
|
|
23
24
|
validateHumanDecisionResponse,
|
|
24
25
|
} from "./human-decision.js";
|
|
25
26
|
import { extractJsonValue } from "./json.js";
|
|
27
|
+
import {
|
|
28
|
+
parseWorkflowSettingsValue,
|
|
29
|
+
resolveInitialWorkflowSettings,
|
|
30
|
+
type WorkflowSettingsScopeRecord,
|
|
31
|
+
} from "./settings.js";
|
|
26
32
|
import { runShellAction, shellResultFromError } from "./shell.js";
|
|
27
33
|
import {
|
|
28
34
|
RUN_STATE_SCHEMA,
|
|
@@ -68,6 +74,7 @@ const TITLE_TIMEOUT_MS = 30_000;
|
|
|
68
74
|
const TIMEOUT_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
69
75
|
// Covers the shell SIGTERM → SIGKILL escalation (1s) plus stdio close.
|
|
70
76
|
const ABORT_CLEANUP_GRACE_MS = 2_000;
|
|
77
|
+
const MAX_SETTINGS_ROUTE_RETRIES = 16;
|
|
71
78
|
|
|
72
79
|
type NodeExecution = {
|
|
73
80
|
output: unknown;
|
|
@@ -89,6 +96,7 @@ type NodeExecutionMeta = {
|
|
|
89
96
|
type NodeAttempt = {
|
|
90
97
|
result: WorkflowNodeResult;
|
|
91
98
|
execution: NodeExecution | null;
|
|
99
|
+
settings?: WorkflowSettingsScopeRecord;
|
|
92
100
|
error?: unknown;
|
|
93
101
|
};
|
|
94
102
|
|
|
@@ -96,6 +104,7 @@ type ResumedNodeAttempt = {
|
|
|
96
104
|
nodeId: string;
|
|
97
105
|
attemptId: string;
|
|
98
106
|
startedAt: string;
|
|
107
|
+
settings?: WorkflowSettingsScopeRecord;
|
|
99
108
|
};
|
|
100
109
|
|
|
101
110
|
/**
|
|
@@ -120,6 +129,7 @@ export class WorkflowEngine {
|
|
|
120
129
|
state: WorkflowRunState;
|
|
121
130
|
nodeId: string;
|
|
122
131
|
attemptId: string;
|
|
132
|
+
settingsScopeId?: string;
|
|
123
133
|
signal: AbortSignal;
|
|
124
134
|
}
|
|
125
135
|
| undefined;
|
|
@@ -128,6 +138,7 @@ export class WorkflowEngine {
|
|
|
128
138
|
private cancelled = false;
|
|
129
139
|
private parked = false;
|
|
130
140
|
private paused = false;
|
|
141
|
+
private presentationRequired = false;
|
|
131
142
|
private wakePause: (() => void) | null = null;
|
|
132
143
|
|
|
133
144
|
constructor(options: WorkflowEngineOptions) {
|
|
@@ -237,6 +248,10 @@ export class WorkflowEngine {
|
|
|
237
248
|
return this.paused;
|
|
238
249
|
}
|
|
239
250
|
|
|
251
|
+
get activeSettingsScopeId(): string | undefined {
|
|
252
|
+
return this.activeAttempt?.settingsScopeId;
|
|
253
|
+
}
|
|
254
|
+
|
|
240
255
|
async run(
|
|
241
256
|
workflow: WorkflowDefinition,
|
|
242
257
|
input: unknown,
|
|
@@ -244,6 +259,7 @@ export class WorkflowEngine {
|
|
|
244
259
|
): Promise<WorkflowRunResult> {
|
|
245
260
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
246
261
|
validateWorkflowDefinition(workflow);
|
|
262
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
247
263
|
// Fail before any run row exists so bad input cannot leave partial state.
|
|
248
264
|
const suppliedInput = input === undefined ? null : input;
|
|
249
265
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -261,7 +277,18 @@ export class WorkflowEngine {
|
|
|
261
277
|
options.workflowSource,
|
|
262
278
|
options.runId,
|
|
263
279
|
);
|
|
264
|
-
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 });
|
|
265
292
|
await this.persist(runId, state, {
|
|
266
293
|
scope: "run",
|
|
267
294
|
type: "run_started",
|
|
@@ -300,6 +327,7 @@ export class WorkflowEngine {
|
|
|
300
327
|
): Promise<WorkflowRunResult> {
|
|
301
328
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
302
329
|
validateWorkflowDefinition(workflow);
|
|
330
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
303
331
|
// Reset before any await: a park or cancel landing during preparation
|
|
304
332
|
// must survive, or a host drain would hang while the run executes.
|
|
305
333
|
this.cancelled = false;
|
|
@@ -315,18 +343,22 @@ export class WorkflowEngine {
|
|
|
315
343
|
const point = this.resumePointFor(workflow, state, "wait");
|
|
316
344
|
const interruptedNode =
|
|
317
345
|
state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
318
|
-
|
|
346
|
+
let resumedAttempt: ResumedNodeAttempt | undefined;
|
|
347
|
+
if (
|
|
319
348
|
state.currentNode !== undefined &&
|
|
320
349
|
state.currentAttemptId !== undefined &&
|
|
321
350
|
state.currentNodeStartedAt !== undefined &&
|
|
322
351
|
interruptedNode?.nodeType === "agent" &&
|
|
323
352
|
assistantMessageConfig(interruptedNode) !== undefined
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
:
|
|
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
|
+
}
|
|
330
362
|
// A resumed run starts unpaused. Submitted and non-agent nodes discard
|
|
331
363
|
// stale in-flight markers and start a new attempt. Assistant-message
|
|
332
364
|
// nodes keep their attempt id so the origin session can adopt an already
|
|
@@ -336,6 +368,9 @@ export class WorkflowEngine {
|
|
|
336
368
|
delete state.currentNode;
|
|
337
369
|
delete state.currentAttemptId;
|
|
338
370
|
delete state.currentNodeStartedAt;
|
|
371
|
+
delete state.currentSettingsScopeId;
|
|
372
|
+
delete state.currentSettingsChangeNumber;
|
|
373
|
+
delete state.currentSettingsHash;
|
|
339
374
|
delete state.statusDetail;
|
|
340
375
|
}
|
|
341
376
|
await this.persist(runId, state, {
|
|
@@ -408,6 +443,7 @@ export class WorkflowEngine {
|
|
|
408
443
|
): Promise<WorkflowRunResult> {
|
|
409
444
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
410
445
|
validateWorkflowDefinition(workflow);
|
|
446
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
411
447
|
this.cancelled = false;
|
|
412
448
|
this.paused = false;
|
|
413
449
|
this.parked = false;
|
|
@@ -687,6 +723,7 @@ export class WorkflowEngine {
|
|
|
687
723
|
let currentNodeId: string | null = startNodeId;
|
|
688
724
|
let executedSteps = executedStepsBase;
|
|
689
725
|
let lastOutput: unknown = initialLastOutput;
|
|
726
|
+
const settingsRouteRetries = new Map<string, number>();
|
|
690
727
|
|
|
691
728
|
while (currentNodeId !== null) {
|
|
692
729
|
await this.holdWhilePaused(state, runId);
|
|
@@ -723,28 +760,84 @@ export class WorkflowEngine {
|
|
|
723
760
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
724
761
|
throw new RunParkedError();
|
|
725
762
|
}
|
|
763
|
+
const beforeRecord = structuredClone(state);
|
|
726
764
|
this.recordAttempt(workflow, state, attempt);
|
|
727
|
-
// The durable step row owns the output. The trace keeps the terminal fact
|
|
728
|
-
// and compact execution metadata
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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);
|
|
748
841
|
|
|
749
842
|
if (attempt.result.outcome !== "ok") {
|
|
750
843
|
currentNodeId = this.routeAfterFailure(workflow, state, attempt);
|
|
@@ -887,11 +980,21 @@ export class WorkflowEngine {
|
|
|
887
980
|
...(attempt.execution?.conversation !== undefined
|
|
888
981
|
? { conversation: attempt.execution.conversation }
|
|
889
982
|
: {}),
|
|
983
|
+
...(attempt.settings !== undefined
|
|
984
|
+
? {
|
|
985
|
+
settingsScopeId: attempt.settings.scopeId,
|
|
986
|
+
settingsChangeNumber: attempt.settings.changeNumber,
|
|
987
|
+
settingsHash: attempt.settings.settingsHash,
|
|
988
|
+
}
|
|
989
|
+
: {}),
|
|
890
990
|
};
|
|
891
991
|
state.steps.push(step);
|
|
892
992
|
delete state.currentNode;
|
|
893
993
|
delete state.currentAttemptId;
|
|
894
994
|
delete state.currentNodeStartedAt;
|
|
995
|
+
delete state.currentSettingsScopeId;
|
|
996
|
+
delete state.currentSettingsChangeNumber;
|
|
997
|
+
delete state.currentSettingsHash;
|
|
895
998
|
delete state.statusDetail;
|
|
896
999
|
}
|
|
897
1000
|
|
|
@@ -905,9 +1008,20 @@ export class WorkflowEngine {
|
|
|
905
1008
|
): Promise<NodeAttempt> {
|
|
906
1009
|
const attemptId = resumedAttempt?.attemptId ?? randomUUID();
|
|
907
1010
|
const startedAt = resumedAttempt?.startedAt ?? new Date().toISOString();
|
|
1011
|
+
const settings =
|
|
1012
|
+
resumedAttempt?.settings ?? (await this.captureSettingsBinding(workflow, state, nodeId));
|
|
908
1013
|
state.currentNode = nodeId;
|
|
909
1014
|
state.currentAttemptId = attemptId;
|
|
910
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
|
+
}
|
|
911
1025
|
if (node.statusDetail !== undefined) {
|
|
912
1026
|
state.statusDetail = node.statusDetail;
|
|
913
1027
|
}
|
|
@@ -917,7 +1031,16 @@ export class WorkflowEngine {
|
|
|
917
1031
|
type: "node_started",
|
|
918
1032
|
nodeId,
|
|
919
1033
|
attemptId,
|
|
920
|
-
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
|
+
},
|
|
921
1044
|
});
|
|
922
1045
|
}
|
|
923
1046
|
|
|
@@ -931,10 +1054,12 @@ export class WorkflowEngine {
|
|
|
931
1054
|
attemptId,
|
|
932
1055
|
node,
|
|
933
1056
|
meta,
|
|
1057
|
+
settings,
|
|
934
1058
|
);
|
|
935
1059
|
return {
|
|
936
1060
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
937
1061
|
execution,
|
|
1062
|
+
...(settings !== undefined ? { settings } : {}),
|
|
938
1063
|
};
|
|
939
1064
|
} catch (error) {
|
|
940
1065
|
const outcome = this.outcomeForError(error);
|
|
@@ -950,6 +1075,7 @@ export class WorkflowEngine {
|
|
|
950
1075
|
promptText: meta.promptText,
|
|
951
1076
|
...(meta.action !== undefined ? { action: meta.action } : {}),
|
|
952
1077
|
},
|
|
1078
|
+
...(settings !== undefined ? { settings } : {}),
|
|
953
1079
|
error,
|
|
954
1080
|
};
|
|
955
1081
|
}
|
|
@@ -994,9 +1120,10 @@ export class WorkflowEngine {
|
|
|
994
1120
|
attemptId: string,
|
|
995
1121
|
node: WorkflowNodeDefinition,
|
|
996
1122
|
meta: NodeExecutionMeta,
|
|
1123
|
+
settings?: WorkflowSettingsScopeRecord,
|
|
997
1124
|
): Promise<NodeExecution> {
|
|
998
1125
|
const abort = new AbortController();
|
|
999
|
-
const context = this.createNodeContext(state, abort.signal);
|
|
1126
|
+
const context = this.createNodeContext(state, abort.signal, settings);
|
|
1000
1127
|
let timer: NodeJS.Timeout | undefined;
|
|
1001
1128
|
let dispatchSettled: Promise<void> | undefined;
|
|
1002
1129
|
this.activeAbort = abort;
|
|
@@ -1019,7 +1146,14 @@ export class WorkflowEngine {
|
|
|
1019
1146
|
abort.abort(new TimeoutError(timeoutMs));
|
|
1020
1147
|
}, timeoutMs);
|
|
1021
1148
|
}
|
|
1022
|
-
this.activeAttempt = {
|
|
1149
|
+
this.activeAttempt = {
|
|
1150
|
+
runId,
|
|
1151
|
+
state,
|
|
1152
|
+
nodeId,
|
|
1153
|
+
attemptId,
|
|
1154
|
+
...(settings !== undefined ? { settingsScopeId: settings.scopeId } : {}),
|
|
1155
|
+
signal: abort.signal,
|
|
1156
|
+
};
|
|
1023
1157
|
const dispatched = this.dispatchNode(
|
|
1024
1158
|
workflow,
|
|
1025
1159
|
state,
|
|
@@ -1162,12 +1296,89 @@ export class WorkflowEngine {
|
|
|
1162
1296
|
}
|
|
1163
1297
|
}
|
|
1164
1298
|
|
|
1165
|
-
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 {
|
|
1166
1370
|
return {
|
|
1167
1371
|
input: state.input,
|
|
1168
1372
|
outputs: state.outputs,
|
|
1169
1373
|
results: state.results,
|
|
1170
1374
|
state,
|
|
1375
|
+
...(settings !== undefined
|
|
1376
|
+
? {
|
|
1377
|
+
settings: deepFreezeJson(structuredClone(settings.settings)),
|
|
1378
|
+
settingsScopeId: settings.scopeId,
|
|
1379
|
+
settingsChangeNumber: settings.changeNumber,
|
|
1380
|
+
}
|
|
1381
|
+
: {}),
|
|
1171
1382
|
signal,
|
|
1172
1383
|
};
|
|
1173
1384
|
}
|
|
@@ -1202,7 +1413,8 @@ export class WorkflowEngine {
|
|
|
1202
1413
|
}
|
|
1203
1414
|
}
|
|
1204
1415
|
|
|
1205
|
-
const
|
|
1416
|
+
const authoredPrompt = await node.prompt(context);
|
|
1417
|
+
const basePrompt = appendLiveControlInstructions(authoredPrompt, workflow, nodeId, context);
|
|
1206
1418
|
if (signal.aborted) {
|
|
1207
1419
|
// The node timed out or the run was cancelled while the async prompt
|
|
1208
1420
|
// builder ran; a late continuation must not write into a run that
|
|
@@ -1352,6 +1564,9 @@ export class WorkflowEngine {
|
|
|
1352
1564
|
delete state.currentNode;
|
|
1353
1565
|
delete state.currentAttemptId;
|
|
1354
1566
|
delete state.currentNodeStartedAt;
|
|
1567
|
+
delete state.currentSettingsScopeId;
|
|
1568
|
+
delete state.currentSettingsChangeNumber;
|
|
1569
|
+
delete state.currentSettingsHash;
|
|
1355
1570
|
await this.persist(runId, state, {
|
|
1356
1571
|
scope: "run",
|
|
1357
1572
|
type: `run_${status}`,
|
|
@@ -1360,6 +1575,7 @@ export class WorkflowEngine {
|
|
|
1360
1575
|
...(fields.error !== undefined ? { error: fields.error } : {}),
|
|
1361
1576
|
...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
|
|
1362
1577
|
...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
|
|
1578
|
+
presentationRequired: status === "completed" && this.presentationRequired,
|
|
1363
1579
|
},
|
|
1364
1580
|
});
|
|
1365
1581
|
}
|
|
@@ -1478,6 +1694,58 @@ function abortRejection(signal: AbortSignal): Promise<never> {
|
|
|
1478
1694
|
});
|
|
1479
1695
|
}
|
|
1480
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
|
+
|
|
1481
1749
|
function countExecutableSteps(workflow: WorkflowDefinition, steps: WorkflowStepRecord[]): number {
|
|
1482
1750
|
const metadata = compositionMetadata(workflow);
|
|
1483
1751
|
if (metadata === undefined) return steps.length;
|
|
@@ -1547,7 +1815,7 @@ function workflowSourceMismatch(
|
|
|
1547
1815
|
);
|
|
1548
1816
|
}
|
|
1549
1817
|
|
|
1550
|
-
function workflowIdentityMismatch(
|
|
1818
|
+
export function workflowIdentityMismatch(
|
|
1551
1819
|
state: WorkflowRunState,
|
|
1552
1820
|
workflow: WorkflowDefinition,
|
|
1553
1821
|
source: WorkflowSource | undefined,
|
|
@@ -1608,6 +1876,66 @@ function assistantMessageConfig(node: AgentNodeDefinition): AssistantMessageOutp
|
|
|
1608
1876
|
return assistantMessageOutput(node.expectedOutput);
|
|
1609
1877
|
}
|
|
1610
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
|
+
|
|
1611
1939
|
/**
|
|
1612
1940
|
* The step contract appended to every agent-node prompt. This is the
|
|
1613
1941
|
* documented standard for how the model completes a workflow step.
|