ai-ctrl-workflow-logic 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,28 +1,29 @@
1
1
  {
2
2
  "name": "ai-ctrl-workflow-logic",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "WorkCtrlFlow-backed AI Ctrl Workflow adapter logic",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
- "./browser": "./src/browser.ts"
10
+ "./browser": "./src/browser.ts",
11
+ "./filesystem": "./src/filesystem.ts"
11
12
  },
12
13
  "dependencies": {
13
- "ai-ctrl-workflow-contract": "0.1.0",
14
- "ai-workflow-contract": "0.1.0",
15
- "ai-workflow-logic": "0.1.0",
16
- "instant-ctrl-flow-contract": "0.1.0",
17
- "instant-ctrl-flow-logic": "^0.1.1",
18
- "work-ctrl-flow-contract": "0.1.0",
19
- "work-ctrl-flow-logic": "0.1.0"
14
+ "ai-ctrl-workflow-contract": "0.1.2",
15
+ "ai-workflow-contract": "0.1.5",
16
+ "ai-workflow-logic": "0.1.5",
17
+ "flow-step-space-logic": "0.1.0",
18
+ "instant-ctrl-flow-contract": "0.1.1",
19
+ "instant-ctrl-flow-logic": "0.1.3",
20
+ "work-ctrl-flow-contract": "0.1.2",
21
+ "work-ctrl-flow-logic": "0.1.2"
20
22
  },
21
23
  "files": [
22
24
  "src"
23
25
  ],
24
26
  "publishConfig": {
25
- "access": "public",
26
- "registry": "https://registry.npmjs.com"
27
+ "access": "public"
27
28
  }
28
29
  }
