@osolmaz/pi-workflows 0.13.2 → 0.13.4
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 +77 -73
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +8 -8
- package/dist/builtins/autoplan.workflow.js +44 -35
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +2 -2
- 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 +29 -29
- 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 +16 -1
- 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/skills/autoplan/SKILL.md +6 -6
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/autoplan.workflow.ts +45 -37
- package/src/builtins/catalog.ts +2 -2
- 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
|
@@ -155,9 +155,8 @@ type RunRow = {
|
|
|
155
155
|
workflowName: string;
|
|
156
156
|
workflowRef: string;
|
|
157
157
|
runStatus: string;
|
|
158
|
-
workflowSourceHash: Buffer;
|
|
159
|
-
workflowSourcesHash: Buffer | null;
|
|
160
158
|
definitionDigest: Buffer;
|
|
159
|
+
definitionHash: Buffer;
|
|
161
160
|
inputHash: Buffer;
|
|
162
161
|
launchOptionsHash: Buffer;
|
|
163
162
|
status: WorkflowRunLaunchStatus;
|
|
@@ -176,6 +175,12 @@ type RunRow = {
|
|
|
176
175
|
ownerId: string | null;
|
|
177
176
|
claimExpiresAt: number | null;
|
|
178
177
|
};
|
|
178
|
+
type RunSourceIdentityRow = {
|
|
179
|
+
mountPath: string;
|
|
180
|
+
sourceType: "builtin" | "file";
|
|
181
|
+
sourceRef: string;
|
|
182
|
+
sourceRevision: string;
|
|
183
|
+
};
|
|
179
184
|
|
|
180
185
|
type EffectRow = {
|
|
181
186
|
effectId: string;
|
|
@@ -1049,9 +1054,6 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1049
1054
|
const resourceId = resourceIdFor("run", options.runId);
|
|
1050
1055
|
const definitionHash = this.state.putJson(options.definitionSnapshot, now);
|
|
1051
1056
|
const queuedSource = queuedWorkflowSource(options.workflowSource);
|
|
1052
|
-
const sourceHash = this.state.putJson(queuedSource.root, now);
|
|
1053
|
-
const mountedSourcesHash =
|
|
1054
|
-
queuedSource.mounted.length === 0 ? null : this.state.putJson(queuedSource.mounted, now);
|
|
1055
1057
|
const inputHash = this.state.putJson(options.input ?? null, now);
|
|
1056
1058
|
const launchHash = this.state.putJson(options.launchOptions ?? {}, now);
|
|
1057
1059
|
this.state.connection
|
|
@@ -1071,15 +1073,13 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1071
1073
|
this.state.connection
|
|
1072
1074
|
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
1073
1075
|
.run(resourceId);
|
|
1074
|
-
const source = sourceParts(options.workflowSourceRef, options.definitionDigest);
|
|
1075
1076
|
this.state.connection
|
|
1076
1077
|
.prepare(
|
|
1077
1078
|
`INSERT INTO runs(
|
|
1078
1079
|
run_id, resource_id, project_id, parent_run_id, definition_digest,
|
|
1079
|
-
workflow_ref,
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?, ?)`,
|
|
1080
|
+
workflow_ref, launch_options_hash, status, paused,
|
|
1081
|
+
input_hash, created_at, updated_at
|
|
1082
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`,
|
|
1083
1083
|
)
|
|
1084
1084
|
.run(
|
|
1085
1085
|
options.runId,
|
|
@@ -1088,16 +1088,12 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1088
1088
|
options.parentRunId ?? null,
|
|
1089
1089
|
definitionDigest,
|
|
1090
1090
|
options.workflowSourceRef,
|
|
1091
|
-
sourceHash,
|
|
1092
1091
|
launchHash,
|
|
1093
|
-
source.type,
|
|
1094
|
-
source.ref,
|
|
1095
|
-
source.revision,
|
|
1096
1092
|
inputHash,
|
|
1097
|
-
mountedSourcesHash,
|
|
1098
1093
|
now,
|
|
1099
1094
|
now,
|
|
1100
1095
|
);
|
|
1096
|
+
insertQueuedRunSources(this.state, options.runId, queuedSource);
|
|
1101
1097
|
this.state.connection
|
|
1102
1098
|
.prepare(
|
|
1103
1099
|
`INSERT INTO run_bindings(run_id, origin_session_id, execution_mode, created_at)
|
|
@@ -2238,11 +2234,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2238
2234
|
runId: row.runId,
|
|
2239
2235
|
workflowName: row.workflowName,
|
|
2240
2236
|
workflowSourceRef: row.workflowRef,
|
|
2241
|
-
workflowSource:
|
|
2242
|
-
root: this.state.readJson(row.workflowSourceHash),
|
|
2243
|
-
mounted:
|
|
2244
|
-
row.workflowSourcesHash === null ? [] : this.state.readJson(row.workflowSourcesHash),
|
|
2245
|
-
},
|
|
2237
|
+
workflowSource: readQueuedRunSources(this.state, row),
|
|
2246
2238
|
initialized: row.runStatus !== "queued",
|
|
2247
2239
|
definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
|
|
2248
2240
|
input: this.state.readJson(row.inputHash),
|
|
@@ -2469,8 +2461,6 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2469
2461
|
.prepare(
|
|
2470
2462
|
`UPDATE runs
|
|
2471
2463
|
SET status = ?, paused = 0, status_detail = NULL, error_hash = ?,
|
|
2472
|
-
current_node = NULL, current_attempt_id = NULL,
|
|
2473
|
-
current_node_started_at = NULL, waiting_on = NULL,
|
|
2474
2464
|
updated_at = ?, finished_at = ?
|
|
2475
2465
|
WHERE run_id = ?`,
|
|
2476
2466
|
)
|
|
@@ -2776,9 +2766,8 @@ function workflowSelect(clause: string): string {
|
|
|
2776
2766
|
function workflowRunSelect(clause: string): string {
|
|
2777
2767
|
return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
|
|
2778
2768
|
d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
|
|
2779
|
-
r.
|
|
2780
|
-
r.
|
|
2781
|
-
r.definition_digest AS definitionDigest, r.input_hash AS inputHash,
|
|
2769
|
+
r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
|
|
2770
|
+
r.input_hash AS inputHash,
|
|
2782
2771
|
r.launch_options_hash AS launchOptionsHash,
|
|
2783
2772
|
q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
|
|
2784
2773
|
q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
|
|
@@ -2886,6 +2875,9 @@ function isLeaseRow(value: unknown): value is LeaseRow {
|
|
|
2886
2875
|
function isRunRow(value: unknown): value is RunRow {
|
|
2887
2876
|
return isRecord(value);
|
|
2888
2877
|
}
|
|
2878
|
+
function isRunSourceIdentityRow(value: unknown): value is RunSourceIdentityRow {
|
|
2879
|
+
return isRecord(value);
|
|
2880
|
+
}
|
|
2889
2881
|
function isEffectRow(value: unknown): value is EffectRow {
|
|
2890
2882
|
return isRecord(value);
|
|
2891
2883
|
}
|
|
@@ -2958,24 +2950,126 @@ function digestBuffer(value: string): Buffer {
|
|
|
2958
2950
|
if (!/^[a-f0-9]{64}$/i.test(hex)) throw new Error("Expected a SHA-256 digest");
|
|
2959
2951
|
return Buffer.from(hex, "hex");
|
|
2960
2952
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2953
|
+
type QueuedSource = {
|
|
2954
|
+
root:
|
|
2955
|
+
| { kind: "builtin"; id: string; revision: string }
|
|
2956
|
+
| { kind: "file"; path: string; hash: string };
|
|
2957
|
+
mounted: Array<{
|
|
2958
|
+
mountPath: string[];
|
|
2959
|
+
workflowName: string;
|
|
2960
|
+
source:
|
|
2961
|
+
| { kind: "builtin"; id: string; revision: string }
|
|
2962
|
+
| { kind: "file"; path: string; hash: string };
|
|
2963
|
+
}>;
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
function queuedWorkflowSource(value: unknown): QueuedSource {
|
|
2967
|
+
if (isWorkflowSource(value)) {
|
|
2963
2968
|
return { root: value, mounted: [] };
|
|
2964
2969
|
}
|
|
2965
|
-
if (
|
|
2970
|
+
if (
|
|
2971
|
+
isRecord(value) &&
|
|
2972
|
+
isWorkflowSource(value.root) &&
|
|
2973
|
+
Array.isArray(value.mounted) &&
|
|
2974
|
+
value.mounted.every(isMountedWorkflowSource)
|
|
2975
|
+
) {
|
|
2966
2976
|
return { root: value.root, mounted: value.mounted };
|
|
2967
2977
|
}
|
|
2968
2978
|
throw new Error("Stored workflow source identity is invalid");
|
|
2969
2979
|
}
|
|
2970
2980
|
|
|
2971
|
-
function
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2981
|
+
function isWorkflowSource(value: unknown): value is QueuedSource["root"] {
|
|
2982
|
+
return (
|
|
2983
|
+
isRecord(value) &&
|
|
2984
|
+
((value.kind === "builtin" &&
|
|
2985
|
+
typeof value.id === "string" &&
|
|
2986
|
+
typeof value.revision === "string") ||
|
|
2987
|
+
(value.kind === "file" && typeof value.path === "string" && typeof value.hash === "string"))
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function isMountedWorkflowSource(value: unknown): value is QueuedSource["mounted"][number] {
|
|
2992
|
+
return (
|
|
2993
|
+
isRecord(value) &&
|
|
2994
|
+
Array.isArray(value.mountPath) &&
|
|
2995
|
+
value.mountPath.every((part) => typeof part === "string") &&
|
|
2996
|
+
typeof value.workflowName === "string" &&
|
|
2997
|
+
isWorkflowSource(value.source)
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
function insertQueuedRunSources(state: StateDatabase, runId: string, value: QueuedSource): void {
|
|
3002
|
+
const insert = state.connection.prepare(
|
|
3003
|
+
`INSERT INTO run_sources(run_id, mount_path, source_type, source_ref, source_revision)
|
|
3004
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
3005
|
+
);
|
|
3006
|
+
const root = queuedSourceParts(value.root);
|
|
3007
|
+
insert.run(runId, "", root.type, root.ref, root.revision);
|
|
3008
|
+
for (const mounted of value.mounted) {
|
|
3009
|
+
const source = queuedSourceParts(mounted.source);
|
|
3010
|
+
insert.run(runId, mounted.mountPath.join("/"), source.type, source.ref, source.revision);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function queuedSourceParts(source: QueuedSource["root"]): {
|
|
3015
|
+
type: "builtin" | "file";
|
|
3016
|
+
ref: string;
|
|
3017
|
+
revision: string;
|
|
3018
|
+
} {
|
|
3019
|
+
return source.kind === "builtin"
|
|
3020
|
+
? { type: "builtin", ref: source.id, revision: source.revision }
|
|
3021
|
+
: { type: "file", ref: source.path, revision: source.hash };
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function readQueuedRunSources(state: StateDatabase, run: RunRow): QueuedSource {
|
|
3025
|
+
const rows = state.connection
|
|
3026
|
+
.prepare(
|
|
3027
|
+
`SELECT mount_path AS mountPath, source_type AS sourceType,
|
|
3028
|
+
source_ref AS sourceRef, source_revision AS sourceRevision
|
|
3029
|
+
FROM run_sources WHERE run_id = ? ORDER BY mount_path`,
|
|
3030
|
+
)
|
|
3031
|
+
.all(run.runId)
|
|
3032
|
+
.filter(isRunSourceIdentityRow);
|
|
3033
|
+
const rootRow = rows.find((row) => row.mountPath === "");
|
|
3034
|
+
if (rootRow === undefined) throw new Error(`Workflow run source is missing: ${run.runId}`);
|
|
3035
|
+
const snapshot = state.readJson(run.definitionHash);
|
|
3036
|
+
const mounts =
|
|
3037
|
+
isRecord(snapshot) &&
|
|
3038
|
+
isRecord(snapshot.composition) &&
|
|
3039
|
+
Array.isArray(snapshot.composition.mounts)
|
|
3040
|
+
? snapshot.composition.mounts
|
|
3041
|
+
: [];
|
|
3042
|
+
const names = new Map(
|
|
3043
|
+
mounts.flatMap((mount) => {
|
|
3044
|
+
if (
|
|
3045
|
+
!isRecord(mount) ||
|
|
3046
|
+
!Array.isArray(mount.mountPath) ||
|
|
3047
|
+
!mount.mountPath.every((part) => typeof part === "string") ||
|
|
3048
|
+
typeof mount.workflowName !== "string"
|
|
3049
|
+
) {
|
|
3050
|
+
return [];
|
|
3051
|
+
}
|
|
3052
|
+
return [[mount.mountPath.join("/"), mount.workflowName] as const];
|
|
3053
|
+
}),
|
|
3054
|
+
);
|
|
3055
|
+
return {
|
|
3056
|
+
root: rowToSource(rootRow),
|
|
3057
|
+
mounted: rows
|
|
3058
|
+
.filter((row) => row.mountPath !== "")
|
|
3059
|
+
.map((row) => ({
|
|
3060
|
+
mountPath: row.mountPath.split("/"),
|
|
3061
|
+
workflowName: names.get(row.mountPath) ?? row.mountPath,
|
|
3062
|
+
source: rowToSource(row),
|
|
3063
|
+
})),
|
|
3064
|
+
};
|
|
2978
3065
|
}
|
|
3066
|
+
|
|
3067
|
+
function rowToSource(row: RunSourceIdentityRow): QueuedSource["root"] {
|
|
3068
|
+
return row.sourceType === "builtin"
|
|
3069
|
+
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
3070
|
+
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2979
3073
|
function effectStatus(value: EffectRecord["state"]): string {
|
|
2980
3074
|
return value === "indeterminate" ? "ambiguous" : value;
|
|
2981
3075
|
}
|
package/src/controllers/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { JsonPatch } from "../workflows/json-patch.js";
|
|
2
|
+
|
|
1
3
|
export type MaybePromise<T> = T | Promise<T>;
|
|
2
4
|
export type JsonPrimitive = string | number | boolean | null;
|
|
3
5
|
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
@@ -131,8 +133,46 @@ export type ChildWorkflowRecord = ChildWorkflowReference & {
|
|
|
131
133
|
error?: string;
|
|
132
134
|
};
|
|
133
135
|
|
|
136
|
+
export type ControllerSettingsChangeRequest = {
|
|
137
|
+
requestKey: string;
|
|
138
|
+
runId: string;
|
|
139
|
+
scopeId?: string;
|
|
140
|
+
expectedChangeNumber?: number;
|
|
141
|
+
patch: JsonPatch;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type ControllerSettingsChangeResult = {
|
|
145
|
+
runId: string;
|
|
146
|
+
scopeId: string;
|
|
147
|
+
changeNumber: number;
|
|
148
|
+
adopted: boolean;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type ControllerQueueFollowUpRequest = {
|
|
152
|
+
requestKey: string;
|
|
153
|
+
runId: string;
|
|
154
|
+
prompt: string;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ControllerFollowUpResult = {
|
|
158
|
+
runId: string;
|
|
159
|
+
followUpId: string;
|
|
160
|
+
order: number;
|
|
161
|
+
state: string;
|
|
162
|
+
adopted: boolean;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export type ControllerRemoveFollowUpRequest = {
|
|
166
|
+
requestKey: string;
|
|
167
|
+
runId: string;
|
|
168
|
+
followUpId: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
134
171
|
export interface ControllerWorkflows {
|
|
135
172
|
ensure(request: ChildWorkflowRequest): Promise<ChildWorkflowRecord>;
|
|
173
|
+
changeSettings(request: ControllerSettingsChangeRequest): Promise<ControllerSettingsChangeResult>;
|
|
174
|
+
queueFollowUp(request: ControllerQueueFollowUpRequest): Promise<ControllerFollowUpResult>;
|
|
175
|
+
removeFollowUp(request: ControllerRemoveFollowUpRequest): Promise<ControllerFollowUpResult>;
|
|
136
176
|
}
|
|
137
177
|
|
|
138
178
|
export type ReconcileContext<TStatus> = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compositionMetadata } from "../workflows/composition.js";
|
|
2
|
+
import { WorkflowEngine, workflowIdentityMismatch } from "../workflows/engine.js";
|
|
2
3
|
import type { WorkflowRunStore } from "../workflows/store.js";
|
|
3
4
|
import type {
|
|
4
5
|
WorkflowDefinition,
|
|
@@ -7,6 +8,14 @@ import type {
|
|
|
7
8
|
WorkflowSource,
|
|
8
9
|
} from "../workflows/types.js";
|
|
9
10
|
import type {
|
|
11
|
+
ControllerFollowUpResult,
|
|
12
|
+
ControllerQueueFollowUpRequest,
|
|
13
|
+
ControllerRemoveFollowUpRequest,
|
|
14
|
+
ControllerSettingsChangeRequest,
|
|
15
|
+
ControllerSettingsChangeResult,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
import type {
|
|
18
|
+
ControllerWorkflowControlRequest,
|
|
10
19
|
ControllerWorkflowScheduler,
|
|
11
20
|
WorkflowSchedulerRequest,
|
|
12
21
|
WorkflowSchedulerResult,
|
|
@@ -96,6 +105,99 @@ export class WorkflowEngineScheduler implements ControllerWorkflowScheduler {
|
|
|
96
105
|
return { state: "running", runId };
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
async changeSettings(
|
|
109
|
+
request: ControllerWorkflowControlRequest<ControllerSettingsChangeRequest>,
|
|
110
|
+
signal: AbortSignal,
|
|
111
|
+
): Promise<ControllerSettingsChangeResult> {
|
|
112
|
+
signal.throwIfAborted();
|
|
113
|
+
const bundle = this.options.store.readRun(request.runId);
|
|
114
|
+
if (bundle === null) throw new Error(`Workflow run not found: ${request.runId}`);
|
|
115
|
+
const resolved = await this.options.resolveWorkflow(bundle.state.workflowName);
|
|
116
|
+
signal.throwIfAborted();
|
|
117
|
+
if (workflowIdentityMismatch(bundle.state, resolved.workflow, resolved.workflowSource)) {
|
|
118
|
+
throw new Error(`Workflow source changed since run ${request.runId} started`);
|
|
119
|
+
}
|
|
120
|
+
const scopes = this.options.store.listSettingsScopes(request.runId);
|
|
121
|
+
const scopeId = request.scopeId ?? (scopes.length === 1 ? scopes[0]?.scopeId : undefined);
|
|
122
|
+
if (scopeId === undefined) {
|
|
123
|
+
throw new Error("Controller settings changes require scopeId when several scopes exist");
|
|
124
|
+
}
|
|
125
|
+
const scope = this.options.store.getSettingsScope(scopeId);
|
|
126
|
+
if (scope === undefined || scope.activeRunId !== request.runId) {
|
|
127
|
+
throw new Error(`Workflow settings scope not found: ${scopeId}`);
|
|
128
|
+
}
|
|
129
|
+
const definition =
|
|
130
|
+
scope.mountPath === ""
|
|
131
|
+
? resolved.workflow.settings
|
|
132
|
+
: compositionMetadata(resolved.workflow)?.scopes[scope.mountPath]?.settings;
|
|
133
|
+
if (definition === undefined) {
|
|
134
|
+
throw new Error(`Workflow scope ${scopeId} does not declare editable settings`);
|
|
135
|
+
}
|
|
136
|
+
const result = await this.options.store.changeSettings(definition, {
|
|
137
|
+
runId: request.runId,
|
|
138
|
+
scopeId,
|
|
139
|
+
requestId: request.actorRequestKey,
|
|
140
|
+
...(request.expectedChangeNumber !== undefined
|
|
141
|
+
? { expectedChangeNumber: request.expectedChangeNumber }
|
|
142
|
+
: {}),
|
|
143
|
+
actor: { type: "controller", id: request.controllerResourceUid },
|
|
144
|
+
source: "controller-request",
|
|
145
|
+
patch: request.patch,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
runId: request.runId,
|
|
149
|
+
scopeId,
|
|
150
|
+
changeNumber: result.scope.changeNumber,
|
|
151
|
+
adopted: result.adopted,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async queueFollowUp(
|
|
156
|
+
request: ControllerWorkflowControlRequest<ControllerQueueFollowUpRequest>,
|
|
157
|
+
signal: AbortSignal,
|
|
158
|
+
): Promise<ControllerFollowUpResult> {
|
|
159
|
+
signal.throwIfAborted();
|
|
160
|
+
const targetSessionId = this.options.store.originSessionId(request.runId);
|
|
161
|
+
if (targetSessionId === undefined) {
|
|
162
|
+
throw new Error("Controller follow-up requires a workflow run with an origin Pi session");
|
|
163
|
+
}
|
|
164
|
+
const result = this.options.store.queueFollowUp({
|
|
165
|
+
runId: request.runId,
|
|
166
|
+
requestId: request.actorRequestKey,
|
|
167
|
+
targetSessionId,
|
|
168
|
+
actor: { type: "controller", id: request.controllerResourceUid },
|
|
169
|
+
source: "controller-request",
|
|
170
|
+
prompt: request.prompt,
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
runId: request.runId,
|
|
174
|
+
followUpId: result.followUp.followUpId,
|
|
175
|
+
order: result.followUp.order,
|
|
176
|
+
state: result.followUp.state,
|
|
177
|
+
adopted: result.adopted,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async removeFollowUp(
|
|
182
|
+
request: ControllerWorkflowControlRequest<ControllerRemoveFollowUpRequest>,
|
|
183
|
+
signal: AbortSignal,
|
|
184
|
+
): Promise<ControllerFollowUpResult> {
|
|
185
|
+
signal.throwIfAborted();
|
|
186
|
+
const result = this.options.store.removeFollowUp({
|
|
187
|
+
runId: request.runId,
|
|
188
|
+
followUpId: request.followUpId,
|
|
189
|
+
actor: { type: "controller", id: request.controllerResourceUid },
|
|
190
|
+
source: "controller-request",
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
runId: request.runId,
|
|
194
|
+
followUpId: result.followUpId,
|
|
195
|
+
order: result.order,
|
|
196
|
+
state: result.state,
|
|
197
|
+
adopted: false,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
99
201
|
async waitForIdle(): Promise<void> {
|
|
100
202
|
await Promise.all([...this.active.values()].map((entry) => entry.promise));
|
|
101
203
|
}
|
|
@@ -5,8 +5,13 @@ import type {
|
|
|
5
5
|
ChildWorkflowRecord,
|
|
6
6
|
ChildWorkflowRequest,
|
|
7
7
|
ChildWorkflowState,
|
|
8
|
+
ControllerFollowUpResult,
|
|
8
9
|
ControllerQueueClaim,
|
|
10
|
+
ControllerQueueFollowUpRequest,
|
|
11
|
+
ControllerRemoveFollowUpRequest,
|
|
9
12
|
ControllerResource,
|
|
13
|
+
ControllerSettingsChangeRequest,
|
|
14
|
+
ControllerSettingsChangeResult,
|
|
10
15
|
ControllerWorkflows,
|
|
11
16
|
} from "./types.js";
|
|
12
17
|
|
|
@@ -24,12 +29,36 @@ export type WorkflowSchedulerResult = {
|
|
|
24
29
|
error?: string;
|
|
25
30
|
};
|
|
26
31
|
|
|
32
|
+
export type ControllerWorkflowControlRequest<TRequest> = TRequest & {
|
|
33
|
+
controllerResourceUid: string;
|
|
34
|
+
actorRequestKey: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
27
37
|
export interface ControllerWorkflowScheduler {
|
|
28
38
|
ensure(
|
|
29
39
|
request: WorkflowSchedulerRequest,
|
|
30
40
|
signal: AbortSignal,
|
|
31
41
|
onComplete: (result: WorkflowSchedulerResult) => void,
|
|
32
42
|
): Promise<WorkflowSchedulerResult>;
|
|
43
|
+
changeSettings?(
|
|
44
|
+
request: ControllerWorkflowControlRequest<ControllerSettingsChangeRequest>,
|
|
45
|
+
signal: AbortSignal,
|
|
46
|
+
): Promise<ControllerSettingsChangeResult>;
|
|
47
|
+
queueFollowUp?(
|
|
48
|
+
request: ControllerWorkflowControlRequest<ControllerQueueFollowUpRequest>,
|
|
49
|
+
signal: AbortSignal,
|
|
50
|
+
): Promise<ControllerFollowUpResult>;
|
|
51
|
+
removeFollowUp?(
|
|
52
|
+
request: ControllerWorkflowControlRequest<ControllerRemoveFollowUpRequest>,
|
|
53
|
+
signal: AbortSignal,
|
|
54
|
+
): Promise<ControllerFollowUpResult>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function controllerRequestKey(resource: ControllerResource, requestKey: string): string {
|
|
58
|
+
if (requestKey.trim().length === 0) {
|
|
59
|
+
throw new Error("Controller workflow requestKey must not be empty");
|
|
60
|
+
}
|
|
61
|
+
return `controller:${resource.metadata.uid}:${requestKey}`;
|
|
33
62
|
}
|
|
34
63
|
|
|
35
64
|
export class ControllerWorkflowCoordinator {
|
|
@@ -45,6 +74,45 @@ export class ControllerWorkflowCoordinator {
|
|
|
45
74
|
): ControllerWorkflows {
|
|
46
75
|
return {
|
|
47
76
|
ensure: async (request) => await this.ensure(resource, claim, request, signal),
|
|
77
|
+
changeSettings: async (request) => {
|
|
78
|
+
if (this.scheduler?.changeSettings === undefined) {
|
|
79
|
+
throw new Error("This controller host does not support workflow settings changes");
|
|
80
|
+
}
|
|
81
|
+
return await this.scheduler.changeSettings(
|
|
82
|
+
{
|
|
83
|
+
...request,
|
|
84
|
+
controllerResourceUid: resource.metadata.uid,
|
|
85
|
+
actorRequestKey: controllerRequestKey(resource, request.requestKey),
|
|
86
|
+
},
|
|
87
|
+
signal,
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
queueFollowUp: async (request) => {
|
|
91
|
+
if (this.scheduler?.queueFollowUp === undefined) {
|
|
92
|
+
throw new Error("This controller host does not support workflow follow-ups");
|
|
93
|
+
}
|
|
94
|
+
return await this.scheduler.queueFollowUp(
|
|
95
|
+
{
|
|
96
|
+
...request,
|
|
97
|
+
controllerResourceUid: resource.metadata.uid,
|
|
98
|
+
actorRequestKey: controllerRequestKey(resource, request.requestKey),
|
|
99
|
+
},
|
|
100
|
+
signal,
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
removeFollowUp: async (request) => {
|
|
104
|
+
if (this.scheduler?.removeFollowUp === undefined) {
|
|
105
|
+
throw new Error("This controller host does not support workflow follow-ups");
|
|
106
|
+
}
|
|
107
|
+
return await this.scheduler.removeFollowUp(
|
|
108
|
+
{
|
|
109
|
+
...request,
|
|
110
|
+
controllerResourceUid: resource.metadata.uid,
|
|
111
|
+
actorRequestKey: controllerRequestKey(resource, request.requestKey),
|
|
112
|
+
},
|
|
113
|
+
signal,
|
|
114
|
+
);
|
|
115
|
+
},
|
|
48
116
|
};
|
|
49
117
|
}
|
|
50
118
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
loadDiscoveredControllers,
|
|
5
5
|
type AnyControllerDefinition,
|
|
6
6
|
type ControllerResource,
|
|
7
|
+
type ControllerWorkflowScheduler,
|
|
7
8
|
type WorkflowSchedulerRequest,
|
|
8
9
|
type WorkflowSchedulerResult,
|
|
9
10
|
} from "../controllers/index.js";
|
|
@@ -80,6 +81,10 @@ export class PiControllerHost {
|
|
|
80
81
|
static async create(options: {
|
|
81
82
|
cwd: string;
|
|
82
83
|
startChild: PiChildWorkflowStarter;
|
|
84
|
+
workflowControls?: Pick<
|
|
85
|
+
ControllerWorkflowScheduler,
|
|
86
|
+
"changeSettings" | "queueFollowUp" | "removeFollowUp"
|
|
87
|
+
>;
|
|
83
88
|
}): Promise<PiControllerHost | undefined> {
|
|
84
89
|
const definitions = await loadDiscoveredControllers({ cwd: options.cwd });
|
|
85
90
|
if (definitions.length === 0) {
|
|
@@ -89,7 +94,7 @@ export class PiControllerHost {
|
|
|
89
94
|
const manager = new ControllerManager({
|
|
90
95
|
store,
|
|
91
96
|
controllers: definitions,
|
|
92
|
-
workflowScheduler: { ensure: options.startChild },
|
|
97
|
+
workflowScheduler: { ensure: options.startChild, ...options.workflowControls },
|
|
93
98
|
});
|
|
94
99
|
return new PiControllerHost(definitions, store, manager);
|
|
95
100
|
}
|