ai-workflow-logic 0.1.4 → 0.1.6
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/package.json +6 -4
- package/src/checkpoint-filesystem.ts +152 -13
- package/src/checkpoint-profile.ts +175 -0
- package/src/checkpoint-sidecar.ts +547 -0
- package/src/checkpoint-store.ts +73 -18
- package/src/errors.ts +17 -0
- package/src/index.ts +18 -0
- package/src/run-checkpoint.ts +43 -1
- package/src/step-extension.ts +121 -0
package/src/checkpoint-store.ts
CHANGED
|
@@ -11,6 +11,21 @@ import type {
|
|
|
11
11
|
} from 'ai-workflow-contract';
|
|
12
12
|
import type { WorkCtrlFlowSnapshot, WorkCtrlFlowStore } from 'work-ctrl-flow-contract';
|
|
13
13
|
import { normalizeFlowRunCheckpoint } from './run-checkpoint.js';
|
|
14
|
+
import {
|
|
15
|
+
FlowRunCheckpointTransitionError,
|
|
16
|
+
FlowRunCheckpointVersionConflictError,
|
|
17
|
+
} from './errors.js';
|
|
18
|
+
import {
|
|
19
|
+
validateCheckpointAgainstFrozenProfile,
|
|
20
|
+
type FlowRunCheckpointAdmissionRuntime,
|
|
21
|
+
} from './checkpoint-profile.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
FlowRunCheckpointTransitionError,
|
|
25
|
+
FlowRunCheckpointVersionConflictError,
|
|
26
|
+
} from './errors.js';
|
|
27
|
+
|
|
28
|
+
const compareCodeUnits = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
|
|
14
29
|
|
|
15
30
|
export interface WorkCtrlFlowCheckpointStoreOptions {
|
|
16
31
|
readonly instanceId: string;
|
|
@@ -110,23 +125,6 @@ export function createWorkCtrlFlowCheckpointStore(
|
|
|
110
125
|
};
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
export class FlowRunCheckpointVersionConflictError extends Error {
|
|
114
|
-
constructor(
|
|
115
|
-
public readonly expectedVersion: number | null,
|
|
116
|
-
public readonly actualVersion: number | null,
|
|
117
|
-
) {
|
|
118
|
-
super(`Flow run checkpoint version conflict: expected ${String(expectedVersion)}, actual ${String(actualVersion)}`);
|
|
119
|
-
this.name = 'FlowRunCheckpointVersionConflictError';
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export class FlowRunCheckpointTransitionError extends Error {
|
|
124
|
-
constructor(message: string) {
|
|
125
|
-
super(message);
|
|
126
|
-
this.name = 'FlowRunCheckpointTransitionError';
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
128
|
function checkpointKey(key: FlowRunCheckpointKey): string {
|
|
131
129
|
return JSON.stringify([key.instanceId, key.runId]);
|
|
132
130
|
}
|
|
@@ -552,15 +550,71 @@ export function validateFlowRunCheckpointTransition(
|
|
|
552
550
|
next as AIWorkflowFlowRunCheckpoint & { readonly profile: { readonly kind: 'AIDataWorkflow'; readonly runGraph: FlowClosedObject } },
|
|
553
551
|
);
|
|
554
552
|
}
|
|
553
|
+
validateStepExtensionTransition(current, next);
|
|
555
554
|
validateAgentStateTransition(current, next);
|
|
556
555
|
}
|
|
557
556
|
|
|
557
|
+
function validateStepExtensionTransition(
|
|
558
|
+
current: AIWorkflowFlowRunCheckpoint,
|
|
559
|
+
next: AIWorkflowFlowRunCheckpoint,
|
|
560
|
+
): void {
|
|
561
|
+
const before = current.stepExtensions;
|
|
562
|
+
const after = next.stepExtensions;
|
|
563
|
+
if (before === undefined || after === undefined) {
|
|
564
|
+
if (before !== after) {
|
|
565
|
+
throw new FlowRunCheckpointTransitionError('Run step extension membership is immutable after checkpoint version zero.');
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const beforeSteps = Object.keys(before.byStepId).sort(compareCodeUnits);
|
|
570
|
+
const afterSteps = Object.keys(after.byStepId).sort(compareCodeUnits);
|
|
571
|
+
if (!sameClosedValue(beforeSteps, afterSteps)) {
|
|
572
|
+
throw new FlowRunCheckpointTransitionError('Run step extension step membership is immutable.');
|
|
573
|
+
}
|
|
574
|
+
let changed = 0;
|
|
575
|
+
for (const stepId of beforeSteps) {
|
|
576
|
+
const beforeKinds = Object.keys(before.byStepId[stepId]).sort(compareCodeUnits);
|
|
577
|
+
const afterKinds = Object.keys(after.byStepId[stepId]).sort(compareCodeUnits);
|
|
578
|
+
if (!sameClosedValue(beforeKinds, afterKinds)) {
|
|
579
|
+
throw new FlowRunCheckpointTransitionError(`Run step extension kind membership is immutable for ${stepId}.`);
|
|
580
|
+
}
|
|
581
|
+
for (const kind of beforeKinds) {
|
|
582
|
+
const prior = before.byStepId[stepId][kind];
|
|
583
|
+
const candidate = after.byStepId[stepId][kind];
|
|
584
|
+
if (prior.schemaRef !== candidate.schemaRef) {
|
|
585
|
+
throw new FlowRunCheckpointTransitionError(`Run step extension schema is immutable for ${stepId}/${kind}.`);
|
|
586
|
+
}
|
|
587
|
+
if (sameClosedValue(prior, candidate)) continue;
|
|
588
|
+
changed += 1;
|
|
589
|
+
if (candidate.revision !== prior.revision + 1) {
|
|
590
|
+
throw new FlowRunCheckpointTransitionError(`Run step extension revision must advance exactly once for ${stepId}/${kind}.`);
|
|
591
|
+
}
|
|
592
|
+
if (sameClosedValue(prior.value, candidate.value)) {
|
|
593
|
+
throw new FlowRunCheckpointTransitionError(`Run step extension ${stepId}/${kind} cannot advance without a value change.`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (changed > 1) {
|
|
598
|
+
throw new FlowRunCheckpointTransitionError('One checkpoint transition may mutate at most one run step extension fact.');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
558
602
|
/** In-memory semantic owner used to validate the port before physical CAS is introduced. */
|
|
559
603
|
export class InMemoryFlowRunCheckpointStore implements FlowRunCheckpointStore<FlowRunProfileState> {
|
|
560
604
|
readonly #checkpoints = new Map<string, AIWorkflowFlowRunCheckpoint>();
|
|
605
|
+
readonly #admission: FlowRunCheckpointAdmissionRuntime;
|
|
606
|
+
|
|
607
|
+
constructor(options: Partial<FlowRunCheckpointAdmissionRuntime> = {}) {
|
|
608
|
+
this.#admission = Object.freeze({
|
|
609
|
+
...(options.profileAuthority === undefined ? {} : { profileAuthority: options.profileAuthority }),
|
|
610
|
+
extensionCodecs: options.extensionCodecs ?? Object.freeze({ resolve: () => undefined }),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
561
613
|
|
|
562
614
|
async load(key: FlowRunCheckpointKey): Promise<AIWorkflowFlowRunCheckpoint | undefined> {
|
|
563
|
-
|
|
615
|
+
const checkpoint = this.#checkpoints.get(checkpointKey(key));
|
|
616
|
+
if (checkpoint) validateCheckpointAgainstFrozenProfile(checkpoint, this.#admission);
|
|
617
|
+
return checkpoint;
|
|
564
618
|
}
|
|
565
619
|
|
|
566
620
|
async commit(input: FlowRunCheckpointCommit<FlowRunProfileState>): Promise<AIWorkflowFlowRunCheckpoint> {
|
|
@@ -572,6 +626,7 @@ export class InMemoryFlowRunCheckpointStore implements FlowRunCheckpointStore<Fl
|
|
|
572
626
|
throw new FlowRunCheckpointVersionConflictError(input.expectedVersion, actualVersion);
|
|
573
627
|
}
|
|
574
628
|
validateFlowRunCheckpointTransition(current, checkpoint);
|
|
629
|
+
validateCheckpointAgainstFrozenProfile(checkpoint, this.#admission);
|
|
575
630
|
this.#checkpoints.set(key, checkpoint);
|
|
576
631
|
return checkpoint;
|
|
577
632
|
}
|
package/src/errors.ts
CHANGED
|
@@ -13,3 +13,20 @@ export class AIWorkflowResourceProjectionError extends AIWorkflowFoundationError
|
|
|
13
13
|
this.name = 'AIWorkflowResourceProjectionError';
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export class FlowRunCheckpointVersionConflictError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
public readonly expectedVersion: number | null,
|
|
20
|
+
public readonly actualVersion: number | null,
|
|
21
|
+
) {
|
|
22
|
+
super(`Flow run checkpoint version conflict: expected ${String(expectedVersion)}, actual ${String(actualVersion)}`);
|
|
23
|
+
this.name = 'FlowRunCheckpointVersionConflictError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class FlowRunCheckpointTransitionError extends Error {
|
|
28
|
+
constructor(message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'FlowRunCheckpointTransitionError';
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,24 @@ export {
|
|
|
66
66
|
projectFlowRunCheckpoint,
|
|
67
67
|
} from './checkpoint-store.js';
|
|
68
68
|
export type { WorkCtrlFlowCheckpointStoreOptions } from './checkpoint-store.js';
|
|
69
|
+
export {
|
|
70
|
+
createInitialRunStepExtensions,
|
|
71
|
+
validateCheckpointAgainstFrozenProfile,
|
|
72
|
+
} from './checkpoint-profile.js';
|
|
73
|
+
export type {
|
|
74
|
+
FlowRunCheckpointAdmissionRuntime,
|
|
75
|
+
FlowRunProfileAuthorityPort,
|
|
76
|
+
FlowRunProfileAuthorityRequest,
|
|
77
|
+
FrozenFlowRunProfileAuthority,
|
|
78
|
+
FrozenRunStepExtensionDefinition,
|
|
79
|
+
} from './checkpoint-profile.js';
|
|
80
|
+
export { mutateRunStepExtension } from './step-extension.js';
|
|
81
|
+
export type {
|
|
82
|
+
RunStepExtensionMutationConfig,
|
|
83
|
+
RunStepExtensionMutationInvocation,
|
|
84
|
+
RunStepExtensionMutationRuntime,
|
|
85
|
+
RunStepExtensionSelector,
|
|
86
|
+
} from './step-extension.js';
|
|
69
87
|
|
|
70
88
|
export class AIWorkflowEffectHandlerNotFoundError extends AIWorkflowFoundationError {
|
|
71
89
|
constructor(
|
package/src/run-checkpoint.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function normalizeFlowRunCheckpoint(value: unknown): AIWorkflowFlowRunChe
|
|
|
65
65
|
'nodeSidecars',
|
|
66
66
|
'profile',
|
|
67
67
|
],
|
|
68
|
-
[],
|
|
68
|
+
['stepExtensions'],
|
|
69
69
|
'checkpoint',
|
|
70
70
|
);
|
|
71
71
|
const schemaVersion = dataField(record, 'schemaVersion', 'checkpoint.schemaVersion');
|
|
@@ -88,6 +88,9 @@ export function normalizeFlowRunCheckpoint(value: unknown): AIWorkflowFlowRunChe
|
|
|
88
88
|
dataField(record, 'nodeSidecars', 'checkpoint.nodeSidecars'),
|
|
89
89
|
'checkpoint.nodeSidecars',
|
|
90
90
|
);
|
|
91
|
+
const stepExtensions = Object.prototype.hasOwnProperty.call(record, 'stepExtensions')
|
|
92
|
+
? normalizeRunStepExtensions(dataField(record, 'stepExtensions', 'checkpoint.stepExtensions'))
|
|
93
|
+
: undefined;
|
|
91
94
|
const profile = normalizeProfile(dataField(record, 'profile', 'checkpoint.profile'), runId);
|
|
92
95
|
validateProfileProjection(profile, { input, state, output, controllerSidecars, nodeSidecars });
|
|
93
96
|
return Object.freeze({
|
|
@@ -102,10 +105,49 @@ export function normalizeFlowRunCheckpoint(value: unknown): AIWorkflowFlowRunChe
|
|
|
102
105
|
output,
|
|
103
106
|
controllerSidecars,
|
|
104
107
|
nodeSidecars,
|
|
108
|
+
...(stepExtensions === undefined ? {} : { stepExtensions }),
|
|
105
109
|
profile,
|
|
106
110
|
});
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
function normalizeRunStepExtensions(value: unknown) {
|
|
114
|
+
const root = exactRecord(value, ['schemaVersion', 'byStepId'], [], 'checkpoint.stepExtensions');
|
|
115
|
+
if (dataField(root, 'schemaVersion', 'checkpoint.stepExtensions.schemaVersion') !== 'depa.flow-run-step-extensions/v1') {
|
|
116
|
+
invalid('FLOW_RUN_STEP_EXTENSIONS_SCHEMA_INVALID', 'checkpoint.stepExtensions.schemaVersion', 'Step extension state schema is invalid.');
|
|
117
|
+
}
|
|
118
|
+
const byStepIdValue = plainRecord(dataField(root, 'byStepId', 'checkpoint.stepExtensions.byStepId'), 'checkpoint.stepExtensions.byStepId');
|
|
119
|
+
const byStepId: Record<string, Record<string, { readonly schemaRef?: string; readonly revision: number; readonly value: FlowClosedValue }>> = {};
|
|
120
|
+
for (const stepId of stringDataKeys(byStepIdValue, 'checkpoint.stepExtensions.byStepId').sort(compareCodeUnits)) {
|
|
121
|
+
canonicalIdentity(stepId, `checkpoint.stepExtensions.byStepId.${stepId}`);
|
|
122
|
+
const extensionsValue = plainRecord(dataField(byStepIdValue, stepId, `checkpoint.stepExtensions.byStepId.${stepId}`), `checkpoint.stepExtensions.byStepId.${stepId}`);
|
|
123
|
+
const extensionFacts: Record<string, { readonly schemaRef?: string; readonly revision: number; readonly value: FlowClosedValue }> = {};
|
|
124
|
+
for (const kind of stringDataKeys(extensionsValue, `checkpoint.stepExtensions.byStepId.${stepId}`).sort(compareCodeUnits)) {
|
|
125
|
+
canonicalIdentity(kind, `checkpoint.stepExtensions.byStepId.${stepId}.${kind}`);
|
|
126
|
+
const location = `checkpoint.stepExtensions.byStepId.${stepId}.${kind}`;
|
|
127
|
+
const fact = exactRecord(dataField(extensionsValue, kind, location), ['revision', 'value'], ['schemaRef'], location);
|
|
128
|
+
const schemaRef = Object.prototype.hasOwnProperty.call(fact, 'schemaRef')
|
|
129
|
+
? canonicalIdentity(dataField(fact, 'schemaRef', `${location}.schemaRef`), `${location}.schemaRef`)
|
|
130
|
+
: undefined;
|
|
131
|
+
extensionFacts[kind] = Object.freeze({
|
|
132
|
+
...(schemaRef === undefined ? {} : { schemaRef }),
|
|
133
|
+
revision: nonNegativeSafeInteger(dataField(fact, 'revision', `${location}.revision`), `${location}.revision`),
|
|
134
|
+
value: cloneClosedValue(dataField(fact, 'value', `${location}.value`), `${location}.value`),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (Object.keys(extensionFacts).length === 0) {
|
|
138
|
+
invalid('FLOW_RUN_STEP_EXTENSIONS_EMPTY_STEP', `checkpoint.stepExtensions.byStepId.${stepId}`, 'Extension step entries cannot be empty.');
|
|
139
|
+
}
|
|
140
|
+
byStepId[stepId] = Object.freeze(extensionFacts);
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(byStepId).length === 0) {
|
|
143
|
+
invalid('FLOW_RUN_STEP_EXTENSIONS_EMPTY', 'checkpoint.stepExtensions.byStepId', 'Empty extension state must be omitted.');
|
|
144
|
+
}
|
|
145
|
+
return Object.freeze({
|
|
146
|
+
schemaVersion: 'depa.flow-run-step-extensions/v1' as const,
|
|
147
|
+
byStepId: Object.freeze(byStepId),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
function validateProfileProjection(
|
|
110
152
|
profile: FlowRunProfileState,
|
|
111
153
|
envelope: {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowClosedValue,
|
|
3
|
+
FlowRunCheckpointRuntime,
|
|
4
|
+
FlowRunStepExtensions,
|
|
5
|
+
} from 'ai-workflow-contract';
|
|
6
|
+
import type { DefinitionStepExtensionCodecRegistryPort } from 'flow-step-space-contract';
|
|
7
|
+
import {
|
|
8
|
+
validateCheckpointAgainstFrozenProfile,
|
|
9
|
+
type FlowRunProfileAuthorityPort,
|
|
10
|
+
} from './checkpoint-profile.js';
|
|
11
|
+
import { normalizeFlowClosedValue } from './run-checkpoint.js';
|
|
12
|
+
|
|
13
|
+
export interface RunStepExtensionMutationRuntime extends FlowRunCheckpointRuntime {
|
|
14
|
+
readonly profileAuthority: FlowRunProfileAuthorityPort;
|
|
15
|
+
readonly extensionCodecs: DefinitionStepExtensionCodecRegistryPort;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RunStepExtensionSelector {
|
|
19
|
+
readonly instanceId: string;
|
|
20
|
+
readonly runId: string;
|
|
21
|
+
readonly stepId: string;
|
|
22
|
+
readonly kind: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RunStepExtensionMutationInvocation {
|
|
26
|
+
readonly expectedRevision: number;
|
|
27
|
+
readonly value: FlowClosedValue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RunStepExtensionMutationConfig {}
|
|
31
|
+
|
|
32
|
+
/** Targeted Processor: output = fn(runtime, selector, invocation, config). */
|
|
33
|
+
export async function mutateRunStepExtension(
|
|
34
|
+
runtime: RunStepExtensionMutationRuntime,
|
|
35
|
+
selector: RunStepExtensionSelector,
|
|
36
|
+
invocation: RunStepExtensionMutationInvocation,
|
|
37
|
+
config: RunStepExtensionMutationConfig,
|
|
38
|
+
) {
|
|
39
|
+
exactOwnData(config, [], 'config');
|
|
40
|
+
const selectorRecord = exactOwnData(selector, ['instanceId', 'runId', 'stepId', 'kind'], 'selector');
|
|
41
|
+
const normalizedSelector = Object.fromEntries(['instanceId', 'runId', 'stepId', 'kind'].map((name) => [
|
|
42
|
+
name,
|
|
43
|
+
ownDataValue(selectorRecord, name, `selector.${name}`),
|
|
44
|
+
])) as unknown as RunStepExtensionSelector;
|
|
45
|
+
for (const [name, value] of Object.entries(normalizedSelector)) {
|
|
46
|
+
if (typeof value !== 'string' || !value || value !== value.trim()) throw new Error(`Run step extension selector ${name} is invalid.`);
|
|
47
|
+
}
|
|
48
|
+
const invocationRecord = exactOwnData(invocation, ['expectedRevision', 'value'], 'invocation');
|
|
49
|
+
const expectedRevision = ownDataValue(invocationRecord, 'expectedRevision', 'invocation.expectedRevision');
|
|
50
|
+
const invocationValue = ownDataValue(invocationRecord, 'value', 'invocation.value') as FlowClosedValue;
|
|
51
|
+
if (!Number.isSafeInteger(expectedRevision) || (expectedRevision as number) < 0) {
|
|
52
|
+
throw new Error('Run step extension expectedRevision is invalid.');
|
|
53
|
+
}
|
|
54
|
+
const checkpoint = await runtime.checkpointStore.load({
|
|
55
|
+
instanceId: normalizedSelector.instanceId,
|
|
56
|
+
runId: normalizedSelector.runId,
|
|
57
|
+
});
|
|
58
|
+
if (!checkpoint) throw new Error(`Run ${normalizedSelector.runId} does not exist.`);
|
|
59
|
+
const normalized = checkpoint;
|
|
60
|
+
const authority = validateCheckpointAgainstFrozenProfile(normalized, runtime);
|
|
61
|
+
if (!authority) throw new Error('Run step extension mutation requires frozen profile authority.');
|
|
62
|
+
const definition = authority.extensionsByStepId[normalizedSelector.stepId]?.[normalizedSelector.kind];
|
|
63
|
+
const fact = normalized.stepExtensions?.byStepId[normalizedSelector.stepId]?.[normalizedSelector.kind];
|
|
64
|
+
if (!definition || !fact) throw new Error(`Run step extension ${normalizedSelector.stepId}/${normalizedSelector.kind} is not admitted.`);
|
|
65
|
+
if (fact.revision !== expectedRevision) {
|
|
66
|
+
throw new Error(`Run step extension revision conflict: expected ${String(expectedRevision)}, actual ${fact.revision}.`);
|
|
67
|
+
}
|
|
68
|
+
const registration = runtime.extensionCodecs.resolve(normalizedSelector.kind);
|
|
69
|
+
if (!registration?.codec || registration.schemaRef !== definition.schemaRef) {
|
|
70
|
+
throw new Error(`Run step extension codec is unavailable for ${normalizedSelector.stepId}/${normalizedSelector.kind}.`);
|
|
71
|
+
}
|
|
72
|
+
const value = normalizeFlowClosedValue(registration.codec.normalize(invocationValue as never, {
|
|
73
|
+
kind: normalizedSelector.kind,
|
|
74
|
+
...(definition.schemaRef === undefined ? {} : { schemaRef: definition.schemaRef }),
|
|
75
|
+
sourceRef: `frozen-step://${normalizedSelector.stepId}/${normalizedSelector.kind}`,
|
|
76
|
+
stepId: normalizedSelector.stepId,
|
|
77
|
+
}), `runStepExtension.${normalizedSelector.stepId}.${normalizedSelector.kind}`);
|
|
78
|
+
const stepExtensions: FlowRunStepExtensions = {
|
|
79
|
+
schemaVersion: 'depa.flow-run-step-extensions/v1',
|
|
80
|
+
byStepId: {
|
|
81
|
+
...normalized.stepExtensions!.byStepId,
|
|
82
|
+
[normalizedSelector.stepId]: {
|
|
83
|
+
...normalized.stepExtensions!.byStepId[normalizedSelector.stepId],
|
|
84
|
+
[normalizedSelector.kind]: {
|
|
85
|
+
...(definition.schemaRef === undefined ? {} : { schemaRef: definition.schemaRef }),
|
|
86
|
+
revision: fact.revision + 1,
|
|
87
|
+
value,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
return runtime.checkpointStore.commit({
|
|
93
|
+
expectedVersion: normalized.version,
|
|
94
|
+
checkpoint: {
|
|
95
|
+
...normalized,
|
|
96
|
+
version: normalized.version + 1,
|
|
97
|
+
stepExtensions,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function exactOwnData(value: unknown, fields: readonly string[], label: string): Record<string, unknown> {
|
|
103
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be a plain own-data object.`);
|
|
104
|
+
const prototype = Object.getPrototypeOf(value);
|
|
105
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} must be a plain own-data object.`);
|
|
106
|
+
const keys = Reflect.ownKeys(value);
|
|
107
|
+
if (keys.some((key) => typeof key !== 'string')) throw new Error(`${label} contains a symbol field.`);
|
|
108
|
+
const actual = (keys as string[]).sort(compareCodeUnits);
|
|
109
|
+
const expected = [...fields].sort(compareCodeUnits);
|
|
110
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`${label} fields are invalid.`);
|
|
111
|
+
for (const key of actual) ownDataValue(value as Record<string, unknown>, key, `${label}.${key}`);
|
|
112
|
+
return value as Record<string, unknown>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ownDataValue(record: Record<string, unknown>, key: string, label: string): unknown {
|
|
116
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
117
|
+
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) throw new Error(`${label} must be an enumerable own-data field.`);
|
|
118
|
+
return descriptor.value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const compareCodeUnits = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
|