@@ -0,0 +1,145 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { AICtrlWorkflowController } from 'ai-ctrl-workflow-contract';
4
+ import type {
5
+ AIAgentHostEffectPort,
6
+ FrozenAIAgentTaskBinding,
7
+ FlowClosedValue,
8
+ AIWorkflowProfileDurableState,
9
+ } from 'ai-workflow-contract';
10
+ import { FilesystemFlowRunCheckpointStore } from 'ai-workflow-logic/checkpoint-filesystem';
11
+ import { createFilesystemFlowCodeResolver, type FlowModuleImporter } from 'instant-ctrl-flow-logic/filesystem';
12
+ import { assembleDefinitionStepProfileDirectory } from 'flow-step-space-logic/filesystem';
13
+ import {
14
+ loadMaterializedFlowInstance,
15
+ materializeFlowInstance,
16
+ type FlowInstanceSourceIdentity,
17
+ } from 'work-ctrl-flow-logic';
18
+ import type { WorkCtrlFlowSnapshot } from 'work-ctrl-flow-contract';
19
+ import {
20
+ createAICtrlWorkflowCheckpointStore,
21
+ createAICtrlWorkflowAgentNodeRuntimeBinder,
22
+ createAICtrlWorkflowController,
23
+ loadAICtrlWorkflowSources,
24
+ type CreateAICtrlWorkflowControllerOptions,
25
+ } from './index.js';
26
+
27
+ export interface MaterializedAICtrlWorkflowControllerOptions
28
+ extends Omit<CreateAICtrlWorkflowControllerOptions, 'binding' | 'store' | 'resolveCode'> {
29
+ readonly storeRoot: string;
30
+ readonly instanceId: string;
31
+ readonly bundleDir?: string;
32
+ readonly source?: FlowInstanceSourceIdentity;
33
+ readonly checkpointConfig: FlowClosedValue;
34
+ readonly durableAI: AIWorkflowProfileDurableState;
35
+ readonly projectCheckpointOutput?: (snapshot: WorkCtrlFlowSnapshot) => FlowClosedValue;
36
+ readonly importModule?: FlowModuleImporter;
37
+ readonly agentEffects?: AIAgentHostEffectPort;
38
+ readonly agentTaskProofs?: Readonly<Record<string, FrozenAIAgentTaskBinding>>;
39
+ }
40
+
41
+ export interface MaterializedAICtrlWorkflowController extends AICtrlWorkflowController {
42
+ readonly checkpointStore: FilesystemFlowRunCheckpointStore;
43
+ readonly instanceId: string;
44
+ }
45
+
46
+ /** Frozen AICtrl definition plus canonical filesystem checkpoint owner. */
47
+ export function createMaterializedAICtrlWorkflowControllerFromDirectory(
48
+ options: MaterializedAICtrlWorkflowControllerOptions,
49
+ ): MaterializedAICtrlWorkflowController {
50
+ if (options.agentEffects && (!options.agentTaskProofs || Object.keys(options.agentTaskProofs).length === 0)) {
51
+ throw new Error('AI_AGENT_TASK_PROOF_REQUIRED: materialized AI Ctrl Agent effects require run-freeze task proofs.');
52
+ }
53
+ const instancesDir = path.join(options.storeRoot, 'instances');
54
+ const instanceDir = path.join(instancesDir, options.instanceId);
55
+ const validateDefinition = (definitionDir: string) => {
56
+ loadAICtrlBinding(definitionDir);
57
+ };
58
+ const instance = fs.existsSync(instanceDir)
59
+ ? loadMaterializedFlowInstance(instanceDir, options.instanceId, validateDefinition)
60
+ : options.bundleDir && options.source
61
+ ? materializeFlowInstance({
62
+ definitionDir: options.bundleDir,
63
+ instancesDir,
64
+ instanceId: options.instanceId,
65
+ source: options.source,
66
+ validateDefinition,
67
+ })
68
+ : (() => {
69
+ if (options.bundleDir || options.source) throw new Error('bundleDir and source identity must be provided together.');
70
+ throw new Error('bundleDir and source identity are required for first admission.');
71
+ })();
72
+ const binding = loadAICtrlBinding(instance.definitionDir);
73
+ const checkpointStore = new FilesystemFlowRunCheckpointStore({ storeRoot: options.storeRoot });
74
+ const store = createAICtrlWorkflowCheckpointStore(
75
+ { checkpointStore },
76
+ {
77
+ instanceId: options.instanceId,
78
+ definition: instance.descriptor.definition,
79
+ config: options.checkpointConfig,
80
+ ai: options.durableAI,
81
+ ...(options.projectCheckpointOutput ? { projectOutput: options.projectCheckpointOutput } : {}),
82
+ },
83
+ );
84
+ const {
85
+ storeRoot: _storeRoot,
86
+ instanceId: _instanceId,
87
+ bundleDir: _bundleDir,
88
+ source: _source,
89
+ checkpointConfig: _checkpointConfig,
90
+ durableAI: _durableAI,
91
+ projectCheckpointOutput: _projectCheckpointOutput,
92
+ importModule: _importModule,
93
+ agentEffects: _agentEffects,
94
+ agentTaskProofs: _agentTaskProofs,
95
+ ...controllerOptions
96
+ } = options;
97
+ const controller = createAICtrlWorkflowController({
98
+ ...controllerOptions,
99
+ binding,
100
+ store,
101
+ resolveCode: createFilesystemFlowCodeResolver(options.importModule),
102
+ ...(options.agentEffects ? {
103
+ bindNodeRuntime: createAICtrlWorkflowAgentNodeRuntimeBinder({
104
+ flowInstanceId: options.instanceId,
105
+ checkpointRuntime: { checkpointStore },
106
+ effects: options.agentEffects,
107
+ workflowRef: `resource://${binding.definition.fqn}`,
108
+ taskProofs: options.agentTaskProofs ?? {},
109
+ }),
110
+ } : {}),
111
+ });
112
+ const materializedController: AICtrlWorkflowController = {
113
+ ...controller,
114
+ async start(treeId, startOptions) {
115
+ const existing = await controller.store.load(treeId);
116
+ if (existing) {
117
+ if (startOptions?.input !== undefined
118
+ && JSON.stringify(startOptions.input) !== JSON.stringify(existing.input)) {
119
+ throw new Error(`AI Ctrl Workflow run ${treeId} is already bound to different start input.`);
120
+ }
121
+ const outcome = await controller.getOutcome(treeId);
122
+ if (!outcome) throw new Error(`AI Ctrl Workflow run ${treeId} has no durable outcome.`);
123
+ return outcome;
124
+ }
125
+ return controller.start(treeId, startOptions);
126
+ },
127
+ };
128
+ return Object.assign(materializedController, { checkpointStore, instanceId: options.instanceId });
129
+ }
130
+
131
+ function loadAICtrlBinding(definitionDir: string) {
132
+ const assembled = assembleDefinitionStepProfileDirectory(definitionDir, 'AICtrlWorkflow');
133
+ if (assembled.diagnostics.length > 0) {
134
+ throw new Error(`AICtrlWorkflow StepSpace is invalid: ${assembled.diagnostics.map((item) => item.code).join(', ')}`);
135
+ }
136
+ const sources = Object.fromEntries((assembled.sources as readonly ({ readonly ref: string; readonly content: string } | { readonly name: string; readonly content: string })[])
137
+ .map((source) => ['name' in source ? source.name : source.ref, source.content]));
138
+ const loaded = loadAICtrlWorkflowSources(sources, { baseUri: definitionDir });
139
+ if (!loaded.binding || loaded.diagnostics.length > 0) {
140
+ throw new Error(`AICtrlWorkflow definition is invalid: ${loaded.diagnostics.map((item) => item.code).join(', ')}`);
141
+ }
142
+ return assembled.forest
143
+ ? { ...loaded.binding, definition: { ...loaded.binding.definition, definitionStepForest: assembled.forest } }
144
+ : loaded.binding;
145
+ }
package/src/index.ts CHANGED
@@ -5,12 +5,31 @@ import type {
5
5
  } from 'ai-ctrl-workflow-contract';
