@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/dist/workflows/engine.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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 { compileWorkflowDefinition, compositionMetadata, isCompiledWorkflow, } from "./composition.js";
|
|
5
6
|
import { CancelledError, errorMessage, isAbortLikeError, isClaimLostError, isRunParkedError, RunParkedError, TimeoutError, WorkflowSourceChangedError, } from "./errors.js";
|
|
6
7
|
import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
7
8
|
import { HumanDecisionStore, createHumanDecisionRequest, validateHumanDecisionResponse, } from "./human-decision.js";
|
|
8
9
|
import { extractJsonValue } from "./json.js";
|
|
10
|
+
import { parseWorkflowSettingsValue, resolveInitialWorkflowSettings, } from "./settings.js";
|
|
9
11
|
import { runShellAction, shellResultFromError } from "./shell.js";
|
|
10
12
|
import { RUN_STATE_SCHEMA, WorkflowRunStore, createDefinitionSnapshot, createRunId, } from "./store.js";
|
|
11
13
|
import { UpdateRateLimiter, updateReceipt, validateWorkflowUpdate } from "./updates.js";
|
|
@@ -15,6 +17,7 @@ const TITLE_TIMEOUT_MS = 30_000;
|
|
|
15
17
|
const TIMEOUT_RESOLUTION_TIMEOUT_MS = 30_000;
|
|
16
18
|
// Covers the shell SIGTERM → SIGKILL escalation (1s) plus stdio close.
|
|
17
19
|
const ABORT_CLEANUP_GRACE_MS = 2_000;
|
|
20
|
+
const MAX_SETTINGS_ROUTE_RETRIES = 16;
|
|
18
21
|
/**
|
|
19
22
|
* Executes a workflow graph step by step. Agent steps are delegated to the
|
|
20
23
|
* configured executor; compute/action/checkpoint nodes run inline. Every
|
|
@@ -37,6 +40,7 @@ export class WorkflowEngine {
|
|
|
37
40
|
cancelled = false;
|
|
38
41
|
parked = false;
|
|
39
42
|
paused = false;
|
|
43
|
+
presentationRequired = false;
|
|
40
44
|
wakePause = null;
|
|
41
45
|
constructor(options) {
|
|
42
46
|
this.executor = options.executor;
|
|
@@ -123,9 +127,13 @@ export class WorkflowEngine {
|
|
|
123
127
|
get pauseRequested() {
|
|
124
128
|
return this.paused;
|
|
125
129
|
}
|
|
130
|
+
get activeSettingsScopeId() {
|
|
131
|
+
return this.activeAttempt?.settingsScopeId;
|
|
132
|
+
}
|
|
126
133
|
async run(workflow, input, options = {}) {
|
|
127
134
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
128
135
|
validateWorkflowDefinition(workflow);
|
|
136
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
129
137
|
// Fail before any run row exists so bad input cannot leave partial state.
|
|
130
138
|
const suppliedInput = input === undefined ? null : input;
|
|
131
139
|
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
@@ -137,7 +145,17 @@ export class WorkflowEngine {
|
|
|
137
145
|
this.paused = false;
|
|
138
146
|
this.parked = false;
|
|
139
147
|
const state = await this.createRunState(workflow, normalizedInput, options.workflowSource, options.runId);
|
|
140
|
-
const
|
|
148
|
+
const initialSettings = workflow.settings === undefined
|
|
149
|
+
? []
|
|
150
|
+
: [
|
|
151
|
+
{
|
|
152
|
+
mountPath: "",
|
|
153
|
+
invocation: 1,
|
|
154
|
+
settings: (await resolveInitialWorkflowSettings(workflow.settings, normalizedInput))
|
|
155
|
+
.json,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
const runId = await this.store.initializeRun(workflow, state, { initialSettings });
|
|
141
159
|
await this.persist(runId, state, {
|
|
142
160
|
scope: "run",
|
|
143
161
|
type: "run_started",
|
|
@@ -171,6 +189,7 @@ export class WorkflowEngine {
|
|
|
171
189
|
async resumeRun(workflow, runId, options = {}) {
|
|
172
190
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
173
191
|
validateWorkflowDefinition(workflow);
|
|
192
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
174
193
|
// Reset before any await: a park or cancel landing during preparation
|
|
175
194
|
// must survive, or a host drain would hang while the run executes.
|
|
176
195
|
this.cancelled = false;
|
|
@@ -184,17 +203,20 @@ export class WorkflowEngine {
|
|
|
184
203
|
}
|
|
185
204
|
const point = this.resumePointFor(workflow, state, "wait");
|
|
186
205
|
const interruptedNode = state.currentNode !== undefined ? workflow.nodes[state.currentNode] : undefined;
|
|
187
|
-
|
|
206
|
+
let resumedAttempt;
|
|
207
|
+
if (state.currentNode !== undefined &&
|
|
188
208
|
state.currentAttemptId !== undefined &&
|
|
189
209
|
state.currentNodeStartedAt !== undefined &&
|
|
190
210
|
interruptedNode?.nodeType === "agent" &&
|
|
191
|
-
assistantMessageConfig(interruptedNode) !== undefined
|
|
192
|
-
|
|
211
|
+
assistantMessageConfig(interruptedNode) !== undefined) {
|
|
212
|
+
const resumedSettings = this.persistedSettingsBinding(state);
|
|
213
|
+
resumedAttempt = {
|
|
193
214
|
nodeId: state.currentNode,
|
|
194
215
|
attemptId: state.currentAttemptId,
|
|
195
216
|
startedAt: state.currentNodeStartedAt,
|
|
196
|
-
|
|
197
|
-
|
|
217
|
+
...(resumedSettings !== undefined ? { settings: resumedSettings } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
198
220
|
// A resumed run starts unpaused. Submitted and non-agent nodes discard
|
|
199
221
|
// stale in-flight markers and start a new attempt. Assistant-message
|
|
200
222
|
// nodes keep their attempt id so the origin session can adopt an already
|
|
@@ -204,6 +226,9 @@ export class WorkflowEngine {
|
|
|
204
226
|
delete state.currentNode;
|
|
205
227
|
delete state.currentAttemptId;
|
|
206
228
|
delete state.currentNodeStartedAt;
|
|
229
|
+
delete state.currentSettingsScopeId;
|
|
230
|
+
delete state.currentSettingsChangeNumber;
|
|
231
|
+
delete state.currentSettingsHash;
|
|
207
232
|
delete state.statusDetail;
|
|
208
233
|
}
|
|
209
234
|
await this.persist(runId, state, {
|
|
@@ -258,6 +283,7 @@ export class WorkflowEngine {
|
|
|
258
283
|
async continueRun(workflow, parentRunId, input, options = {}) {
|
|
259
284
|
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
260
285
|
validateWorkflowDefinition(workflow);
|
|
286
|
+
this.presentationRequired = workflow.presentationPrompt !== undefined;
|
|
261
287
|
this.cancelled = false;
|
|
262
288
|
this.paused = false;
|
|
263
289
|
this.parked = false;
|
|
@@ -481,6 +507,7 @@ export class WorkflowEngine {
|
|
|
481
507
|
let currentNodeId = startNodeId;
|
|
482
508
|
let executedSteps = executedStepsBase;
|
|
483
509
|
let lastOutput = initialLastOutput;
|
|
510
|
+
const settingsRouteRetries = new Map();
|
|
484
511
|
while (currentNodeId !== null) {
|
|
485
512
|
await this.holdWhilePaused(state, runId);
|
|
486
513
|
const isTransition = composition?.entries[currentNodeId] !== undefined ||
|
|
@@ -504,28 +531,81 @@ export class WorkflowEngine {
|
|
|
504
531
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
505
532
|
throw new RunParkedError();
|
|
506
533
|
}
|
|
534
|
+
const beforeRecord = structuredClone(state);
|
|
507
535
|
this.recordAttempt(workflow, state, attempt);
|
|
508
|
-
// The durable step row owns the output. The trace keeps the terminal fact
|
|
509
|
-
// and compact execution metadata
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
536
|
+
// The durable step row owns the output. The trace keeps the terminal fact,
|
|
537
|
+
// settings binding, and compact execution metadata.
|
|
538
|
+
try {
|
|
539
|
+
await this.persist(runId, state, {
|
|
540
|
+
scope: "node",
|
|
541
|
+
type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
|
|
542
|
+
nodeId: attempt.result.nodeId,
|
|
543
|
+
attemptId: attempt.result.attemptId,
|
|
544
|
+
payload: {
|
|
545
|
+
outcome: attempt.result.outcome,
|
|
546
|
+
durationMs: attempt.result.durationMs,
|
|
547
|
+
...(attempt.result.outcome === "ok" ? { output: attempt.result.output ?? null } : {}),
|
|
548
|
+
...(attempt.result.error !== undefined ? { error: attempt.result.error } : {}),
|
|
549
|
+
...(attempt.execution?.action !== undefined
|
|
550
|
+
? { action: attempt.execution.action }
|
|
551
|
+
: {}),
|
|
552
|
+
...(attempt.execution?.assistantMessage !== undefined
|
|
553
|
+
? { assistantMessage: attempt.execution.assistantMessage }
|
|
554
|
+
: {}),
|
|
555
|
+
...(attempt.execution?.conversation !== undefined
|
|
556
|
+
? { conversation: attempt.execution.conversation }
|
|
557
|
+
: {}),
|
|
558
|
+
...(attempt.settings !== undefined
|
|
559
|
+
? {
|
|
560
|
+
settingsScopeId: attempt.settings.scopeId,
|
|
561
|
+
settingsChangeNumber: attempt.settings.changeNumber,
|
|
562
|
+
settingsHash: attempt.settings.settingsHash,
|
|
563
|
+
}
|
|
564
|
+
: {}),
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
if (!(error instanceof StaleResourceError) ||
|
|
570
|
+
node.nodeType !== "compute" ||
|
|
571
|
+
node.settingsRoute !== true ||
|
|
572
|
+
attempt.settings === undefined) {
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
restoreRunState(state, beforeRecord);
|
|
576
|
+
const retryCount = (settingsRouteRetries.get(currentNodeId) ?? 0) + 1;
|
|
577
|
+
if (retryCount > MAX_SETTINGS_ROUTE_RETRIES) {
|
|
578
|
+
throw new Error(`Settings route ${currentNodeId} changed more than ${MAX_SETTINGS_ROUTE_RETRIES} times before settlement`);
|
|
579
|
+
}
|
|
580
|
+
settingsRouteRetries.set(currentNodeId, retryCount);
|
|
581
|
+
const latest = await this.captureSettingsBinding(workflow, state, currentNodeId);
|
|
582
|
+
if (latest === undefined) {
|
|
583
|
+
throw new Error(`Settings route ${currentNodeId} lost its settings scope`);
|
|
584
|
+
}
|
|
585
|
+
state.currentSettingsScopeId = latest.scopeId;
|
|
586
|
+
state.currentSettingsChangeNumber = latest.changeNumber;
|
|
587
|
+
state.currentSettingsHash = latest.settingsHash;
|
|
588
|
+
await this.persist(runId, state, {
|
|
589
|
+
scope: "run",
|
|
590
|
+
type: "settings_route_retried",
|
|
591
|
+
nodeId: currentNodeId,
|
|
592
|
+
attemptId: attempt.result.attemptId,
|
|
593
|
+
payload: {
|
|
594
|
+
scopeId: latest.scopeId,
|
|
595
|
+
previousChangeNumber: attempt.settings.changeNumber,
|
|
596
|
+
changeNumber: latest.changeNumber,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
resumedAttempt = {
|
|
600
|
+
nodeId: currentNodeId,
|
|
601
|
+
attemptId: attempt.result.attemptId,
|
|
602
|
+
startedAt: attempt.result.startedAt,
|
|
603
|
+
settings: latest,
|
|
604
|
+
};
|
|
605
|
+
executedSteps -= 1;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
settingsRouteRetries.delete(currentNodeId);
|
|
529
609
|
if (attempt.result.outcome !== "ok") {
|
|
530
610
|
currentNodeId = this.routeAfterFailure(workflow, state, attempt);
|
|
531
611
|
continue;
|
|
@@ -649,19 +729,40 @@ export class WorkflowEngine {
|
|
|
649
729
|
...(attempt.execution?.conversation !== undefined
|
|
650
730
|
? { conversation: attempt.execution.conversation }
|
|
651
731
|
: {}),
|
|
732
|
+
...(attempt.settings !== undefined
|
|
733
|
+
? {
|
|
734
|
+
settingsScopeId: attempt.settings.scopeId,
|
|
735
|
+
settingsChangeNumber: attempt.settings.changeNumber,
|
|
736
|
+
settingsHash: attempt.settings.settingsHash,
|
|
737
|
+
}
|
|
738
|
+
: {}),
|
|
652
739
|
};
|
|
653
740
|
state.steps.push(step);
|
|
654
741
|
delete state.currentNode;
|
|
655
742
|
delete state.currentAttemptId;
|
|
656
743
|
delete state.currentNodeStartedAt;
|
|
744
|
+
delete state.currentSettingsScopeId;
|
|
745
|
+
delete state.currentSettingsChangeNumber;
|
|
746
|
+
delete state.currentSettingsHash;
|
|
657
747
|
delete state.statusDetail;
|
|
658
748
|
}
|
|
659
749
|
async executeNode(workflow, state, runId, nodeId, node, resumedAttempt) {
|
|
660
750
|
const attemptId = resumedAttempt?.attemptId ?? randomUUID();
|
|
661
751
|
const startedAt = resumedAttempt?.startedAt ?? new Date().toISOString();
|
|
752
|
+
const settings = resumedAttempt?.settings ?? (await this.captureSettingsBinding(workflow, state, nodeId));
|
|
662
753
|
state.currentNode = nodeId;
|
|
663
754
|
state.currentAttemptId = attemptId;
|
|
664
755
|
state.currentNodeStartedAt = startedAt;
|
|
756
|
+
if (settings !== undefined) {
|
|
757
|
+
state.currentSettingsScopeId = settings.scopeId;
|
|
758
|
+
state.currentSettingsChangeNumber = settings.changeNumber;
|
|
759
|
+
state.currentSettingsHash = settings.settingsHash;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
delete state.currentSettingsScopeId;
|
|
763
|
+
delete state.currentSettingsChangeNumber;
|
|
764
|
+
delete state.currentSettingsHash;
|
|
765
|
+
}
|
|
665
766
|
if (node.statusDetail !== undefined) {
|
|
666
767
|
state.statusDetail = node.statusDetail;
|
|
667
768
|
}
|
|
@@ -671,15 +772,25 @@ export class WorkflowEngine {
|
|
|
671
772
|
type: "node_started",
|
|
672
773
|
nodeId,
|
|
673
774
|
attemptId,
|
|
674
|
-
payload: {
|
|
775
|
+
payload: {
|
|
776
|
+
nodeType: node.nodeType,
|
|
777
|
+
...(settings !== undefined
|
|
778
|
+
? {
|
|
779
|
+
settingsScopeId: settings.scopeId,
|
|
780
|
+
settingsChangeNumber: settings.changeNumber,
|
|
781
|
+
settingsHash: settings.settingsHash,
|
|
782
|
+
}
|
|
783
|
+
: {}),
|
|
784
|
+
},
|
|
675
785
|
});
|
|
676
786
|
}
|
|
677
787
|
const meta = { promptText: null };
|
|
678
788
|
try {
|
|
679
|
-
const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta);
|
|
789
|
+
const execution = await this.runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings);
|
|
680
790
|
return {
|
|
681
791
|
result: this.createNodeResult(nodeId, node, attemptId, startedAt, "ok", execution.output),
|
|
682
792
|
execution,
|
|
793
|
+
...(settings !== undefined ? { settings } : {}),
|
|
683
794
|
};
|
|
684
795
|
}
|
|
685
796
|
catch (error) {
|
|
@@ -696,6 +807,7 @@ export class WorkflowEngine {
|
|
|
696
807
|
promptText: meta.promptText,
|
|
697
808
|
...(meta.action !== undefined ? { action: meta.action } : {}),
|
|
698
809
|
},
|
|
810
|
+
...(settings !== undefined ? { settings } : {}),
|
|
699
811
|
error,
|
|
700
812
|
};
|
|
701
813
|
}
|
|
@@ -722,9 +834,9 @@ export class WorkflowEngine {
|
|
|
722
834
|
...(output !== undefined ? { output } : {}),
|
|
723
835
|
};
|
|
724
836
|
}
|
|
725
|
-
async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta) {
|
|
837
|
+
async runNodeWithTimeout(workflow, state, runId, nodeId, attemptId, node, meta, settings) {
|
|
726
838
|
const abort = new AbortController();
|
|
727
|
-
const context = this.createNodeContext(state, abort.signal);
|
|
839
|
+
const context = this.createNodeContext(state, abort.signal, settings);
|
|
728
840
|
let timer;
|
|
729
841
|
let dispatchSettled;
|
|
730
842
|
this.activeAbort = abort;
|
|
@@ -746,7 +858,14 @@ export class WorkflowEngine {
|
|
|
746
858
|
abort.abort(new TimeoutError(timeoutMs));
|
|
747
859
|
}, timeoutMs);
|
|
748
860
|
}
|
|
749
|
-
this.activeAttempt = {
|
|
861
|
+
this.activeAttempt = {
|
|
862
|
+
runId,
|
|
863
|
+
state,
|
|
864
|
+
nodeId,
|
|
865
|
+
attemptId,
|
|
866
|
+
...(settings !== undefined ? { settingsScopeId: settings.scopeId } : {}),
|
|
867
|
+
signal: abort.signal,
|
|
868
|
+
};
|
|
750
869
|
const dispatched = this.dispatchNode(workflow, state, runId, nodeId, attemptId, node, context, abort.signal, meta);
|
|
751
870
|
dispatchSettled = dispatched.then(() => undefined, () => undefined);
|
|
752
871
|
// Race the dispatch against the abort signal so timeouts and cancel
|
|
@@ -848,12 +967,71 @@ export class WorkflowEngine {
|
|
|
848
967
|
});
|
|
849
968
|
}
|
|
850
969
|
}
|
|
851
|
-
|
|
970
|
+
async captureSettingsBinding(workflow, state, nodeId) {
|
|
971
|
+
const metadata = compositionMetadata(workflow);
|
|
972
|
+
let mountPath = "";
|
|
973
|
+
let definition = workflow.settings;
|
|
974
|
+
if (metadata !== undefined) {
|
|
975
|
+
const entry = metadata.entries[nodeId];
|
|
976
|
+
const exit = metadata.exits[nodeId];
|
|
977
|
+
const scope = entry !== undefined
|
|
978
|
+
? metadata.scopes[entry.parentPath]
|
|
979
|
+
: exit !== undefined
|
|
980
|
+
? metadata.scopes[exit.mountPath]
|
|
981
|
+
: Object.values(metadata.scopes).find((candidate) => Object.values(candidate.authoredNodes).includes(nodeId));
|
|
982
|
+
if (scope === undefined) {
|
|
983
|
+
throw new Error(`Workflow settings scope is missing for node ${nodeId}`);
|
|
984
|
+
}
|
|
985
|
+
mountPath = scope.path;
|
|
986
|
+
definition = scope.settings;
|
|
987
|
+
}
|
|
988
|
+
if (definition === undefined)
|
|
989
|
+
return undefined;
|
|
990
|
+
const invocation = mountPath === "" ? 1 : currentMountInvocation(state, mountPath);
|
|
991
|
+
const existing = this.store.findSettingsScope(state.runId, mountPath, invocation);
|
|
992
|
+
if (existing !== undefined)
|
|
993
|
+
return existing;
|
|
994
|
+
const input = mountPath === "" ? state.input : currentMountInput(state, mountPath);
|
|
995
|
+
const mappedSettings = mountPath === "" ? { present: false } : currentMountSettings(state, mountPath);
|
|
996
|
+
const initial = mappedSettings.present
|
|
997
|
+
? await parseWorkflowSettingsValue(definition, mappedSettings.value)
|
|
998
|
+
: await resolveInitialWorkflowSettings(definition, input);
|
|
999
|
+
return this.store.ensureSettingsScope({
|
|
1000
|
+
runId: state.runId,
|
|
1001
|
+
mountPath,
|
|
1002
|
+
invocation,
|
|
1003
|
+
settings: initial.json,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
persistedSettingsBinding(state) {
|
|
1007
|
+
if (state.currentSettingsScopeId === undefined)
|
|
1008
|
+
return undefined;
|
|
1009
|
+
if (state.currentSettingsChangeNumber === undefined ||
|
|
1010
|
+
state.currentSettingsHash === undefined) {
|
|
1011
|
+
throw new Error("Saved workflow settings binding is incomplete");
|
|
1012
|
+
}
|
|
1013
|
+
const scope = this.store.getSettingsScopeAtChange(state.currentSettingsScopeId, state.currentSettingsChangeNumber);
|
|
1014
|
+
if (scope === undefined) {
|
|
1015
|
+
throw new Error(`Saved workflow settings scope is missing: ${state.currentSettingsScopeId}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (scope.settingsHash !== state.currentSettingsHash) {
|
|
1018
|
+
throw new Error("Saved workflow settings binding does not match its saved change");
|
|
1019
|
+
}
|
|
1020
|
+
return scope;
|
|
1021
|
+
}
|
|
1022
|
+
createNodeContext(state, signal, settings) {
|
|
852
1023
|
return {
|
|
853
1024
|
input: state.input,
|
|
854
1025
|
outputs: state.outputs,
|
|
855
1026
|
results: state.results,
|
|
856
1027
|
state,
|
|
1028
|
+
...(settings !== undefined
|
|
1029
|
+
? {
|
|
1030
|
+
settings: deepFreezeJson(structuredClone(settings.settings)),
|
|
1031
|
+
settingsScopeId: settings.scopeId,
|
|
1032
|
+
settingsChangeNumber: settings.changeNumber,
|
|
1033
|
+
}
|
|
1034
|
+
: {}),
|
|
857
1035
|
signal,
|
|
858
1036
|
};
|
|
859
1037
|
}
|
|
@@ -876,7 +1054,8 @@ export class WorkflowEngine {
|
|
|
876
1054
|
throw new Error("Assistant completion requires an origin Pi session");
|
|
877
1055
|
}
|
|
878
1056
|
}
|
|
879
|
-
const
|
|
1057
|
+
const authoredPrompt = await node.prompt(context);
|
|
1058
|
+
const basePrompt = appendLiveControlInstructions(authoredPrompt, workflow, nodeId, context);
|
|
880
1059
|
if (signal.aborted) {
|
|
881
1060
|
// The node timed out or the run was cancelled while the async prompt
|
|
882
1061
|
// builder ran; a late continuation must not write into a run that
|
|
@@ -993,6 +1172,9 @@ export class WorkflowEngine {
|
|
|
993
1172
|
delete state.currentNode;
|
|
994
1173
|
delete state.currentAttemptId;
|
|
995
1174
|
delete state.currentNodeStartedAt;
|
|
1175
|
+
delete state.currentSettingsScopeId;
|
|
1176
|
+
delete state.currentSettingsChangeNumber;
|
|
1177
|
+
delete state.currentSettingsHash;
|
|
996
1178
|
await this.persist(runId, state, {
|
|
997
1179
|
scope: "run",
|
|
998
1180
|
type: `run_${status}`,
|
|
@@ -1001,6 +1183,7 @@ export class WorkflowEngine {
|
|
|
1001
1183
|
...(fields.error !== undefined ? { error: fields.error } : {}),
|
|
1002
1184
|
...(fields.waitingOn !== undefined ? { waitingOn: fields.waitingOn } : {}),
|
|
1003
1185
|
...(fields.finalOutput !== undefined ? { finalOutput: fields.finalOutput } : {}),
|
|
1186
|
+
presentationRequired: status === "completed" && this.presentationRequired,
|
|
1004
1187
|
},
|
|
1005
1188
|
});
|
|
1006
1189
|
}
|
|
@@ -1094,6 +1277,54 @@ function abortRejection(signal) {
|
|
|
1094
1277
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1095
1278
|
});
|
|
1096
1279
|
}
|
|
1280
|
+
function restoreRunState(target, saved) {
|
|
1281
|
+
for (const key of Object.keys(target)) {
|
|
1282
|
+
delete target[key];
|
|
1283
|
+
}
|
|
1284
|
+
Object.assign(target, saved);
|
|
1285
|
+
}
|
|
1286
|
+
function currentMountInvocation(state, mountPath) {
|
|
1287
|
+
for (let index = state.steps.length - 1; index >= 0; index -= 1) {
|
|
1288
|
+
const step = state.steps[index];
|
|
1289
|
+
if (step?.nodeId !== mountPath || step.outcome !== "ok")
|
|
1290
|
+
continue;
|
|
1291
|
+
const output = step.output;
|
|
1292
|
+
if (typeof output.invocation === "number" && Number.isInteger(output.invocation)) {
|
|
1293
|
+
return output.invocation;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
throw new Error(`Workflow include entry is missing for settings scope ${mountPath}`);
|
|
1297
|
+
}
|
|
1298
|
+
function currentMountInput(state, mountPath) {
|
|
1299
|
+
for (let index = state.steps.length - 1; index >= 0; index -= 1) {
|
|
1300
|
+
const step = state.steps[index];
|
|
1301
|
+
if (step?.nodeId !== mountPath || step.outcome !== "ok")
|
|
1302
|
+
continue;
|
|
1303
|
+
const output = step.output;
|
|
1304
|
+
return output.input ?? null;
|
|
1305
|
+
}
|
|
1306
|
+
throw new Error(`Workflow include input is missing for settings scope ${mountPath}`);
|
|
1307
|
+
}
|
|
1308
|
+
function currentMountSettings(state, mountPath) {
|
|
1309
|
+
for (let index = state.steps.length - 1; index >= 0; index -= 1) {
|
|
1310
|
+
const step = state.steps[index];
|
|
1311
|
+
if (step?.nodeId !== mountPath || step.outcome !== "ok")
|
|
1312
|
+
continue;
|
|
1313
|
+
const output = step.output;
|
|
1314
|
+
return Object.hasOwn(output, "settings")
|
|
1315
|
+
? { present: true, value: output.settings }
|
|
1316
|
+
: { present: false };
|
|
1317
|
+
}
|
|
1318
|
+
throw new Error(`Workflow include entry is missing for settings scope ${mountPath}`);
|
|
1319
|
+
}
|
|
1320
|
+
function deepFreezeJson(value) {
|
|
1321
|
+
if (value !== null && typeof value === "object") {
|
|
1322
|
+
Object.freeze(value);
|
|
1323
|
+
for (const item of Object.values(value))
|
|
1324
|
+
deepFreezeJson(item);
|
|
1325
|
+
}
|
|
1326
|
+
return value;
|
|
1327
|
+
}
|
|
1097
1328
|
function countExecutableSteps(workflow, steps) {
|
|
1098
1329
|
const metadata = compositionMetadata(workflow);
|
|
1099
1330
|
if (metadata === undefined)
|
|
@@ -1144,7 +1375,7 @@ function workflowSourceMismatch(state, source) {
|
|
|
1144
1375
|
return (state.workflowHash !== undefined &&
|
|
1145
1376
|
(source.kind !== "file" || state.workflowHash !== source.hash));
|
|
1146
1377
|
}
|
|
1147
|
-
function workflowIdentityMismatch(state, workflow, source) {
|
|
1378
|
+
export function workflowIdentityMismatch(state, workflow, source) {
|
|
1148
1379
|
if (workflowSourceMismatch(state, source))
|
|
1149
1380
|
return true;
|
|
1150
1381
|
const metadata = compositionMetadata(workflow);
|
|
@@ -1195,6 +1426,41 @@ function assistantMessageOutput(expectedOutput) {
|
|
|
1195
1426
|
function assistantMessageConfig(node) {
|
|
1196
1427
|
return assistantMessageOutput(node.expectedOutput);
|
|
1197
1428
|
}
|
|
1429
|
+
function appendLiveControlInstructions(prompt, workflow, nodeId, context) {
|
|
1430
|
+
const lines = [prompt.trimEnd(), "", "Workflow changes during this run:"];
|
|
1431
|
+
if (context.settingsScopeId !== undefined &&
|
|
1432
|
+
context.settingsChangeNumber !== undefined &&
|
|
1433
|
+
context.settings !== undefined) {
|
|
1434
|
+
const definition = settingsDefinitionForNode(workflow, nodeId);
|
|
1435
|
+
const value = canonicalJson(context.settings);
|
|
1436
|
+
const summary = value.length <= 8_192 ? value : `${value.slice(0, 8_192)}… [settings truncated]`;
|
|
1437
|
+
const allowed = (definition?.paths ?? [])
|
|
1438
|
+
.flatMap((rule) => Object.entries(rule.permissions)
|
|
1439
|
+
.filter(([, actors]) => actors?.includes("session") === true)
|
|
1440
|
+
.map(([permission]) => `${permission} ${rule.path || "/"}`))
|
|
1441
|
+
.join(", ");
|
|
1442
|
+
lines.push(`- Active settings scope: ${context.settingsScopeId}`, `- Current settings change number: ${context.settingsChangeNumber}`, ...(definition?.description !== undefined
|
|
1443
|
+
? [`- Settings purpose: ${definition.description}`]
|
|
1444
|
+
: []), `- Current settings: ${summary}`, `- Changes allowed from this model step: ${allowed || "none"}`, "- Treat the current settings as authoritative for future behavior in this workflow scope.", "- To change future workflow settings, use the workflow tool with action change-settings, this scopeId, an RFC 6902 patch, and optionally expectedChangeNumber.");
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
lines.push("- This workflow scope has no editable settings.");
|
|
1448
|
+
}
|
|
1449
|
+
lines.push("- To save normal user work for after successful completion, use workflow action queue-follow-up once for each prompt.", "- To remove an unsent prompt that this model queued, use workflow action remove-follow-up with its followUpId.", "- Setting changes affect only future node attempts and future settings routes. They do not change this running attempt or completed work.");
|
|
1450
|
+
return lines.join("\n");
|
|
1451
|
+
}
|
|
1452
|
+
function settingsDefinitionForNode(workflow, nodeId) {
|
|
1453
|
+
const metadata = compositionMetadata(workflow);
|
|
1454
|
+
if (metadata === undefined)
|
|
1455
|
+
return workflow.settings;
|
|
1456
|
+
const entry = metadata.entries[nodeId];
|
|
1457
|
+
if (entry !== undefined)
|
|
1458
|
+
return metadata.scopes[entry.parentPath]?.settings;
|
|
1459
|
+
const exit = metadata.exits[nodeId];
|
|
1460
|
+
if (exit !== undefined)
|
|
1461
|
+
return metadata.scopes[exit.mountPath]?.settings;
|
|
1462
|
+
return Object.values(metadata.scopes).find((scope) => Object.values(scope.authoredNodes).includes(nodeId))?.settings;
|
|
1463
|
+
}
|
|
1198
1464
|
/**
|
|
1199
1465
|
* The step contract appended to every agent-node prompt. This is the
|
|
1200
1466
|
* documented standard for how the model completes a workflow step.
|