ai-workflow-logic 0.2.0 → 0.2.2
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 +11 -9
- package/src/agent-code-brand.ts +14 -0
- package/src/agent-code-execution.ts +98 -0
- package/src/agent-code-projection.ts +98 -0
- package/src/agent-definition-selection.ts +256 -17
- package/src/agent-execution-resources.ts +160 -0
- package/src/agent-invocation.ts +62 -2
- package/src/agent-resources.ts +3 -0
- package/src/agent-state.ts +3 -2
- package/src/browser.ts +1 -1
- package/src/checkpoint-filesystem.ts +23 -10
- package/src/checkpoint-profile.ts +3 -0
- package/src/checkpoint-sidecar.ts +77 -16
- package/src/checkpoint-store.ts +58 -8
- package/src/core.ts +544 -0
- package/src/definitions.ts +30 -0
- package/src/delegation.ts +387 -0
- package/src/index.ts +7 -563
- package/src/resource-resolution.ts +38 -14
- package/src/run-checkpoint.ts +30 -6
- package/src/run-freeze.ts +80 -1
package/src/agent-invocation.ts
CHANGED
|
@@ -8,9 +8,11 @@ import type {
|
|
|
8
8
|
AIAgentRunResult,
|
|
9
9
|
AIAgentSelector,
|
|
10
10
|
AIWorkflowAuthoredEffects,
|
|
11
|
+
AIWorkflowNodeTasks,
|
|
11
12
|
AIWorkflowFlowRunCheckpoint,
|
|
12
13
|
AIWorkflowProfileDurableState,
|
|
13
14
|
FlowClosedValue,
|
|
15
|
+
FlowClosedObject,
|
|
14
16
|
} from 'ai-workflow-contract';
|
|
15
17
|
import {
|
|
16
18
|
AIAgentInvocationError,
|
|
@@ -58,6 +60,37 @@ function same(left: unknown, right: unknown): boolean {
|
|
|
58
60
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
function assertNoTaskSourceOverride(checkpoint: AIWorkflowFlowRunCheckpoint, nodeId: string): void {
|
|
64
|
+
const object = (value: FlowClosedValue): value is FlowClosedObject => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
65
|
+
const bindings = object(checkpoint.config) ? checkpoint.config.workflowDelegations : undefined;
|
|
66
|
+
if (bindings && typeof bindings === 'object' && !Array.isArray(bindings) && Object.hasOwn(bindings, nodeId)) {
|
|
67
|
+
fail('AI_AGENT_TASK_DELEGATION_REQUIRED', 'checkpoint.config.workflowDelegations.' + nodeId,
|
|
68
|
+
'This node has an explicit task source. Use ai.delegation.delegateTask so its admitted owner and context cannot be silently ignored.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The canonical instance index owns context reuse; the Host still owns the Actor. */
|
|
73
|
+
function frozenAgentContext(checkpoint:AIWorkflowFlowRunCheckpoint,runtime:AIAgentProcessorRuntime,config:AIAgentEffectConfig):{name?:string;generation:number}|undefined {
|
|
74
|
+
const object=(value:FlowClosedValue|undefined):value is FlowClosedObject=>value!==undefined&&value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
75
|
+
const bindings=normalizeFlowClosedObject(checkpoint.config).workflowAgentContexts;
|
|
76
|
+
if(bindings===undefined)return;
|
|
77
|
+
if(!object(bindings))fail('AI_AGENT_CONTEXT_INVALID','checkpoint.config.workflowAgentContexts','Frozen Agent contexts must be an object.');
|
|
78
|
+
if(!Object.hasOwn(bindings,runtime.nodeId))return;
|
|
79
|
+
const choice=bindings[runtime.nodeId];
|
|
80
|
+
if(!object(choice)||choice.schemaVersion!=='workflow.agent-context/v1'
|
|
81
|
+
||choice.agentDefinitionRef!==config.agentDefinitionRef||!['authored','shared','independent-per-generation'].includes(String(choice.sessionPolicy))
|
|
82
|
+
||choice.nodeIdentity!==undefined&&choice.nodeIdentity!==true
|
|
83
|
+
||Object.keys(choice).some(key=>!['schemaVersion','agentDefinitionRef','sessionPolicy','nodeIdentity'].includes(key)))
|
|
84
|
+
fail('AI_AGENT_CONTEXT_INVALID','checkpoint.config.workflowAgentContexts.'+runtime.nodeId,'Frozen Agent context must match the exact node task definition and supported policy.');
|
|
85
|
+
const generation=runtime.nodeContext?.generation??runtime.generation,scope=runtime.nodeContext?.scope??'';
|
|
86
|
+
if(generation===undefined||!Number.isSafeInteger(generation)||generation<0||typeof scope!=='string'||scope.normalize('NFC')!==scope)
|
|
87
|
+
fail('AI_AGENT_CONTEXT_IDENTITY_REQUIRED','runtime.nodeContext','A frozen Agent context requires its actual native generation and scope.');
|
|
88
|
+
if(choice.sessionPolicy==='authored')return {generation};
|
|
89
|
+
const name='workflow-agent-context:'+JSON.stringify([runtime.nodeId,scope,choice.sessionPolicy==='shared'?null:generation]);
|
|
90
|
+
if(config.instanceName!==undefined&&config.instanceName!==name)fail('AI_AGENT_CONTEXT_NAME_CONFLICT','config.instanceName','An explicit instance name cannot replace the frozen node context identity.');
|
|
91
|
+
return {name,generation};
|
|
92
|
+
}
|
|
93
|
+
|
|
61
94
|
function semanticFacts(config: AIAgentEffectConfig, invocationMetadata?: Readonly<Record<string, FlowClosedValue>>): Readonly<Record<string, FlowClosedValue>> {
|
|
62
95
|
return normalizeFlowClosedObject({
|
|
63
96
|
materialRefs: config.materialRefs ?? [],
|
|
@@ -201,11 +234,16 @@ async function complete(runtime: AIAgentProcessorRuntime, checkpoint: AIWorkflow
|
|
|
201
234
|
|
|
202
235
|
export async function runAgent<Input extends FlowClosedValue = FlowClosedValue, Output extends FlowClosedValue = FlowClosedValue>(runtime: AIAgentProcessorRuntime, input: Input, configInput: AIAgentEffectConfig): Promise<AIAgentRunResult<Output>> {
|
|
203
236
|
const inputValue = normalizeFlowClosedValue(input, 'input') as Input;
|
|
204
|
-
|
|
237
|
+
let config = normalizeAIAgentEffectConfig(configInput);
|
|
205
238
|
assertTaskAuthority(runtime, config);
|
|
206
239
|
let checkpoint = await loadCheckpoint(runtime);
|
|
240
|
+
assertNoTaskSourceOverride(checkpoint, runtime.nodeId);
|
|
241
|
+
const context=frozenAgentContext(checkpoint,runtime,config);
|
|
242
|
+
if(context){runtime={...runtime,generation:context.generation};if(context.name!==undefined)config={...config,instanceName:context.name}}
|
|
207
243
|
let ai = profileAI(checkpoint);
|
|
208
244
|
let receipt = ai.invocationsByKey[runtime.invocationKey];
|
|
245
|
+
if(context?.name!==undefined&&(receipt?.mode==='targeted'||!receipt&&ai.instanceIdByName[context.name]!==undefined))
|
|
246
|
+
return runTargetedAgent<Input,Output>(runtime,{byName:context.name},{kind:'ai.agent',payload:inputValue},config);
|
|
209
247
|
if (receipt) {
|
|
210
248
|
assertSameInvocation(receipt, runtime, 'new', inputValue, config);
|
|
211
249
|
if (receipt.status === 'completed') return resultFromReceipt(ai, receipt) as AIAgentRunResult<Output>;
|
|
@@ -238,11 +276,15 @@ export async function runAgent<Input extends FlowClosedValue = FlowClosedValue,
|
|
|
238
276
|
export async function runTargetedAgent<Input extends FlowClosedValue = FlowClosedValue, Output extends FlowClosedValue = FlowClosedValue>(runtime: AIAgentProcessorRuntime, selectorInput: AIAgentSelector, invocationInput: AIAgentInvocation<Input>, configInput: AIAgentEffectConfig): Promise<AIAgentRunResult<Output>> {
|
|
239
277
|
const selector = normalizeAIAgentSelector(selectorInput);
|
|
240
278
|
const invocation = normalizeAIAgentInvocation<Input>(invocationInput);
|
|
241
|
-
|
|
279
|
+
let config = normalizeAIAgentEffectConfig(configInput);
|
|
242
280
|
assertTaskAuthority(runtime, config);
|
|
243
281
|
let checkpoint = await loadCheckpoint(runtime);
|
|
282
|
+
assertNoTaskSourceOverride(checkpoint, runtime.nodeId);
|
|
283
|
+
const context=frozenAgentContext(checkpoint,runtime,config);
|
|
284
|
+
if(context){runtime={...runtime,generation:context.generation};if(context.name!==undefined)config={...config,instanceName:context.name}}
|
|
244
285
|
let ai = profileAI(checkpoint);
|
|
245
286
|
const instance = resolveSelector(ai, selector, config);
|
|
287
|
+
if(context?.name!==undefined&&instance.instanceName!==context.name)fail('AI_AGENT_CONTEXT_TARGET_CONFLICT','selector','Target must be the original instance for this frozen node generation and scope.');
|
|
246
288
|
let receipt = ai.invocationsByKey[runtime.invocationKey];
|
|
247
289
|
if (receipt) {
|
|
248
290
|
assertSameInvocation(receipt, runtime, 'targeted', invocation.payload, config, invocation.metadata);
|
|
@@ -280,3 +322,21 @@ export function bindAIAgentProcessors(runtime: AIAgentProcessorRuntime): AIWorkf
|
|
|
280
322
|
runTargetedAgent: <Input extends FlowClosedValue, Output extends FlowClosedValue>(selector: AIAgentSelector, invocation: AIAgentInvocation<Input>, config: AIAgentEffectConfig) => runTargetedAgent<Input, Output>(runtime, selector, invocation, config),
|
|
281
323
|
});
|
|
282
324
|
}
|
|
325
|
+
|
|
326
|
+
/** Source selection is an immutable admission fact; authored input cannot select another owner. */
|
|
327
|
+
export function bindWorkflowNodeTasks(runtime: AIAgentProcessorRuntime, delegation?: { delegateTask(input: FlowClosedValue): Promise<FlowClosedValue> }): AIWorkflowNodeTasks {
|
|
328
|
+
return Object.freeze({ async invoke(input: FlowClosedValue, configInput: AIAgentEffectConfig): Promise<FlowClosedValue> {
|
|
329
|
+
const config=normalizeAIAgentEffectConfig(configInput),value=normalizeFlowClosedValue(input);
|
|
330
|
+
assertTaskAuthority(runtime,config);
|
|
331
|
+
const checkpoint=await loadCheckpoint(runtime),closed=normalizeFlowClosedObject(checkpoint.config),bindings=closed.workflowDelegations;
|
|
332
|
+
if(bindings!==undefined&&(bindings===null||typeof bindings!=='object'||Array.isArray(bindings)))
|
|
333
|
+
fail('AI_AGENT_TASK_SOURCES_INVALID','checkpoint.config.workflowDelegations','Frozen node task sources must be an object.');
|
|
334
|
+
if(bindings&&Object.hasOwn(bindings,runtime.nodeId)){
|
|
335
|
+
if(Object.keys(config).some(key=>key!=='agentDefinitionRef'))
|
|
336
|
+
fail('AI_AGENT_TASK_EFFECT_OVERRIDE_UNSUPPORTED','config','Delegated tasks use their frozen materials, model and context; Agent instance or effect overrides cannot be ignored.');
|
|
337
|
+
if(!delegation)fail('AI_AGENT_TASK_OWNER_REQUIRED','runtime.delegation','The admitted node task owner is unavailable.');
|
|
338
|
+
return delegation.delegateTask(value);
|
|
339
|
+
}
|
|
340
|
+
return (await runAgent(runtime,value,config)).output;
|
|
341
|
+
}});
|
|
342
|
+
}
|
package/src/agent-resources.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
} from 'halfcode-compiler.xnl/resource-core';
|
|
26
26
|
import { markAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
|
|
27
27
|
import { AIWorkflowResourceProjectionError } from './errors.js';
|
|
28
|
+
import { projectAIAgentCodeResources } from './agent-code-projection.js';
|
|
28
29
|
|
|
29
30
|
const AGENT_SUBDOMAINS = Object.freeze([
|
|
30
31
|
'Messages',
|
|
@@ -43,6 +44,7 @@ export function projectAIWorkflowAgentResources(
|
|
|
43
44
|
const diagnostics: AIWorkflowResourceProjectionDiagnostic[] = [];
|
|
44
45
|
const resources = buildResourceIndex(registry, diagnostics);
|
|
45
46
|
const edges: ResourceDependencyEdge[] = [];
|
|
47
|
+
const codeResources = projectAIAgentCodeResources(resources, edges);
|
|
46
48
|
|
|
47
49
|
const materialPorts = recordsForKind(registry, 'MaterialPort')
|
|
48
50
|
.map((resource) => projectMaterialPort(resource, resources, edges, diagnostics))
|
|
@@ -60,6 +62,7 @@ export function projectAIWorkflowAgentResources(
|
|
|
60
62
|
|
|
61
63
|
if (diagnostics.length > 0) throwProjectionError(diagnostics);
|
|
62
64
|
return markAIWorkflowAgentResourceProjection(Object.freeze({
|
|
65
|
+
codeResources,
|
|
63
66
|
agentDefinitions: Object.freeze(agentDefinitions),
|
|
64
67
|
materialPorts: Object.freeze(materialPorts),
|
|
65
68
|
materialBindings: Object.freeze(materialBindings),
|
package/src/agent-state.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
LegacyAIWorkflowProfileDurableState,
|
|
13
13
|
} from 'ai-workflow-contract';
|
|
14
14
|
import { normalizeFlowClosedObject, normalizeFlowClosedValue } from './run-checkpoint.js';
|
|
15
|
+
import { normalizeWorkflowDelegations } from './delegation.js';
|
|
15
16
|
|
|
16
17
|
export class AIAgentInvocationError extends Error {
|
|
17
18
|
constructor(public readonly code: string, public readonly location: string, message: string) {
|
|
@@ -231,7 +232,7 @@ function normalizeReceipt(value: unknown, key: string, instances: Readonly<Recor
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
export function normalizeCanonicalAIAgentState(value: unknown, location = 'ai'): AIWorkflowProfileDurableState {
|
|
234
|
-
const input = record(value, ['schemaVersion', 'instancesById', 'instanceIdByName', 'invocationsByKey'], [], location);
|
|
235
|
+
const input = record(value, ['schemaVersion', 'instancesById', 'instanceIdByName', 'invocationsByKey'], ['delegationsByKey'], location);
|
|
235
236
|
if (input.schemaVersion !== 'depa.ai-agent-state/v1') fail('AI_AGENT_STATE_SCHEMA_UNSUPPORTED', `${location}.schemaVersion`, 'Unsupported Agent state schema.');
|
|
236
237
|
const instanceValues = objectMap(input.instancesById, `${location}.instancesById`);
|
|
237
238
|
const instancesById: Record<string, AIAgentInstanceReference> = {};
|
|
@@ -259,7 +260,7 @@ export function normalizeCanonicalAIAgentState(value: unknown, location = 'ai'):
|
|
|
259
260
|
}
|
|
260
261
|
invocationsByKey[key] = receipt;
|
|
261
262
|
}
|
|
262
|
-
return Object.freeze({ schemaVersion: 'depa.ai-agent-state/v1', instancesById: Object.freeze(instancesById), instanceIdByName: Object.freeze(instanceIdByName), invocationsByKey: Object.freeze(invocationsByKey) });
|
|
263
|
+
return Object.freeze({ schemaVersion: 'depa.ai-agent-state/v1', instancesById: Object.freeze(instancesById), instanceIdByName: Object.freeze(instanceIdByName), invocationsByKey: Object.freeze(invocationsByKey), ...(input.delegationsByKey === undefined ? {} : { delegationsByKey: normalizeWorkflowDelegations(input.delegationsByKey) }) });
|
|
263
264
|
}
|
|
264
265
|
|
|
265
266
|
export function upgradeLegacyAIWorkflowProfileDurableState(value: LegacyAIWorkflowProfileDurableState, agentDefinitionRefs: Readonly<Record<string, `resource://${string}`>>, location = 'legacyAI'): AIWorkflowProfileDurableState {
|
package/src/browser.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './core.js';
|
|
@@ -16,6 +16,7 @@ import type { WorkCtrlFlowSnapshot } from 'work-ctrl-flow-contract';
|
|
|
16
16
|
import {
|
|
17
17
|
createMaterializedWorkCtrlFlowEngineFromDirectory,
|
|
18
18
|
loadFrozenCtrlDefinitionStepIds,
|
|
19
|
+
loadFrozenCtrlDefinitionMembership,
|
|
19
20
|
loadMaterializedFlowInstance,
|
|
20
21
|
materializeFlowInstance,
|
|
21
22
|
type MaterializedWorkCtrlFlowDirectoryOptions,
|
|
@@ -293,7 +294,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
293
294
|
if (!fs.existsSync(locations.runDir)) return undefined;
|
|
294
295
|
assertExistingDirectory(locations.runDir, this.#realStoreRoot, 'flow-run-symlink-boundary');
|
|
295
296
|
if (!fs.existsSync(locations.checkpointFile)) return undefined;
|
|
296
|
-
const checkpoint =
|
|
297
|
+
const checkpoint = this.#readStorage(locations.checkpointFile).checkpoint;
|
|
297
298
|
this.#assertCheckpointIdentity(checkpoint, key, descriptor.definition);
|
|
298
299
|
this.#validateFrozenProfile(checkpoint);
|
|
299
300
|
return checkpoint;
|
|
@@ -317,7 +318,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
317
318
|
this.#assertRunPhysicalBoundary(locations);
|
|
318
319
|
removeUncommittedStaging(locations.stagingFile, locations.runDir, this.#realStoreRoot);
|
|
319
320
|
const currentStorage = fs.existsSync(locations.checkpointFile)
|
|
320
|
-
?
|
|
321
|
+
? this.#readStorage(locations.checkpointFile)
|
|
321
322
|
: undefined;
|
|
322
323
|
const current = currentStorage?.checkpoint;
|
|
323
324
|
if (current) {
|
|
@@ -338,13 +339,15 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
338
339
|
));
|
|
339
340
|
const encoded = encodeCheckpointSidecars(checkpoint, currentStorage?.tree, {
|
|
340
341
|
definitionStepIds,
|
|
342
|
+
...(checkpoint.profile.kind === 'AIDataWorkflow' ? {} : { definitionInvocationOwners: frozenAuthority?.definitionInvocationOwners
|
|
343
|
+
?? loadFrozenCtrlDefinitionMembership(path.join(locations.instanceDir, 'definition'), checkpoint.profile.kind, this.#extensionCodecs).invocationOwners }),
|
|
341
344
|
previousMaterialRecordDigests: currentStorage?.materialRecordDigests,
|
|
342
345
|
});
|
|
343
346
|
removeUnacceptedSidecarReceipt(locations.runDir, checkpoint.version, this.#realStoreRoot);
|
|
344
347
|
persistCheckpointSidecars(locations.runDir, encoded, this.#durability);
|
|
345
348
|
assertSidecarDirectories(locations.runDir, this.#realStoreRoot);
|
|
346
349
|
writeCheckpointCandidate(locations.stagingFile, encoded.head, this.#realStoreRoot, this.#durability);
|
|
347
|
-
const staged =
|
|
350
|
+
const staged = this.#readStorage(locations.stagingFile).checkpoint;
|
|
348
351
|
this.#assertCheckpointIdentity(staged, key, descriptor.definition);
|
|
349
352
|
this.#validateFrozenProfile(staged);
|
|
350
353
|
if (stableJson(staged) !== stableJson(checkpoint)) {
|
|
@@ -359,7 +362,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
359
362
|
assertRegularFile(locations.checkpointFile, this.#realStoreRoot, 'flow-run-checkpoint-invalid');
|
|
360
363
|
}
|
|
361
364
|
renameDurably(locations.stagingFile, locations.checkpointFile, 'head', this.#durability);
|
|
362
|
-
const accepted =
|
|
365
|
+
const accepted = this.#readStorage(locations.checkpointFile).checkpoint;
|
|
363
366
|
this.#assertCheckpointIdentity(accepted, key, descriptor.definition);
|
|
364
367
|
this.#validateFrozenProfile(accepted);
|
|
365
368
|
if (stableJson(accepted) !== stableJson(checkpoint)) {
|
|
@@ -452,7 +455,7 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
452
455
|
try {
|
|
453
456
|
this.#assertRunPhysicalBoundary(locations);
|
|
454
457
|
const checkpoint = fs.existsSync(locations.checkpointFile)
|
|
455
|
-
?
|
|
458
|
+
? this.#readStorage(locations.checkpointFile).checkpoint
|
|
456
459
|
: undefined;
|
|
457
460
|
if (!checkpoint) {
|
|
458
461
|
throw new LegacyWorkCtrlImportError(
|
|
@@ -574,6 +577,13 @@ export class FilesystemFlowRunCheckpointStore implements FlowRunCheckpointStore<
|
|
|
574
577
|
}, { requireAIDataAuthority: true });
|
|
575
578
|
}
|
|
576
579
|
|
|
580
|
+
#readStorage(file: string) {
|
|
581
|
+
return readCheckpointStorage(file, this.#realStoreRoot, {
|
|
582
|
+
profileAuthority: this.#profileAuthority,
|
|
583
|
+
extensionCodecs: this.#extensionCodecs,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
577
587
|
#ensureRunDirectory(locations: FlowRunFilesystemLocations): void {
|
|
578
588
|
ensureChildDirectory(locations.instanceDir, locations.runsDir, this.#realStoreRoot, this.#durability, 'head');
|
|
579
589
|
ensureChildDirectory(locations.runsDir, locations.runDir, this.#realStoreRoot, this.#durability, 'head');
|
|
@@ -1018,13 +1028,10 @@ function ensureChildDirectory(
|
|
|
1018
1028
|
assertExistingDirectory(child, realRoot, 'flow-run-boundary-invalid');
|
|
1019
1029
|
}
|
|
1020
1030
|
|
|
1021
|
-
function readCheckpointFile(file: string, realRoot: string): AIWorkflowFlowRunCheckpoint {
|
|
1022
|
-
return readCheckpointStorage(file, realRoot).checkpoint;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
1031
|
function readCheckpointStorage(
|
|
1026
1032
|
file: string,
|
|
1027
1033
|
realRoot: string,
|
|
1034
|
+
runtime: { readonly profileAuthority?: import('./checkpoint-profile.js').FlowRunProfileAuthorityPort; readonly extensionCodecs: import('flow-step-space-contract').DefinitionStepExtensionCodecRegistryPort },
|
|
1028
1035
|
): {
|
|
1029
1036
|
readonly checkpoint: AIWorkflowFlowRunCheckpoint;
|
|
1030
1037
|
readonly tree?: StepMerkleTree;
|
|
@@ -1034,7 +1041,13 @@ function readCheckpointStorage(
|
|
|
1034
1041
|
try {
|
|
1035
1042
|
if (isCheckpointHead(value)) {
|
|
1036
1043
|
assertSidecarDirectories(path.dirname(file), realRoot);
|
|
1037
|
-
const loaded = readCheckpointHeadAndLogical(file)
|
|
1044
|
+
const loaded = readCheckpointHeadAndLogical(file,{ctrlMembership(head,kind){
|
|
1045
|
+
assertSafeSegment(head.instanceId);
|
|
1046
|
+
const authority=runtime.profileAuthority?.load({instanceId:head.instanceId,definition:head.definition,profileKind:kind});
|
|
1047
|
+
return authority?.definitionInvocationOwners
|
|
1048
|
+
? {definitionStepIds:authority.definitionStepIds,invocationOwners:authority.definitionInvocationOwners}
|
|
1049
|
+
: loadFrozenCtrlDefinitionMembership(path.join(realRoot,'instances',head.instanceId,'definition'),kind,runtime.extensionCodecs);
|
|
1050
|
+
}});
|
|
1038
1051
|
return {
|
|
1039
1052
|
checkpoint: loaded.checkpoint,
|
|
1040
1053
|
tree: loaded.tree,
|
|
@@ -18,6 +18,7 @@ export interface FrozenRunStepExtensionDefinition {
|
|
|
18
18
|
export interface FrozenFlowRunProfileAuthority {
|
|
19
19
|
readonly profileKind: FlowRunProfileState['kind'];
|
|
20
20
|
readonly definitionStepIds: readonly string[];
|
|
21
|
+
readonly definitionInvocationOwners?: Readonly<Record<string, string>>;
|
|
21
22
|
readonly binding?: FlowClosedObject;
|
|
22
23
|
readonly extensionsByStepId: Readonly<
|
|
23
24
|
Record<string, Readonly<Record<string, FrozenRunStepExtensionDefinition>>>
|
|
@@ -103,6 +104,8 @@ export function validateCheckpointAgainstFrozenProfile(
|
|
|
103
104
|
if (ids.some((id) => typeof id !== 'string' || !id || id !== id.trim()) || new Set(ids).size !== ids.length) {
|
|
104
105
|
throw new FlowRunCheckpointTransitionError('Frozen profile step membership is invalid.');
|
|
105
106
|
}
|
|
107
|
+
if (Object.entries(authority.definitionInvocationOwners ?? {}).some(([key, owner]) => !key || !ids.includes(owner)))
|
|
108
|
+
throw new FlowRunCheckpointTransitionError('Frozen compiled action ownership is invalid.');
|
|
106
109
|
for (const stepId of Object.keys(authority.extensionsByStepId)) {
|
|
107
110
|
if (!ids.includes(stepId)) {
|
|
108
111
|
throw new FlowRunCheckpointTransitionError(`Frozen extension step ${stepId} is not a definition member.`);
|
|
@@ -42,8 +42,15 @@ export interface FlowRunCheckpointSidecarReceipt {
|
|
|
42
42
|
}
|
|
43
43
|
export interface CheckpointSidecarEncodeConfig {
|
|
44
44
|
readonly definitionStepIds?: readonly string[];
|
|
45
|
+
readonly definitionInvocationOwners?: Readonly<Record<string, string>>;
|
|
45
46
|
readonly previousMaterialRecordDigests?: readonly DefinitionStepDigest[];
|
|
46
47
|
}
|
|
48
|
+
export interface CheckpointSidecarReadConfig {
|
|
49
|
+
readonly ctrlMembership?: (head: FlowRunCheckpointHead, kind: 'WorkCtrlFlow' | 'AICtrlWorkflow') => {
|
|
50
|
+
readonly definitionStepIds: readonly string[];
|
|
51
|
+
readonly invocationOwners: Readonly<Record<string, string>>;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
47
54
|
type EncodedCheckpoint = {
|
|
48
55
|
readonly head: FlowRunCheckpointHead;
|
|
49
56
|
readonly controlRecords: readonly ImmutableStepRecord[];
|
|
@@ -123,12 +130,12 @@ function dataMembership(
|
|
|
123
130
|
const ORCHESTRATION_FIELDS = ['attempts', 'deadlines', 'lanes', 'winners', 'cancellations', 'composites'] as const;
|
|
124
131
|
type OrchestrationField = typeof ORCHESTRATION_FIELDS[number];
|
|
125
132
|
|
|
126
|
-
function runtimeCtrlMembers(orchestrationValue: unknown, definitionStepIds: readonly string[]): readonly string[] {
|
|
133
|
+
function runtimeCtrlMembers(orchestrationValue: unknown, definitionStepIds: readonly string[], invocationOwners: Readonly<Record<string, string>> = {}, waitOwners:ReadonlyMap<string,string>=new Map()): readonly string[] {
|
|
127
134
|
const orchestration = object(orchestrationValue, 'ctrl orchestration');
|
|
128
135
|
const members = new Set<string>();
|
|
129
136
|
for (const field of ORCHESTRATION_FIELDS) {
|
|
130
137
|
for (const [identity] of entries(orchestration[field], `ctrl orchestration.${field}`)) {
|
|
131
|
-
if (!definitionOwner(identity, definitionStepIds)) members.add(identity);
|
|
138
|
+
if (!definitionOwner(identity, definitionStepIds, invocationOwners)&&!waitOwners.has(identity)) members.add(identity);
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
return [...members].sort(compare);
|
|
@@ -161,22 +168,55 @@ function createRunStepLeaf(
|
|
|
161
168
|
});
|
|
162
169
|
return { leaf, materialRecords: [coreRecord, ...extensionRecords.map(({ record }) => record)] as readonly ImmutableStepRecord[] };
|
|
163
170
|
}
|
|
164
|
-
function definitionOwner(invocationKey: string, definitionStepIds: readonly string[]): string | undefined {
|
|
171
|
+
function definitionOwner(invocationKey: string, definitionStepIds: readonly string[], invocationOwners: Readonly<Record<string, string>> = {}): string | undefined {
|
|
165
172
|
const visit = invocationKey.lastIndexOf('#');
|
|
166
173
|
const stablePath = visit >= 0 ? invocationKey.slice(0, visit) : invocationKey;
|
|
167
174
|
const id = stablePath.slice(stablePath.lastIndexOf('/') + 1);
|
|
168
|
-
|
|
175
|
+
const owner = invocationOwners[id];
|
|
176
|
+
return definitionStepIds.includes(id) ? id : owner && definitionStepIds.includes(owner) ? owner : undefined;
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
|
|
179
|
+
/** Dynamic waits belong to the exact compiled action that created their native handle, never a prefix wildcard. */
|
|
180
|
+
function delegationWaitOwners(snapshotValue:unknown,definitionStepIds:readonly string[],invocationOwners:Readonly<Record<string,string>>,aiValue?:unknown):ReadonlyMap<string,string>{
|
|
181
|
+
const snapshot=object(snapshotValue,'ctrl snapshot'),owners=new Map<string,string>();
|
|
182
|
+
for(const field of ['closedWaitHandles','openWaitHandles']){
|
|
183
|
+
const handles=snapshot[field];if(!Array.isArray(handles))throw Error('Ctrl wait handles must be an array.');
|
|
184
|
+
for(const value of handles){
|
|
185
|
+
const handle=object(value,'ctrl wait handle');
|
|
186
|
+
if(!handle.metadata||typeof handle.metadata!=='object'||Array.isArray(handle.metadata))continue;
|
|
187
|
+
const metadata=object(handle.metadata,'ctrl wait metadata');
|
|
188
|
+
if(metadata.workflowDelegation===undefined)continue;
|
|
189
|
+
const delegated=object(metadata.workflowDelegation,'ctrl delegation wait');
|
|
190
|
+
const action=delegated.actionInvocationKey,invocation=delegated.invocationKey;
|
|
191
|
+
if(delegated.kind!=='workflow.task-delegation/v1'||typeof action!=='string'||typeof invocation!=='string'||typeof handle.invocationKey!=='string'||typeof handle.nodeKey!=='string'||handle.waitKind!=='external-job'||handle.signalKind!=='workflow.delegation.observe'||handle.signalKey!==action)throw Error('Ctrl delegation wait identity is invalid.');
|
|
192
|
+
const owner=definitionOwner(action,definitionStepIds,invocationOwners);
|
|
193
|
+
const actionPath=action.slice(0,action.lastIndexOf('#')),scope=actionPath.slice(0,actionPath.lastIndexOf('/')+1),node=actionPath.slice(actionPath.lastIndexOf('/')+1);
|
|
194
|
+
const prefix=`${node}:delegation:`;
|
|
195
|
+
if(!owner||definitionOwner(invocation,definitionStepIds,invocationOwners)!==owner||!handle.nodeKey.startsWith(prefix)||!/^(0|[1-9][0-9]*)$/.test(handle.nodeKey.slice(prefix.length))||!handle.invocationKey.startsWith(`${scope}${handle.nodeKey}#`)||!/^(0|[1-9][0-9]*)$/.test(handle.invocationKey.slice(`${scope}${handle.nodeKey}#`.length)))throw Error('Ctrl delegation wait is not owned by its frozen action.');
|
|
196
|
+
if(aiValue!==undefined){
|
|
197
|
+
const ai=object(aiValue,'ctrl AI state'),records=object(ai.delegationsByKey??{},'ctrl delegations'),record=object(records[invocation],'ctrl delegation record');
|
|
198
|
+
if(!Array.isArray(record.attempts)||record.attempts.length===0)throw Error('Ctrl delegation wait has no accepted invocation.');
|
|
199
|
+
const attempt=object(record.attempts.at(-1),'ctrl delegation attempt'),request=object(attempt.request,'ctrl delegation request'),lineage=object(request.lineage,'ctrl delegation lineage');
|
|
200
|
+
if(attempt.dispatchClaimed!==true||lineage.invocationKey!==invocation||lineage.nodeId!==owner)throw Error('Ctrl delegation wait receipt does not match its frozen action.');
|
|
201
|
+
}
|
|
202
|
+
if(owners.has(handle.invocationKey))throw Error('Ctrl delegation wait identity is duplicated.');
|
|
203
|
+
owners.set(handle.invocationKey,owner);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return owners;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: readonly string[], invocationOwners: Readonly<Record<string, string>> = {}) {
|
|
172
210
|
if (!idsValue?.length || idsValue.some((id) => typeof id !== 'string' || !id) || new Set(idsValue).size !== idsValue.length) {
|
|
173
211
|
throw new Error('Ctrl sidecar requires unique frozen definition step identities.');
|
|
174
212
|
}
|
|
175
213
|
if (checkpoint.profile.kind !== 'WorkCtrlFlow' && checkpoint.profile.kind !== 'AICtrlWorkflow') throw new Error('Ctrl profile expected.');
|
|
176
214
|
const definitionStepIds = [...idsValue];
|
|
215
|
+
if (Object.entries(invocationOwners).some(([key, owner]) => !key || !definitionStepIds.includes(owner))) throw new Error('Ctrl compiled action ownership is invalid.');
|
|
177
216
|
const snapshot = checkpoint.profile.snapshot as FlowClosedObject;
|
|
178
217
|
const { branchDecisions, nodeResults, waitResults, orchestration, ...snapshotControl } = snapshot;
|
|
179
|
-
const
|
|
218
|
+
const waitOwners=delegationWaitOwners(snapshot,definitionStepIds,invocationOwners,checkpoint.profile.kind==='AICtrlWorkflow'?checkpoint.profile.ai:undefined);
|
|
219
|
+
const runtimeMembers = runtimeCtrlMembers(orchestration, definitionStepIds, invocationOwners,waitOwners);
|
|
180
220
|
const memberIds = [...definitionStepIds.map((id) => `definition:${id}`), ...runtimeMembers.map((id) => `runtime:${id}`)].sort(compare);
|
|
181
221
|
const facts = new Map(memberIds.map((id) => [id, {
|
|
182
222
|
branchDecisions: {},
|
|
@@ -186,7 +226,7 @@ function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: reado
|
|
|
186
226
|
}]));
|
|
187
227
|
const runtimeSet = new Set(runtimeMembers);
|
|
188
228
|
const ownerOf = (identity: string) => {
|
|
189
|
-
const owner = definitionOwner(identity, definitionStepIds);
|
|
229
|
+
const owner = definitionOwner(identity, definitionStepIds, invocationOwners)??waitOwners.get(identity);
|
|
190
230
|
return owner ? `definition:${owner}` : runtimeSet.has(identity) ? `runtime:${identity}` : undefined;
|
|
191
231
|
};
|
|
192
232
|
const place = (field: 'branchDecisions' | 'nodeResults' | 'waitResults', value: unknown) => {
|
|
@@ -223,6 +263,7 @@ function partitionCtrl(checkpoint: AIWorkflowFlowRunCheckpoint, idsValue?: reado
|
|
|
223
263
|
profile: {
|
|
224
264
|
kind: checkpoint.profile.kind,
|
|
225
265
|
definitionStepIds,
|
|
266
|
+
...(Object.keys(invocationOwners).length ? { definitionInvocationOwners: invocationOwners } : {}),
|
|
226
267
|
snapshot: {
|
|
227
268
|
...snapshotControl,
|
|
228
269
|
orchestration: Object.fromEntries(ORCHESTRATION_FIELDS.map((field) => [field, {}])),
|
|
@@ -269,6 +310,7 @@ function reconstruct(
|
|
|
269
310
|
records: readonly ImmutableStepRecord[],
|
|
270
311
|
controlRecords: ReadonlyMap<DefinitionStepDigest, ImmutableStepRecord>,
|
|
271
312
|
readMaterial: (digest: DefinitionStepDigest) => ImmutableStepRecord,
|
|
313
|
+
config: CheckpointSidecarReadConfig,
|
|
272
314
|
): AIWorkflowFlowRunCheckpoint {
|
|
273
315
|
const control = object(head.control, 'checkpoint control');
|
|
274
316
|
if (control.schemaVersion !== 'depa.flow-run-checkpoint-control/v1') throw new Error('Checkpoint control schema is invalid.');
|
|
@@ -339,7 +381,15 @@ function reconstruct(
|
|
|
339
381
|
if (!Array.isArray(profileControl.definitionStepIds) || profileControl.definitionStepIds.some((id) => typeof id !== 'string' || !id)) throw new Error('Ctrl definition membership is invalid.');
|
|
340
382
|
const definitionStepIds = profileControl.definitionStepIds as readonly string[];
|
|
341
383
|
if (new Set(definitionStepIds).size !== definitionStepIds.length) throw new Error('Ctrl definition membership is duplicated.');
|
|
384
|
+
const savedOwners = object(profileControl.definitionInvocationOwners ?? {}, 'ctrl compiled action owners');
|
|
385
|
+
if (Object.values(savedOwners).some(owner => typeof owner !== 'string' || !definitionStepIds.includes(owner))) throw new Error('Ctrl compiled action owner is invalid.');
|
|
386
|
+
const membership = config.ctrlMembership?.(head, profileControl.kind);
|
|
387
|
+
if (membership && (canonicalJson([...definitionStepIds].sort(compare)) !== canonicalJson([...membership.definitionStepIds].sort(compare))
|
|
388
|
+
|| profileControl.definitionInvocationOwners !== undefined && canonicalJson(savedOwners) !== canonicalJson(membership.invocationOwners)))
|
|
389
|
+
throw new Error('Ctrl sidecar ownership differs from the frozen compiled definition.');
|
|
390
|
+
const invocationOwners = membership?.invocationOwners ?? savedOwners as Readonly<Record<string, string>>;
|
|
342
391
|
const snapshotControl = object(profileControl.snapshot, 'ctrl snapshot control');
|
|
392
|
+
const waitOwners=delegationWaitOwners(snapshotControl,definitionStepIds,invocationOwners,profileControl.kind==='AICtrlWorkflow'?profileControl.ai:undefined);
|
|
343
393
|
const orchestrationOutput = Object.fromEntries(
|
|
344
394
|
ORCHESTRATION_FIELDS.map((field) => [field, {}]),
|
|
345
395
|
) as Record<OrchestrationField, Record<string, FlowClosedValue>>;
|
|
@@ -365,7 +415,7 @@ function reconstruct(
|
|
|
365
415
|
actual.push(value.memberId);
|
|
366
416
|
for (const field of ['branchDecisions', 'nodeResults', 'waitResults'] as const) {
|
|
367
417
|
for (const [key, fact] of entries(value[field], `ctrl ${field}`)) {
|
|
368
|
-
const owner = definitionOwner(key, definitionStepIds);
|
|
418
|
+
const owner = definitionOwner(key, definitionStepIds, invocationOwners)??waitOwners.get(key);
|
|
369
419
|
if (projectedDefinition !== null ? owner !== projectedDefinition : value.memberId !== `runtime:${key}`) throw new Error(`Ctrl ${field} member is invalid.`);
|
|
370
420
|
if (Object.prototype.hasOwnProperty.call(output[field], key)) throw new Error(`Ctrl ${field} member is duplicated.`);
|
|
371
421
|
output[field][key] = fact;
|
|
@@ -374,14 +424,14 @@ function reconstruct(
|
|
|
374
424
|
const recordOrchestration = object(value.orchestration, 'ctrl record orchestration');
|
|
375
425
|
for (const field of ORCHESTRATION_FIELDS) {
|
|
376
426
|
for (const [key, fact] of entries(recordOrchestration[field], `ctrl orchestration.${field}`)) {
|
|
377
|
-
const owner = definitionOwner(key, definitionStepIds);
|
|
427
|
+
const owner = definitionOwner(key, definitionStepIds, invocationOwners)??waitOwners.get(key);
|
|
378
428
|
if (projectedDefinition !== null ? owner !== projectedDefinition : value.memberId !== `runtime:${key}`) throw new Error(`Ctrl orchestration.${field} member is invalid.`);
|
|
379
429
|
if (Object.prototype.hasOwnProperty.call(orchestrationOutput[field], key)) throw new Error(`Ctrl orchestration.${field} member is duplicated.`);
|
|
380
430
|
orchestrationOutput[field][key] = fact;
|
|
381
431
|
}
|
|
382
432
|
}
|
|
383
433
|
}
|
|
384
|
-
const runtimeMembers = runtimeCtrlMembers(orchestrationOutput, definitionStepIds);
|
|
434
|
+
const runtimeMembers = runtimeCtrlMembers(orchestrationOutput, definitionStepIds, invocationOwners,waitOwners);
|
|
385
435
|
const expected = [...definitionStepIds.map((id) => `definition:${id}`), ...runtimeMembers.map((id) => `runtime:${id}`)].sort(compare);
|
|
386
436
|
if (canonicalJson(expected) !== canonicalJson(actual.sort(compare))) throw new Error('Ctrl sidecar membership is invalid.');
|
|
387
437
|
const snapshot: FlowClosedObject = { ...snapshotControl, ...output };
|
|
@@ -469,9 +519,20 @@ function runEvent(runtime: CheckpointDurabilityRuntime | undefined, event: Check
|
|
|
469
519
|
runtime?.beforeEvent?.(event); operation(); runtime?.onEvent?.(event);
|
|
470
520
|
}
|
|
471
521
|
function syncDirectory(directory: string, role: CheckpointDurabilityRole, runtime?: CheckpointDurabilityRuntime): void {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
522
|
+
runEvent(runtime, { operation: 'directory-fsync', role, path: directory }, () => {
|
|
523
|
+
let fd: number | undefined;
|
|
524
|
+
try {
|
|
525
|
+
fd = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
526
|
+
fs.fsyncSync(fd);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
// Windows may reject either opening or syncing a directory. File-data
|
|
529
|
+
// fsync and durability observers retain their original failure behavior.
|
|
530
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
531
|
+
if (process.platform !== 'win32' || !code || !['EPERM', 'EINVAL', 'ENOTSUP', 'EISDIR'].includes(code)) throw error;
|
|
532
|
+
} finally {
|
|
533
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
475
536
|
}
|
|
476
537
|
export function createDirectoryDurably(directory: string, role: CheckpointDurabilityRole, runtime?: CheckpointDurabilityRuntime): void {
|
|
477
538
|
runEvent(runtime, { operation: 'directory-create', role, path: directory }, () => fs.mkdirSync(directory));
|
|
@@ -500,7 +561,7 @@ function writeImmutable(file: string, value: unknown, role: 'record' | 'tree' |
|
|
|
500
561
|
|
|
501
562
|
export function encodeCheckpointSidecars(checkpointValue: AIWorkflowFlowRunCheckpoint, previousTree: StepMerkleTree | undefined, config: CheckpointSidecarEncodeConfig = {}): EncodedCheckpoint {
|
|
502
563
|
const checkpoint = normalizeFlowRunCheckpoint(checkpointValue);
|
|
503
|
-
const partitioned = checkpoint.profile.kind === 'AIDataWorkflow' ? partitionData(checkpoint, config.definitionStepIds) : partitionCtrl(checkpoint, config.definitionStepIds);
|
|
564
|
+
const partitioned = checkpoint.profile.kind === 'AIDataWorkflow' ? partitionData(checkpoint, config.definitionStepIds) : partitionCtrl(checkpoint, config.definitionStepIds, config.definitionInvocationOwners);
|
|
504
565
|
const tree = buildStepMerkleTree(partitioned.records); const treeDiff = diffStepMerkleTrees(previousTree, tree);
|
|
505
566
|
const previousRecords = new Set<string>(); Object.values(previousTree?.nodes ?? {}).forEach((node) => { if (node.kind === 'leaf') previousRecords.add(node.recordDigest); });
|
|
506
567
|
const addedRecordDigests = partitioned.records.map((record) => record.digest).filter((value) => !previousRecords.has(value)).sort(compare);
|
|
@@ -525,7 +586,7 @@ function assertReceipt(root: string, head: FlowRunCheckpointHead): Record<string
|
|
|
525
586
|
if (value.schemaVersion !== 'depa.flow-run-checkpoint-sidecar-receipt/v1' || value.version !== head.version || value.rootDigest !== head.runStepStateRootDigest || value.logicalDigest !== head.logicalDigest || (value.previousRootDigest !== null && (typeof value.previousRootDigest !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value.previousRootDigest)))) throw new Error('Receipt does not bind head.');
|
|
526
587
|
return value;
|
|
527
588
|
}
|
|
528
|
-
export function readCheckpointHeadAndLogical(file: string) {
|
|
589
|
+
export function readCheckpointHeadAndLogical(file: string, config: CheckpointSidecarReadConfig = {}) {
|
|
529
590
|
const head = normalizeHead(readJson(file)); const root = path.join(path.dirname(file), 'step-space'); const receipt = assertReceipt(root, head);
|
|
530
591
|
const controlRecords = new Map<DefinitionStepDigest, ImmutableStepRecord>();
|
|
531
592
|
for (const value of receipt.controlRecordDigests as readonly DefinitionStepDigest[]) {
|
|
@@ -536,7 +597,7 @@ export function readCheckpointHeadAndLogical(file: string) {
|
|
|
536
597
|
materialDigests.add(value);
|
|
537
598
|
return readRecord(path.join(root, 'records'), value);
|
|
538
599
|
};
|
|
539
|
-
const checkpoint = reconstruct(head, collectRecords(root, head.runStepStateRootDigest), controlRecords, readMaterial);
|
|
600
|
+
const checkpoint = reconstruct(head, collectRecords(root, head.runStepStateRootDigest), controlRecords, readMaterial, config);
|
|
540
601
|
if (digest(checkpoint) !== head.logicalDigest || checkpoint.instanceId !== head.instanceId || checkpoint.runId !== head.runId || checkpoint.version !== head.version || canonicalJson(checkpoint.definition) !== canonicalJson(head.definition)) throw new Error('Head and logical checkpoint differ.');
|
|
541
602
|
const nodes: Record<string, StepMerkleNode> = {}; const leafByKey: Record<string, DefinitionStepDigest> = {}; const keyOrder: string[] = [];
|
|
542
603
|
const visit = (value: DefinitionStepDigest): void => { if (nodes[value]) return; const node = readNode(path.join(root, 'trees'), value); nodes[value] = node; if (node.kind === 'leaf') { leafByKey[node.key] = value; keyOrder.push(node.key); } else { visit(node.leftDigest); visit(node.rightDigest); } };
|
package/src/checkpoint-store.ts
CHANGED
|
@@ -34,22 +34,38 @@ export interface WorkCtrlFlowCheckpointStoreOptions {
|
|
|
34
34
|
readonly projectOutput?: (snapshot: WorkCtrlFlowSnapshot) => FlowClosedValue;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
37
|
+
/** Projects engine-owned faults and optional fields; business values remain subject to closed-data validation. */
|
|
38
38
|
export function projectWorkCtrlSnapshotToCheckpointData(
|
|
39
39
|
snapshot: WorkCtrlFlowSnapshot,
|
|
40
40
|
): WorkCtrlFlowSnapshot {
|
|
41
|
+
const projectState = (state: unknown): unknown => {
|
|
42
|
+
if (!state || typeof state !== 'object') return state;
|
|
43
|
+
const fault: unknown = Object.getOwnPropertyDescriptor(state,'fault')?.value;
|
|
44
|
+
if(!(fault instanceof Error))return state;
|
|
45
|
+
const code: unknown = Object.getOwnPropertyDescriptor(fault, 'code')?.value;
|
|
46
|
+
return { ...state, fault: { name: fault.name, message: fault.message, ...(typeof code === 'string' ? { code } : {}) } };
|
|
47
|
+
};
|
|
41
48
|
const visit = (value: unknown): unknown => {
|
|
42
|
-
if (Array.isArray(value))
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
if(Reflect.ownKeys(value).length!==value.length+1)return value;
|
|
51
|
+
const items:unknown[]=[];
|
|
52
|
+
for(let index=0;index<value.length;index++){
|
|
53
|
+
const descriptor=Object.getOwnPropertyDescriptor(value,String(index));
|
|
54
|
+
if(!descriptor||!('value'in descriptor))return value;
|
|
55
|
+
items.push(visit(descriptor.value));
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
43
59
|
if (value === null || typeof value !== 'object') return value;
|
|
44
60
|
const prototype = Object.getPrototypeOf(value);
|
|
45
61
|
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.map(([key, nested]) => [key, visit(nested)]),
|
|
50
|
-
);
|
|
62
|
+
const descriptors=Object.getOwnPropertyDescriptors(value);
|
|
63
|
+
if(Reflect.ownKeys(value).some(key=>typeof key!=='string'||!('value'in descriptors[key]!)))return value;
|
|
64
|
+
return Object.fromEntries(Object.entries(descriptors).filter(([,descriptor])=>descriptor.value!==undefined).map(([key,descriptor])=>[key,visit(descriptor.value)]));
|
|
51
65
|
};
|
|
52
|
-
return visit(snapshot)
|
|
66
|
+
return visit({ ...snapshot, vars: { ...snapshot.vars, __flow_execution__: projectState(snapshot.vars.__flow_execution__) }, ...(snapshot.orchestration ? { orchestration: { ...snapshot.orchestration,
|
|
67
|
+
lanes: Object.fromEntries(Object.entries(snapshot.orchestration.lanes).map(([key, lane]) => [key, { ...lane, ...(lane.flowState === undefined ? {} : { flowState: projectState(lane.flowState) }) }])),
|
|
68
|
+
} } : {}) }) as WorkCtrlFlowSnapshot;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
function workCtrlControllerSidecars(snapshot: WorkCtrlFlowSnapshot): FlowClosedObject {
|
|
@@ -283,6 +299,9 @@ function validateAIDataProfileTransition(
|
|
|
283
299
|
`AI Data node ${nodeId} generation cannot decrease from ${node.generation} to ${candidate.generation}.`,
|
|
284
300
|
);
|
|
285
301
|
}
|
|
302
|
+
const waitContext=node.result&&typeof node.result==='object'?Object.getOwnPropertyDescriptor(node.result,'waitContext')?.value:undefined;
|
|
303
|
+
const nextContext=candidate.result&&typeof candidate.result==='object'?Object.getOwnPropertyDescriptor(candidate.result,'waitContext')?.value:undefined;
|
|
304
|
+
if(node.generation===candidate.generation&&waitContext!==undefined&&!sameClosedValue(waitContext,nextContext))throw new FlowRunCheckpointTransitionError(`AI Data native wait context ${nodeId}@${node.generation} is immutable.`);
|
|
286
305
|
}
|
|
287
306
|
const appendedPatches = after.patchHistory.slice(before.patchHistory.length);
|
|
288
307
|
const appendedInvalidations = after.invalidations.filter((fact) =>
|
|
@@ -469,6 +488,7 @@ function validateAgentStateTransition(
|
|
|
469
488
|
|| (next.profile.kind !== 'AICtrlWorkflow' && next.profile.kind !== 'AIDataWorkflow')) return;
|
|
470
489
|
const before = current.profile.ai;
|
|
471
490
|
const after = next.profile.ai;
|
|
491
|
+
validateWorkflowDelegationTransition(before.delegationsByKey ?? {}, after.delegationsByKey ?? {});
|
|
472
492
|
for (const [instanceId, instance] of Object.entries(before.instancesById)) {
|
|
473
493
|
if (!sameClosedValue(instance, after.instancesById[instanceId])) {
|
|
474
494
|
throw new FlowRunCheckpointTransitionError(`Accepted Agent instance ${instanceId} is immutable and cannot disappear.`);
|
|
@@ -553,9 +573,38 @@ export function validateFlowRunCheckpointTransition(
|
|
|
553
573
|
);
|
|
554
574
|
}
|
|
555
575
|
validateStepExtensionTransition(current, next);
|
|
576
|
+
validateFrozenCtrlExpansion(current, next);
|
|
556
577
|
validateAgentStateTransition(current, next);
|
|
557
578
|
}
|
|
558
579
|
|
|
580
|
+
function validateFrozenCtrlExpansion(current: AIWorkflowFlowRunCheckpoint, next: AIWorkflowFlowRunCheckpoint): void {
|
|
581
|
+
if (current.profile.kind === 'AIDataWorkflow' || next.profile.kind === 'AIDataWorkflow') return;
|
|
582
|
+
function object(value: FlowClosedValue | undefined): FlowClosedObject {
|
|
583
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new FlowRunCheckpointTransitionError('Ctrl orchestration facts are missing.');
|
|
584
|
+
return value as FlowClosedObject;
|
|
585
|
+
}
|
|
586
|
+
const before = object(current.profile.snapshot.orchestration);
|
|
587
|
+
const after = object(next.profile.snapshot.orchestration);
|
|
588
|
+
const composites = object(before.composites);
|
|
589
|
+
const nextComposites = object(after.composites);
|
|
590
|
+
const lanes = object(before.lanes), nextLanes = object(after.lanes);
|
|
591
|
+
for (const [key, value] of Object.entries(composites)) {
|
|
592
|
+
const fact = object(value);
|
|
593
|
+
if (!fact.expansion) continue;
|
|
594
|
+
const candidate = object(nextComposites[key]);
|
|
595
|
+
if (!sameClosedValue(fact.expansion, candidate.expansion)) throw new FlowRunCheckpointTransitionError(`ForEach ${key} cannot rewrite frozen expansion.`);
|
|
596
|
+
for (const [laneKey, value] of Object.entries(lanes)) {
|
|
597
|
+
const lane = object(value);
|
|
598
|
+
if (lane.invocationKey !== key) continue;
|
|
599
|
+
const candidateLane = object(nextLanes[laneKey]);
|
|
600
|
+
for (const field of ['invocationKey', 'laneId', 'bodyId', 'item']) {
|
|
601
|
+
if (!sameClosedValue(lane[field], candidateLane[field])) throw new FlowRunCheckpointTransitionError(`ForEach ${key} cannot rewrite frozen lane ${laneKey}.${field}.`);
|
|
602
|
+
}
|
|
603
|
+
if (lane.started === true && candidateLane.started !== true) throw new FlowRunCheckpointTransitionError(`ForEach ${key} cannot release an unsettled lane slot by rewriting its claim.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
559
608
|
function validateStepExtensionTransition(
|
|
560
609
|
current: AIWorkflowFlowRunCheckpoint,
|
|
561
610
|
next: AIWorkflowFlowRunCheckpoint,
|
|
@@ -656,3 +705,4 @@ export async function projectFlowRunCheckpoint(
|
|
|
656
705
|
const checkpoint = await loadFlowRunCheckpoint(runtime, input);
|
|
657
706
|
return checkpoint === undefined ? undefined : normalizeFlowRunCheckpoint(checkpoint);
|
|
658
707
|
}
|
|
708
|
+
import { validateWorkflowDelegationTransition } from './delegation.js';
|