6
6
  import type {
7
7
  AICtrlWorkflowDefinitionBinding,
8
+ AICtrlWorkflowRunProfileState,
8
9
  AICtrlWorkflowRuntime,
10
+ AIAgentHostEffectPort,
11
+ FrozenAIAgentTaskBinding,
12
+ AIWorkflowAuthoredEffects,
9
13
  AIWorkflowEffectProvider,
14
+ AIWorkflowFlowRunCheckpoint,
15
+ AIWorkflowProfileDurableState,
10
16
  AIWorkflowRuntimeContext,
11
- AIWorkflowStateStore,
17
+ AIWorkflowStateProjection,
18
+ FlowClosedObject,
19
+ FlowClosedValue,
20
+ FlowDefinitionArtifactIdentity,
21
+ FlowRunCheckpointKey,
22
+ FlowRunCheckpointRuntime,
12
23
  } from 'ai-workflow-contract';
13
- import { createAIWorkflowRuntimeContext, defineAICtrlWorkflow } from 'ai-workflow-logic';
24
+ import {
25
+ commitFlowRunCheckpoint,
26
+ createAIWorkflowRuntimeContext,
27
+ defineAICtrlWorkflow,
28
+ loadFlowRunCheckpoint,
29
+ projectWorkCtrlSnapshotToCheckpointData,
30
+ bindAIAgentProcessors,
31
+ } from 'ai-workflow-logic';
32
+ import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
14
33
  import type {
15
34
  FlowCodeResolver,
16
35
  FlowDiagnostic,
@@ -28,9 +47,99 @@ import type {
28
47
  } from 'work-ctrl-flow-contract';
29
48
  import { WorkCtrlFlowEngine, type WorkCtrlFlowDefinition, type WorkCtrlFlowEngineOptions } from 'work-ctrl-flow-logic';
30
49
 
50
+ export interface AICtrlWorkflowCheckpointStoreOptions {
51
+ readonly instanceId: string;
52
+ readonly definition: FlowDefinitionArtifactIdentity;
53
+ readonly config: FlowClosedValue;
54
+ readonly ai: AIWorkflowProfileDurableState;
55
+ readonly projectOutput?: (snapshot: WorkCtrlFlowSnapshot) => FlowClosedValue;
56
+ }
57
+
58
+ function ctrlControllerSidecars(snapshot: WorkCtrlFlowSnapshot) {
59
+ return {
60
+ status: snapshot.status,
61
+ lastTickNo: snapshot.lastTickNo,
62
+ branchDecisions: snapshot.branchDecisions,
63
+ orchestration: snapshot.orchestration,
64
+ openWaitHandles: snapshot.openWaitHandles,
65
+ closedWaitHandles: snapshot.closedWaitHandles,
66
+ appliedResumeSignals: snapshot.appliedResumeSignals,
67
+ };
68
+ }
69
+
70
+ function ctrlNodeSidecars(snapshot: WorkCtrlFlowSnapshot) {
71
+ return {
72
+ nodeResults: snapshot.nodeResults,
73
+ waitResults: snapshot.waitResults,
74
+ };
75
+ }
76
+
77
+ /** Runtime-first adapter that makes the canonical checkpoint the WorkCtrl store owner. */
78
+ export function createAICtrlWorkflowCheckpointStore(
79
+ runtime: FlowRunCheckpointRuntime,
80
+ options: AICtrlWorkflowCheckpointStoreOptions,
81
+ ): WorkCtrlFlowStore {
82
+ return {
83
+ async load(treeId) {
84
+ const checkpoint = await loadAICtrlWorkflowCheckpoint(runtime, {
85
+ instanceId: options.instanceId,
86
+ runId: treeId,
87
+ });
88
+ // WorkCtrlFlow mutates its working snapshot during a tick. The canonical
89
+ // checkpoint remains immutable; only this detached replay copy is mutable.
90
+ return checkpoint === undefined ? undefined : structuredClone(checkpoint.profile.snapshot);
91
+ },
92
+ async save(snapshot) {
93
+ const durableSnapshot = projectWorkCtrlSnapshotToCheckpointData(snapshot);
94
+ const key = { instanceId: options.instanceId, runId: snapshot.treeId };
95
+ const current = await loadAICtrlWorkflowCheckpoint(runtime, key);
96
+ const expectedVersion = current?.version ?? null;
97
+ const checkpoint: AIWorkflowFlowRunCheckpoint = {
98
+ schemaVersion: 'depa.flow-run-checkpoint/v1',
99
+ instanceId: options.instanceId,
100
+ runId: snapshot.treeId,
101
+ definition: options.definition,
102
+ version: current === undefined ? 0 : current.version + 1,
103
+ input: current?.input ?? snapshot.input as FlowClosedValue,
104
+ config: current?.config ?? options.config,
105
+ state: durableSnapshot.vars as FlowClosedValue,
106
+ output: options.projectOutput?.(snapshot) ?? current?.output ?? null,
107
+ controllerSidecars: ctrlControllerSidecars(durableSnapshot) as unknown as FlowClosedObject,
108
+ nodeSidecars: ctrlNodeSidecars(durableSnapshot) as unknown as FlowClosedObject,
109
+ profile: {
110
+ kind: 'AICtrlWorkflow',
111
+ snapshot: durableSnapshot as unknown as FlowClosedObject,
112
+ ai: current?.profile.kind === 'AICtrlWorkflow' ? current.profile.ai : options.ai,
113
+ },
114
+ };
115
+ await commitFlowRunCheckpoint(runtime, { expectedVersion, checkpoint });
116
+ },
117
+ async remove(treeId) {
118
+ throw new Error(`Canonical FlowRunCheckpoint removal is not supported for ${treeId}.`);
119
+ },
120
+ };
121
+ }
122
+
123
+ export async function loadAICtrlWorkflowCheckpoint(
124
+ runtime: FlowRunCheckpointRuntime,
125
+ input: FlowRunCheckpointKey,
126
+ ): Promise<
127
+ | (AIWorkflowFlowRunCheckpoint & { readonly profile: AICtrlWorkflowRunProfileState<WorkCtrlFlowSnapshot> })
128
+ | undefined
129
+ > {
130
+ const checkpoint = await loadFlowRunCheckpoint(runtime, input);
131
+ if (checkpoint === undefined) return undefined;
132
+ if (checkpoint.profile.kind !== 'AICtrlWorkflow') {
133
+ throw new Error(`Flow run ${input.runId} is not an AI Ctrl Workflow checkpoint.`);
134
+ }
135
+ return checkpoint as AIWorkflowFlowRunCheckpoint & {
136
+ readonly profile: AICtrlWorkflowRunProfileState<WorkCtrlFlowSnapshot>;
137
+ };
138
+ }
139
+
31
140
  export interface CreateAICtrlWorkflowRuntimeOptions<
32
141
  Effects extends AIWorkflowEffectProvider,
33
- Store extends AIWorkflowStateStore,
142
+ Store extends AIWorkflowStateProjection,
34
143
  > {
35
144
  readonly runtime?: WorkCtrlFlowRuntime;
36
145
  readonly ai: AIWorkflowRuntimeContext<Effects, Store>;
@@ -38,7 +147,7 @@ export interface CreateAICtrlWorkflowRuntimeOptions<
38
147
 
39
148
  export function createAICtrlWorkflowRuntime<
40
149
  Effects extends AIWorkflowEffectProvider,
41
- Store extends AIWorkflowStateStore,
150
+ Store extends AIWorkflowStateProjection,
42
151
  >(
43
152
  options: CreateAICtrlWorkflowRuntimeOptions<Effects, Store>,
44
153
  ): AICtrlWorkflowRuntime<Effects, Store> {
@@ -48,6 +157,40 @@ export function createAICtrlWorkflowRuntime<
48
157
  };
49
158
  }
50
159
 
160
+ export interface CreateAICtrlWorkflowAgentNodeRuntimeBinderOptions {
161
+ readonly flowInstanceId: string;
162
+ readonly checkpointRuntime: FlowRunCheckpointRuntime;
163
+ readonly effects: AIAgentHostEffectPort;
164
+ readonly workflowRef: `resource://${string}`;
165
+ readonly taskProofs: Readonly<Record<string, FrozenAIAgentTaskBinding>>;
166
+ readonly metadata?: FlowClosedObject;
167
+ }
168
+
169
+ /** Composition seam: ordinary WorkCtrl Run nodes receive the same-named bound facade. */
170
+ export function createAICtrlWorkflowAgentNodeRuntimeBinder(
171
+ options: CreateAICtrlWorkflowAgentNodeRuntimeBinderOptions,
172
+ ): NonNullable<WorkCtrlFlowEngineOptions['bindNodeRuntime']> {
173
+ return (runtime, identity) => {
174
+ const workflowRuntime = runtime as AICtrlWorkflowRuntime<any, AIWorkflowStateProjection>;
175
+ if (!workflowRuntime.ai) throw new Error('AI Ctrl node runtime requires an AI runtime context.');
176
+ const proof = options.taskProofs[identity.nodeId];
177
+ if (!proof) throw new Error(`AI_AGENT_TASK_PROOF_REQUIRED: no frozen proof for node ${identity.nodeId}.`);
178
+ const taskBinding = assertFrozenAIAgentTaskBinding(proof).task;
179
+ const effects: AIWorkflowAuthoredEffects = bindAIAgentProcessors({
180
+ checkpointRuntime: options.checkpointRuntime,
181
+ checkpointKey: { instanceId: options.flowInstanceId, runId: identity.treeId },
182
+ nodeId: identity.nodeId,
183
+ invocationKey: identity.invocationKey,
184
+ workflowKind: 'AICtrlWorkflow',
185
+ workflowRef: options.workflowRef,
186
+ taskBinding,
187
+ effects: options.effects,
188
+ ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
189
+ });
190
+ return Object.freeze({ ...runtime, ai: Object.freeze({ ...workflowRuntime.ai, effects }) });
191
+ };
192
+ }
193
+
51
194
  export interface CreateAICtrlWorkflowControllerOptions
52
195
  extends Omit<WorkCtrlFlowEngineOptions, 'definition' | 'runtime' | 'expectedForm'> {
53
196
  readonly binding: AICtrlWorkflowDefinitionBinding;