@osolmaz/pi-workflows 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/builtins/autodoc.workflow.d.ts +5 -5
- package/dist/builtins/autoimplement.workflow.d.ts +69 -61
- package/dist/builtins/autoimplement.workflow.js +47 -8
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +11 -5
- package/dist/builtins/autoplan.workflow.js +68 -32
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +4 -4
- package/dist/builtins/change-verification.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +1 -1
- package/dist/builtins/plain-summary.workflow.js +35 -24
- package/dist/builtins/plain-summary.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +25 -23
- package/dist/builtins/sanity-check.workflow.d.ts +3 -3
- package/dist/builtins/sanity-check.workflow.js +19 -22
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
- package/dist/controllers/index.d.ts +2 -2
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.js +88 -61
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/types.d.ts +34 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
- package/dist/controllers/workflow-engine-scheduler.js +83 -1
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +8 -1
- package/dist/controllers/workflows.js +36 -0
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.d.ts +2 -1
- package/dist/extension/controller-host.js +1 -1
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.js +45 -7
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/follow-up-coordinator.d.ts +27 -0
- package/dist/extension/follow-up-coordinator.js +131 -0
- package/dist/extension/follow-up-coordinator.js.map +1 -0
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +462 -76
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +1 -1
- package/dist/extension/recorder.js +8 -8
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -3
- package/dist/extension/session-events.js +11 -10
- package/dist/extension/session-events.js.map +1 -1
- package/dist/extension/widget.js +9 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/state/database.d.ts +2 -1
- package/dist/state/database.js +11 -13
- package/dist/state/database.js.map +1 -1
- package/dist/state/json.js +6 -1
- package/dist/state/json.js.map +1 -1
- package/dist/state/mutation.d.ts +1 -1
- package/dist/state/mutation.js.map +1 -1
- package/dist/state/prune.d.ts +18 -0
- package/dist/state/prune.js +373 -0
- package/dist/state/prune.js.map +1 -0
- package/dist/state/schema.d.ts +1 -1
- package/dist/state/schema.js +130 -13
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/cli.d.ts +3 -1
- package/dist/viewer/cli.js +55 -4
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +0 -1
- package/dist/viewer/session-reducer.js +2 -24
- package/dist/viewer/session-reducer.js.map +1 -1
- package/dist/workflows/composition.d.ts +2 -0
- package/dist/workflows/composition.js +15 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -3
- package/dist/workflows/definition.js +3 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -1
- package/dist/workflows/engine.js +303 -36
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +1 -0
- package/dist/workflows/human-decision.js +25 -9
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/json-patch.d.ts +47 -0
- package/dist/workflows/json-patch.js +298 -0
- package/dist/workflows/json-patch.js.map +1 -0
- package/dist/workflows/schema.js +29 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/settings.d.ts +128 -0
- package/dist/workflows/settings.js +199 -0
- package/dist/workflows/settings.js.map +1 -0
- package/dist/workflows/store.d.ts +81 -3
- package/dist/workflows/store.js +1563 -50
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +22 -0
- package/dist/workflows/tool-input.js +43 -0
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +41 -6
- package/docs/2026-08-25-workflow-follow-ups.md +132 -0
- package/docs/2026-08-25-workflow-settings.md +169 -0
- package/docs/DEFERRED_TURNS.md +6 -0
- package/docs/DESIGN_PHILOSOPHY.md +2 -0
- package/docs/SQLITE_STATE.md +36 -17
- package/docs/WORKFLOW_COMPOSITION.md +15 -0
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
- package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
- package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
- package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
- package/docs/session-event-journal.md +2 -3
- package/docs/tui-viewer.md +7 -6
- package/docs/workflows.md +58 -10
- package/examples/workflows/live-settings.workflow.ts +93 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/builtins/autoimplement.workflow.ts +56 -8
- package/src/builtins/autoplan.workflow.ts +80 -32
- package/src/builtins/catalog.ts +4 -4
- package/src/builtins/plain-summary.workflow.ts +38 -40
- package/src/builtins/sanity-check.workflow.ts +19 -22
- package/src/controllers/index.ts +6 -0
- package/src/controllers/sqlite.ts +132 -72
- package/src/controllers/types.ts +40 -0
- package/src/controllers/workflow-engine-scheduler.ts +103 -1
- package/src/controllers/workflows.ts +68 -0
- package/src/extension/controller-host.ts +6 -1
- package/src/extension/decision-channels.ts +47 -7
- package/src/extension/follow-up-coordinator.ts +151 -0
- package/src/extension/index.ts +540 -89
- package/src/extension/recorder.ts +10 -8
- package/src/extension/session-events.ts +15 -13
- package/src/extension/widget.ts +8 -0
- package/src/state/database.ts +12 -12
- package/src/state/json.ts +6 -1
- package/src/state/mutation.ts +4 -1
- package/src/state/prune.ts +502 -0
- package/src/state/schema.ts +130 -13
- package/src/viewer/cli.ts +52 -5
- package/src/viewer/render.ts +43 -1
- package/src/viewer/session-reducer.ts +2 -29
- package/src/workflows/composition.ts +19 -0
- package/src/workflows/definition.ts +19 -5
- package/src/workflows/engine.ts +365 -36
- package/src/workflows/human-decision.ts +41 -11
- package/src/workflows/index.ts +43 -0
- package/src/workflows/json-patch.ts +375 -0
- package/src/workflows/schema.ts +31 -1
- package/src/workflows/settings.ts +430 -0
- package/src/workflows/store.ts +2268 -121
- package/src/workflows/tool-input.ts +58 -0
- package/src/workflows/types.ts +43 -5
|
@@ -651,7 +651,7 @@ export class HumanDecisionStore {
|
|
|
651
651
|
return this.state.transaction(() => {
|
|
652
652
|
const existing = this.readContinuationRow(decisionId);
|
|
653
653
|
if (existing !== undefined) {
|
|
654
|
-
const stored = this.
|
|
654
|
+
const stored = this.continuationRecord(decisionId, existing);
|
|
655
655
|
if (canonicalJson(stored) !== canonicalJson(value)) {
|
|
656
656
|
throw new Error("Immutable human decision continuation conflicts");
|
|
657
657
|
}
|
|
@@ -661,23 +661,20 @@ export class HumanDecisionStore {
|
|
|
661
661
|
if (resolution?.outcome !== "accepted") {
|
|
662
662
|
throw new Error("A continuation requires an accepted human decision");
|
|
663
663
|
}
|
|
664
|
-
const recordHash = this.state.putJson(value, Date.parse(value.createdAt));
|
|
665
664
|
this.state.connection
|
|
666
665
|
.prepare(
|
|
667
666
|
`INSERT INTO continuations(
|
|
668
|
-
decision_id, parent_run_id, continuation_run_id,
|
|
669
|
-
) VALUES (?, ?, ?,
|
|
667
|
+
decision_id, parent_run_id, continuation_run_id, created_at
|
|
668
|
+
) VALUES (?, ?, ?, ?)`,
|
|
670
669
|
)
|
|
671
|
-
.run(decisionId, value.parentRunId, value.runId,
|
|
670
|
+
.run(decisionId, value.parentRunId, value.runId, Date.parse(value.createdAt));
|
|
672
671
|
return "created";
|
|
673
672
|
});
|
|
674
673
|
}
|
|
675
674
|
|
|
676
675
|
async readContinuation(decisionId: string): Promise<HumanDecisionContinuationRecord | null> {
|
|
677
676
|
const row = this.readContinuationRow(decisionId);
|
|
678
|
-
return row === undefined
|
|
679
|
-
? null
|
|
680
|
-
: (this.state.readJson(row.recordHash) as HumanDecisionContinuationRecord);
|
|
677
|
+
return row === undefined ? null : this.continuationRecord(decisionId, row);
|
|
681
678
|
}
|
|
682
679
|
|
|
683
680
|
markEffectApplied(
|
|
@@ -1145,12 +1142,37 @@ export class HumanDecisionStore {
|
|
|
1145
1142
|
.run(outcome, resultHash, decisionId, attemptId);
|
|
1146
1143
|
}
|
|
1147
1144
|
|
|
1148
|
-
private readContinuationRow(decisionId: string):
|
|
1145
|
+
private readContinuationRow(decisionId: string): ContinuationRow | undefined {
|
|
1149
1146
|
const row = this.state.connection
|
|
1150
|
-
.prepare(
|
|
1147
|
+
.prepare(
|
|
1148
|
+
`SELECT c.parent_run_id AS parentRunId, c.continuation_run_id AS continuationRunId,
|
|
1149
|
+
c.created_at AS createdAt, d.request_hash AS requestHash,
|
|
1150
|
+
r.response_hash AS resolutionHash
|
|
1151
|
+
FROM continuations c
|
|
1152
|
+
JOIN human_decisions d ON d.decision_id = c.decision_id
|
|
1153
|
+
JOIN human_decision_resolutions r ON r.decision_id = c.decision_id
|
|
1154
|
+
WHERE c.decision_id = ? AND r.outcome = 'accepted'`,
|
|
1155
|
+
)
|
|
1151
1156
|
.get(decisionId);
|
|
1152
1157
|
return isContinuationRow(row) ? row : undefined;
|
|
1153
1158
|
}
|
|
1159
|
+
|
|
1160
|
+
private continuationRecord(
|
|
1161
|
+
decisionId: string,
|
|
1162
|
+
row: ContinuationRow,
|
|
1163
|
+
): HumanDecisionContinuationRecord {
|
|
1164
|
+
const request = this.state.readJson(row.requestHash) as HumanDecisionRequest;
|
|
1165
|
+
const decision = this.state.readJson(row.resolutionHash) as ResolvedHumanDecision;
|
|
1166
|
+
return {
|
|
1167
|
+
schema: "pi-workflows.human-decision-continuation.v1",
|
|
1168
|
+
decisionId,
|
|
1169
|
+
requestDigest: request.requestDigest,
|
|
1170
|
+
provenance: decision.provenance,
|
|
1171
|
+
parentRunId: row.parentRunId,
|
|
1172
|
+
runId: row.continuationRunId,
|
|
1173
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1154
1176
|
}
|
|
1155
1177
|
|
|
1156
1178
|
function digestBuffer(value: string): Buffer {
|
|
@@ -1215,7 +1237,15 @@ function isContentHashRow(value: unknown): value is { contentHash: Buffer } {
|
|
|
1215
1237
|
return isRecordValue(value);
|
|
1216
1238
|
}
|
|
1217
1239
|
|
|
1218
|
-
|
|
1240
|
+
type ContinuationRow = {
|
|
1241
|
+
parentRunId: string;
|
|
1242
|
+
continuationRunId: string;
|
|
1243
|
+
createdAt: number;
|
|
1244
|
+
requestHash: Buffer;
|
|
1245
|
+
resolutionHash: Buffer;
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
function isContinuationRow(value: unknown): value is ContinuationRow {
|
|
1219
1249
|
return isRecordValue(value);
|
|
1220
1250
|
}
|
|
1221
1251
|
|
package/src/workflows/index.ts
CHANGED
|
@@ -63,6 +63,47 @@ export {
|
|
|
63
63
|
parseStrictJsonValue,
|
|
64
64
|
type JsonParseMode,
|
|
65
65
|
} from "./json.js";
|
|
66
|
+
export {
|
|
67
|
+
MAX_JSON_PATCH_BYTES,
|
|
68
|
+
MAX_JSON_PATCH_OPERATIONS,
|
|
69
|
+
MAX_JSON_POINTER_BYTES,
|
|
70
|
+
MAX_WORKFLOW_SETTINGS_BYTES,
|
|
71
|
+
applyJsonPatch,
|
|
72
|
+
encodeJsonPointer,
|
|
73
|
+
jsonPointerTarget,
|
|
74
|
+
parseJsonPointer,
|
|
75
|
+
validateJsonPatch,
|
|
76
|
+
type JsonPatch,
|
|
77
|
+
type JsonPatchOperation,
|
|
78
|
+
} from "./json-patch.js";
|
|
79
|
+
export {
|
|
80
|
+
allowSettingsPath,
|
|
81
|
+
applyWorkflowSettingsPatch,
|
|
82
|
+
parseWorkflowSettingsValue,
|
|
83
|
+
resolveInitialWorkflowSettings,
|
|
84
|
+
settingsRoute,
|
|
85
|
+
settingsRuleForPath,
|
|
86
|
+
workflowSettings,
|
|
87
|
+
workflowSettingsScopeId,
|
|
88
|
+
type AppliedWorkflowSettings,
|
|
89
|
+
type InitialWorkflowSettingsScope,
|
|
90
|
+
type WorkflowFollowUpQueueRecord,
|
|
91
|
+
type WorkflowFollowUpRecord,
|
|
92
|
+
type WorkflowFollowUpState,
|
|
93
|
+
type WorkflowPresentationState,
|
|
94
|
+
type WorkflowQueueFollowUpRequest,
|
|
95
|
+
type WorkflowRemoveFollowUpRequest,
|
|
96
|
+
type WorkflowSettingsChangeContext,
|
|
97
|
+
type WorkflowSettingsChangeRecord,
|
|
98
|
+
type WorkflowSettingsChangeRequest,
|
|
99
|
+
type WorkflowSettingsChangeResult,
|
|
100
|
+
type WorkflowSettingsScopeRecord,
|
|
101
|
+
type WorkflowSettingsDefinition,
|
|
102
|
+
type WorkflowSettingsOptions,
|
|
103
|
+
type WorkflowSettingsPathPermissions,
|
|
104
|
+
type WorkflowSettingsPathRule,
|
|
105
|
+
type WorkflowSettingsPermission,
|
|
106
|
+
} from "./settings.js";
|
|
66
107
|
export { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
67
108
|
export {
|
|
68
109
|
discoverWorkflows,
|
|
@@ -191,6 +232,7 @@ export type {
|
|
|
191
232
|
WorkflowIncludeDefinition,
|
|
192
233
|
WorkflowIncludedResult,
|
|
193
234
|
WorkflowInputOf,
|
|
235
|
+
WorkflowSettingsOf,
|
|
194
236
|
WorkflowMountedSource,
|
|
195
237
|
WorkflowMountSnapshot,
|
|
196
238
|
WorkflowValueParser,
|
|
@@ -213,6 +255,7 @@ export type {
|
|
|
213
255
|
WorkflowSource,
|
|
214
256
|
WorkflowSessionBinding,
|
|
215
257
|
WorkflowSessionEntryRecord,
|
|
258
|
+
WorkflowSettingsSnapshot,
|
|
216
259
|
WorkflowStepRecord,
|
|
217
260
|
WorkflowTraceEvent,
|
|
218
261
|
WorkflowTraceEventDraft,
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { canonicalJson, type JsonValue } from "../state/json.js";
|
|
2
|
+
|
|
3
|
+
export const MAX_JSON_PATCH_OPERATIONS = 256;
|
|
4
|
+
export const MAX_JSON_PATCH_BYTES = 64 * 1024;
|
|
5
|
+
export const MAX_JSON_POINTER_BYTES = 1024;
|
|
6
|
+
export const MAX_WORKFLOW_SETTINGS_BYTES = 256 * 1024;
|
|
7
|
+
|
|
8
|
+
export type JsonPatchAddOperation = {
|
|
9
|
+
op: "add";
|
|
10
|
+
path: string;
|
|
11
|
+
value: JsonValue;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type JsonPatchRemoveOperation = {
|
|
15
|
+
op: "remove";
|
|
16
|
+
path: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type JsonPatchReplaceOperation = {
|
|
20
|
+
op: "replace";
|
|
21
|
+
path: string;
|
|
22
|
+
value: JsonValue;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type JsonPatchMoveOperation = {
|
|
26
|
+
op: "move";
|
|
27
|
+
path: string;
|
|
28
|
+
from: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type JsonPatchCopyOperation = {
|
|
32
|
+
op: "copy";
|
|
33
|
+
path: string;
|
|
34
|
+
from: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type JsonPatchTestOperation = {
|
|
38
|
+
op: "test";
|
|
39
|
+
path: string;
|
|
40
|
+
value: JsonValue;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type JsonPatchOperation =
|
|
44
|
+
| JsonPatchAddOperation
|
|
45
|
+
| JsonPatchRemoveOperation
|
|
46
|
+
| JsonPatchReplaceOperation
|
|
47
|
+
| JsonPatchMoveOperation
|
|
48
|
+
| JsonPatchCopyOperation
|
|
49
|
+
| JsonPatchTestOperation;
|
|
50
|
+
|
|
51
|
+
export type JsonPatch = JsonPatchOperation[];
|
|
52
|
+
|
|
53
|
+
export type JsonPointerTarget = {
|
|
54
|
+
exists: boolean;
|
|
55
|
+
value?: JsonValue;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function validateJsonPatch(value: unknown): JsonPatch {
|
|
59
|
+
if (!Array.isArray(value)) {
|
|
60
|
+
throw new Error("JSON Patch must be an array");
|
|
61
|
+
}
|
|
62
|
+
if (value.length > MAX_JSON_PATCH_OPERATIONS) {
|
|
63
|
+
throw new Error(`JSON Patch cannot contain more than ${MAX_JSON_PATCH_OPERATIONS} operations`);
|
|
64
|
+
}
|
|
65
|
+
const patch = value.map((operation, index) => validateOperation(operation, index));
|
|
66
|
+
const bytes = Buffer.byteLength(canonicalJson(patch), "utf8");
|
|
67
|
+
if (bytes > MAX_JSON_PATCH_BYTES) {
|
|
68
|
+
throw new Error(`JSON Patch cannot exceed ${MAX_JSON_PATCH_BYTES} bytes`);
|
|
69
|
+
}
|
|
70
|
+
return patch;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseJsonPointer(pointer: string): string[] {
|
|
74
|
+
if (typeof pointer !== "string") {
|
|
75
|
+
throw new Error("JSON Pointer must be a string");
|
|
76
|
+
}
|
|
77
|
+
if (Buffer.byteLength(pointer, "utf8") > MAX_JSON_POINTER_BYTES) {
|
|
78
|
+
throw new Error(`JSON Pointer cannot exceed ${MAX_JSON_POINTER_BYTES} bytes`);
|
|
79
|
+
}
|
|
80
|
+
if (pointer === "") return [];
|
|
81
|
+
if (!pointer.startsWith("/")) {
|
|
82
|
+
throw new Error(`Invalid JSON Pointer: ${JSON.stringify(pointer)}`);
|
|
83
|
+
}
|
|
84
|
+
return pointer
|
|
85
|
+
.slice(1)
|
|
86
|
+
.split("/")
|
|
87
|
+
.map((segment) => {
|
|
88
|
+
if (/~(?![01])/u.test(segment)) {
|
|
89
|
+
throw new Error(`Invalid JSON Pointer escape in ${JSON.stringify(pointer)}`);
|
|
90
|
+
}
|
|
91
|
+
return segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function encodeJsonPointer(segments: readonly string[]): string {
|
|
96
|
+
if (segments.length === 0) return "";
|
|
97
|
+
return `/${segments.map((segment) => segment.replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function jsonPointerTarget(document: JsonValue, pointer: string): JsonPointerTarget {
|
|
101
|
+
const segments = parseJsonPointer(pointer);
|
|
102
|
+
if (segments.length === 0) return { exists: true, value: document };
|
|
103
|
+
let current: JsonValue = document;
|
|
104
|
+
for (const segment of segments) {
|
|
105
|
+
if (Array.isArray(current)) {
|
|
106
|
+
const index = parseArrayIndex(segment, current.length, false);
|
|
107
|
+
if (index >= current.length) return { exists: false };
|
|
108
|
+
current = current[index] as JsonValue;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (isJsonObject(current)) {
|
|
112
|
+
if (!Object.hasOwn(current, segment)) return { exists: false };
|
|
113
|
+
current = current[segment] as JsonValue;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
return { exists: false };
|
|
117
|
+
}
|
|
118
|
+
return { exists: true, value: current };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function applyJsonPatch(document: JsonValue, value: unknown): JsonValue {
|
|
122
|
+
const patch = validateJsonPatch(value);
|
|
123
|
+
let current = cloneJson(document);
|
|
124
|
+
for (const operation of patch) {
|
|
125
|
+
current = applyJsonPatchOperation(current, operation);
|
|
126
|
+
}
|
|
127
|
+
const bytes = Buffer.byteLength(canonicalJson(current), "utf8");
|
|
128
|
+
if (bytes > MAX_WORKFLOW_SETTINGS_BYTES) {
|
|
129
|
+
throw new Error(`JSON Patch result cannot exceed ${MAX_WORKFLOW_SETTINGS_BYTES} bytes`);
|
|
130
|
+
}
|
|
131
|
+
return current;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function applyJsonPatchOperation(
|
|
135
|
+
document: JsonValue,
|
|
136
|
+
operation: JsonPatchOperation,
|
|
137
|
+
): JsonValue {
|
|
138
|
+
const path = parseJsonPointer(operation.path);
|
|
139
|
+
switch (operation.op) {
|
|
140
|
+
case "add":
|
|
141
|
+
return addValue(document, path, cloneJson(operation.value));
|
|
142
|
+
case "remove":
|
|
143
|
+
return removeValue(document, path).document;
|
|
144
|
+
case "replace":
|
|
145
|
+
if (path.length === 0) return cloneJson(operation.value);
|
|
146
|
+
requireTarget(document, path, operation.path);
|
|
147
|
+
return addValue(document, path, cloneJson(operation.value), true);
|
|
148
|
+
case "copy": {
|
|
149
|
+
const source = requireTarget(document, parseJsonPointer(operation.from), operation.from);
|
|
150
|
+
return addValue(document, path, cloneJson(source.value));
|
|
151
|
+
}
|
|
152
|
+
case "move": {
|
|
153
|
+
const from = parseJsonPointer(operation.from);
|
|
154
|
+
if (sameSegments(from, path)) return document;
|
|
155
|
+
if (from.length === 0) {
|
|
156
|
+
throw new Error("JSON Patch move cannot move the document root into a child path");
|
|
157
|
+
}
|
|
158
|
+
if (isPrefix(from, path)) {
|
|
159
|
+
throw new Error("JSON Patch move destination cannot be inside its source");
|
|
160
|
+
}
|
|
161
|
+
const removed = removeValue(document, from);
|
|
162
|
+
return addValue(removed.document, path, removed.value);
|
|
163
|
+
}
|
|
164
|
+
case "test": {
|
|
165
|
+
const target = requireTarget(document, path, operation.path);
|
|
166
|
+
if (canonicalJson(target.value) !== canonicalJson(operation.value)) {
|
|
167
|
+
throw new Error(`JSON Patch test failed at ${JSON.stringify(operation.path)}`);
|
|
168
|
+
}
|
|
169
|
+
return document;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function cloneJson<T extends JsonValue>(value: T): T {
|
|
175
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
176
|
+
if (typeof value === "number") {
|
|
177
|
+
if (!Number.isFinite(value)) throw new Error("JSON numbers must be finite");
|
|
178
|
+
return (Object.is(value, -0) ? 0 : value) as T;
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
return value.map((item) => cloneJson(item)) as T;
|
|
182
|
+
}
|
|
183
|
+
if (!isJsonObject(value)) {
|
|
184
|
+
throw new Error("Workflow settings must contain only JSON values");
|
|
185
|
+
}
|
|
186
|
+
const output: Record<string, JsonValue> = {};
|
|
187
|
+
for (const key of Object.keys(value)) {
|
|
188
|
+
defineJsonProperty(output, key, cloneJson(value[key] as JsonValue));
|
|
189
|
+
}
|
|
190
|
+
return output as T;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function validateOperation(value: unknown, index: number): JsonPatchOperation {
|
|
194
|
+
if (!isRecord(value) || typeof value.op !== "string" || typeof value.path !== "string") {
|
|
195
|
+
throw new Error(`JSON Patch operation ${index + 1} must contain string op and path fields`);
|
|
196
|
+
}
|
|
197
|
+
parseJsonPointer(value.path);
|
|
198
|
+
switch (value.op) {
|
|
199
|
+
case "add":
|
|
200
|
+
case "replace":
|
|
201
|
+
case "test":
|
|
202
|
+
assertExactFields(value, ["op", "path", "value"], index);
|
|
203
|
+
if (!Object.hasOwn(value, "value")) {
|
|
204
|
+
throw new Error(`JSON Patch ${value.op} operation ${index + 1} requires value`);
|
|
205
|
+
}
|
|
206
|
+
return { op: value.op, path: value.path, value: cloneJsonValue(value.value, index) };
|
|
207
|
+
case "remove":
|
|
208
|
+
assertExactFields(value, ["op", "path"], index);
|
|
209
|
+
return { op: "remove", path: value.path };
|
|
210
|
+
case "copy":
|
|
211
|
+
case "move":
|
|
212
|
+
assertExactFields(value, ["op", "path", "from"], index);
|
|
213
|
+
if (typeof value.from !== "string") {
|
|
214
|
+
throw new Error(`JSON Patch ${value.op} operation ${index + 1} requires string from`);
|
|
215
|
+
}
|
|
216
|
+
parseJsonPointer(value.from);
|
|
217
|
+
return { op: value.op, path: value.path, from: value.from };
|
|
218
|
+
default:
|
|
219
|
+
throw new Error(`Unknown JSON Patch operation ${JSON.stringify(value.op)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function cloneJsonValue(value: unknown, index: number): JsonValue {
|
|
224
|
+
try {
|
|
225
|
+
return cloneJson(value as JsonValue);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw new Error(`JSON Patch operation ${index + 1} value is not valid JSON`, { cause: error });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function assertExactFields(
|
|
232
|
+
value: Record<string, unknown>,
|
|
233
|
+
allowed: readonly string[],
|
|
234
|
+
index: number,
|
|
235
|
+
): void {
|
|
236
|
+
const allowedSet = new Set(allowed);
|
|
237
|
+
const unknown = Object.keys(value).find((key) => !allowedSet.has(key));
|
|
238
|
+
if (unknown !== undefined) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`JSON Patch operation ${index + 1} has unknown field ${JSON.stringify(unknown)}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function addValue(
|
|
246
|
+
document: JsonValue,
|
|
247
|
+
path: readonly string[],
|
|
248
|
+
value: JsonValue,
|
|
249
|
+
replaceOnly = false,
|
|
250
|
+
): JsonValue {
|
|
251
|
+
if (path.length === 0) return value;
|
|
252
|
+
const { parent, key } = resolveParent(document, path);
|
|
253
|
+
if (Array.isArray(parent)) {
|
|
254
|
+
if (replaceOnly) {
|
|
255
|
+
const index = parseArrayIndex(key, parent.length, false);
|
|
256
|
+
if (index >= parent.length) throw missingPath(encodeJsonPointer(path));
|
|
257
|
+
parent[index] = value;
|
|
258
|
+
return document;
|
|
259
|
+
}
|
|
260
|
+
const index = parseArrayIndex(key, parent.length, true);
|
|
261
|
+
if (index > parent.length) throw missingPath(encodeJsonPointer(path));
|
|
262
|
+
parent.splice(index, 0, value);
|
|
263
|
+
return document;
|
|
264
|
+
}
|
|
265
|
+
if (!isJsonObject(parent)) throw missingPath(encodeJsonPointer(path));
|
|
266
|
+
if (replaceOnly && !Object.hasOwn(parent, key)) throw missingPath(encodeJsonPointer(path));
|
|
267
|
+
defineJsonProperty(parent, key, value);
|
|
268
|
+
return document;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function removeValue(
|
|
272
|
+
document: JsonValue,
|
|
273
|
+
path: readonly string[],
|
|
274
|
+
): { document: JsonValue; value: JsonValue } {
|
|
275
|
+
if (path.length === 0) {
|
|
276
|
+
throw new Error("JSON Patch remove cannot remove the document root");
|
|
277
|
+
}
|
|
278
|
+
const { parent, key } = resolveParent(document, path);
|
|
279
|
+
if (Array.isArray(parent)) {
|
|
280
|
+
const index = parseArrayIndex(key, parent.length, false);
|
|
281
|
+
if (index >= parent.length) throw missingPath(encodeJsonPointer(path));
|
|
282
|
+
const [value] = parent.splice(index, 1);
|
|
283
|
+
return { document, value: value as JsonValue };
|
|
284
|
+
}
|
|
285
|
+
if (!isJsonObject(parent) || !Object.hasOwn(parent, key)) {
|
|
286
|
+
throw missingPath(encodeJsonPointer(path));
|
|
287
|
+
}
|
|
288
|
+
const value = parent[key] as JsonValue;
|
|
289
|
+
delete parent[key];
|
|
290
|
+
return { document, value };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function requireTarget(
|
|
294
|
+
document: JsonValue,
|
|
295
|
+
path: readonly string[],
|
|
296
|
+
pointer: string,
|
|
297
|
+
): { value: JsonValue } {
|
|
298
|
+
if (path.length === 0) return { value: document };
|
|
299
|
+
const { parent, key } = resolveParent(document, path);
|
|
300
|
+
if (Array.isArray(parent)) {
|
|
301
|
+
const index = parseArrayIndex(key, parent.length, false);
|
|
302
|
+
if (index >= parent.length) throw missingPath(pointer);
|
|
303
|
+
return { value: parent[index] as JsonValue };
|
|
304
|
+
}
|
|
305
|
+
if (!isJsonObject(parent) || !Object.hasOwn(parent, key)) throw missingPath(pointer);
|
|
306
|
+
return { value: parent[key] as JsonValue };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolveParent(
|
|
310
|
+
document: JsonValue,
|
|
311
|
+
path: readonly string[],
|
|
312
|
+
): { parent: JsonValue; key: string } {
|
|
313
|
+
let current = document;
|
|
314
|
+
for (const segment of path.slice(0, -1)) {
|
|
315
|
+
if (Array.isArray(current)) {
|
|
316
|
+
const index = parseArrayIndex(segment, current.length, false);
|
|
317
|
+
if (index >= current.length) throw missingPath(encodeJsonPointer(path));
|
|
318
|
+
current = current[index] as JsonValue;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (isJsonObject(current) && Object.hasOwn(current, segment)) {
|
|
322
|
+
current = current[segment] as JsonValue;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
throw missingPath(encodeJsonPointer(path));
|
|
326
|
+
}
|
|
327
|
+
return { parent: current, key: path.at(-1) as string };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseArrayIndex(segment: string, length: number, allowEnd: boolean): number {
|
|
331
|
+
if (allowEnd && segment === "-") return length;
|
|
332
|
+
if (!/^(0|[1-9][0-9]*)$/u.test(segment)) {
|
|
333
|
+
throw new Error(`Invalid JSON Patch array index: ${JSON.stringify(segment)}`);
|
|
334
|
+
}
|
|
335
|
+
const index = Number(segment);
|
|
336
|
+
if (!Number.isSafeInteger(index) || index > length || (!allowEnd && index === length)) {
|
|
337
|
+
return length + 1;
|
|
338
|
+
}
|
|
339
|
+
return index;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isPrefix(prefix: readonly string[], value: readonly string[]): boolean {
|
|
343
|
+
return prefix.length < value.length && prefix.every((segment, index) => value[index] === segment);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function sameSegments(left: readonly string[], right: readonly string[]): boolean {
|
|
347
|
+
return left.length === right.length && left.every((segment, index) => right[index] === segment);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function missingPath(path: string): Error {
|
|
351
|
+
return new Error(`JSON Patch path does not exist: ${JSON.stringify(path)}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function defineJsonProperty(
|
|
355
|
+
target: Record<string, JsonValue>,
|
|
356
|
+
key: string,
|
|
357
|
+
value: JsonValue,
|
|
358
|
+
): void {
|
|
359
|
+
Object.defineProperty(target, key, {
|
|
360
|
+
value,
|
|
361
|
+
enumerable: true,
|
|
362
|
+
configurable: true,
|
|
363
|
+
writable: true,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isJsonObject(value: JsonValue): value is { [key: string]: JsonValue } {
|
|
368
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
369
|
+
const prototype = Object.getPrototypeOf(value) as object | null;
|
|
370
|
+
return prototype === Object.prototype || prototype === null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
374
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
375
|
+
}
|
package/src/workflows/schema.ts
CHANGED
|
@@ -79,6 +79,9 @@ export function assertValidComputeNode(node: ComputeNodeDefinition, nodeId = "co
|
|
|
79
79
|
if (typeof node.run !== "function") {
|
|
80
80
|
fail(`node ${nodeId} requires a run function`);
|
|
81
81
|
}
|
|
82
|
+
if (node.settingsRoute !== undefined && node.settingsRoute !== true) {
|
|
83
|
+
fail(`node ${nodeId} settingsRoute must be true when provided`);
|
|
84
|
+
}
|
|
82
85
|
assertCommonNodeFields(node, nodeId);
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -250,7 +253,17 @@ function assertValidEdgeShape(edge: WorkflowEdge, index: number): void {
|
|
|
250
253
|
* Names claimed by `/workflow` subcommands; a workflow with one of these
|
|
251
254
|
* names could never be started because the keyword wins the argument slot.
|
|
252
255
|
*/
|
|
253
|
-
const RESERVED_WORKFLOW_NAMES = new Set([
|
|
256
|
+
const RESERVED_WORKFLOW_NAMES = new Set([
|
|
257
|
+
"answer",
|
|
258
|
+
"cancel",
|
|
259
|
+
"change-settings",
|
|
260
|
+
"list",
|
|
261
|
+
"pause",
|
|
262
|
+
"queue-follow-up",
|
|
263
|
+
"remove-follow-up",
|
|
264
|
+
"resume",
|
|
265
|
+
"status",
|
|
266
|
+
]);
|
|
254
267
|
|
|
255
268
|
export function assertValidWorkflowDefinitionShape(
|
|
256
269
|
definition: WorkflowDefinition,
|
|
@@ -274,6 +287,22 @@ export function assertValidWorkflowDefinitionShape(
|
|
|
274
287
|
fail("workflow contractId must be a stable identifier");
|
|
275
288
|
}
|
|
276
289
|
assertOptionalFunction(definition.input, "workflow input");
|
|
290
|
+
if (definition.settings !== undefined) {
|
|
291
|
+
assertRecord(definition.settings, "workflow settings");
|
|
292
|
+
if (
|
|
293
|
+
typeof definition.settings.initial !== "function" &&
|
|
294
|
+
definition.settings.initial === undefined
|
|
295
|
+
) {
|
|
296
|
+
fail("workflow settings requires initial");
|
|
297
|
+
}
|
|
298
|
+
if (typeof definition.settings.parse !== "function") {
|
|
299
|
+
fail("workflow settings requires a parse function");
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(definition.settings.paths)) {
|
|
302
|
+
fail("workflow settings paths must be an array");
|
|
303
|
+
}
|
|
304
|
+
assertOptionalFunction(definition.settings.validateChange, "workflow settings validateChange");
|
|
305
|
+
}
|
|
277
306
|
if (
|
|
278
307
|
definition.title !== undefined &&
|
|
279
308
|
typeof definition.title !== "string" &&
|
|
@@ -339,6 +368,7 @@ export function assertValidWorkflowDefinitionShape(
|
|
|
339
368
|
fail(`include ${mountName} requires a workflow definition or reference`);
|
|
340
369
|
}
|
|
341
370
|
assertOptionalFunction(include.input, `include ${mountName} input`);
|
|
371
|
+
assertOptionalFunction(include.settings, `include ${mountName} settings`);
|
|
342
372
|
if (
|
|
343
373
|
include.contract !== undefined &&
|
|
344
374
|
(include.contract === null || typeof include.contract !== "object")
|