ai-workflow-logic 0.1.0

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 ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "ai-workflow-logic",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Pure helpers for AI workflow profile descriptors, runtime contexts, and reuse policy resolution",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./browser": "./src/browser.ts"
11
+ },
12
+ "dependencies": {
13
+ "ai-workflow-contract": "0.1.0"
14
+ },
15
+ "files": [
16
+ "src"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.com"
21
+ }
22
+ }
package/src/browser.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './index.js';
package/src/index.ts ADDED
@@ -0,0 +1,299 @@
1
+ import {
2
+ AI_CTRL_WORKFLOW_DESCRIPTOR,
3
+ AI_DATA_WORKFLOW_DESCRIPTOR,
4
+ AI_WORKFLOW_ALLOWED_RESOURCE_REF_SCHEMES,
5
+ AI_WORKFLOW_MACHINE_NODE_DEFAULT_REUSE_POLICY,
6
+ AI_WORKFLOW_MANUAL_NODE_DEFAULT_REUSE_POLICY,
7
+ type AICtrlWorkflowDefinitionBinding,
8
+ type AIDataWorkflowDefinitionBinding,
9
+ type AIWorkflowEffectiveReusePolicy,
10
+ type AIWorkflowEmbeddedEffectProvider,
11
+ type AIWorkflowEffectProvider,
12
+ type AIWorkflowEffectHandler,
13
+ type AIWorkflowEffectHandlerMap,
14
+ type AIWorkflowEffectHandlerRegistry,
15
+ type AIWorkflowEffectRequest,
16
+ type AIWorkflowMutableEffectHandlerRegistry,
17
+ type AIWorkflowKind,
18
+ type AIWorkflowNodeType,
19
+ type AIWorkflowProfileDescriptor,
20
+ type AIWorkflowResourceRef,
21
+ type AIWorkflowResourceRefScheme,
22
+ type AIWorkflowReusePolicy,
23
+ type AIWorkflowRuntimeRoots,
24
+ type AIWorkflowRuntimeContext,
25
+ type AIWorkflowStateStore,
26
+ type AIWorkflowSubstrate,
27
+ } from 'ai-workflow-contract';
28
+
29
+ export class AIWorkflowFoundationError extends Error {
30
+ constructor(message: string) {
31
+ super(message);
32
+ this.name = 'AIWorkflowFoundationError';
33
+ }
34
+ }
35
+
36
+ export class AIWorkflowEffectHandlerNotFoundError extends AIWorkflowFoundationError {
37
+ constructor(
38
+ public readonly operation: string,
39
+ public readonly effectId: string,
40
+ ) {
41
+ super(`AI workflow effect handler not found for operation ${operation}`);
42
+ this.name = 'AIWorkflowEffectHandlerNotFoundError';
43
+ }
44
+ }
45
+
46
+ export function descriptorForAIWorkflowKind(
47
+ kind: 'AICtrlWorkflow',
48
+ ): AIWorkflowProfileDescriptor<'AICtrlWorkflow', 'WorkCtrlFlow'>;
49
+ export function descriptorForAIWorkflowKind(
50
+ kind: 'AIDataWorkflow',
51
+ ): AIWorkflowProfileDescriptor<'AIDataWorkflow', 'EagerDataFlow'>;
52
+ export function descriptorForAIWorkflowKind(kind: AIWorkflowKind): AIWorkflowProfileDescriptor {
53
+ switch (kind) {
54
+ case 'AICtrlWorkflow':
55
+ return AI_CTRL_WORKFLOW_DESCRIPTOR;
56
+ case 'AIDataWorkflow':
57
+ return AI_DATA_WORKFLOW_DESCRIPTOR;
58
+ }
59
+ }
60
+
61
+ export function assertAIWorkflowSubstrate(
62
+ descriptor: AIWorkflowProfileDescriptor,
63
+ expected: AIWorkflowSubstrate,
64
+ ): void {
65
+ if (descriptor.substrate !== expected) {
66
+ throw new AIWorkflowFoundationError(
67
+ `${descriptor.kind} must use ${expected}, got ${descriptor.substrate}`,
68
+ );
69
+ }
70
+ }
71
+
72
+ export function defineAICtrlWorkflow(
73
+ definition: AICtrlWorkflowDefinitionBinding['definition'],
74
+ ): AICtrlWorkflowDefinitionBinding {
75
+ return Object.freeze({
76
+ kind: 'AICtrlWorkflow',
77
+ substrate: 'WorkCtrlFlow',
78
+ descriptor: AI_CTRL_WORKFLOW_DESCRIPTOR,
79
+ definition,
80
+ });
81
+ }
82
+
83
+ export function defineAIDataWorkflow(
84
+ definition: AIDataWorkflowDefinitionBinding['definition'],
85
+ ): AIDataWorkflowDefinitionBinding {
86
+ return Object.freeze({
87
+ kind: 'AIDataWorkflow',
88
+ substrate: 'EagerDataFlow',
89
+ descriptor: AI_DATA_WORKFLOW_DESCRIPTOR,
90
+ definition,
91
+ });
92
+ }
93
+
94
+ export interface ResolveAIWorkflowReusePolicyInput {
95
+ readonly nodeType: AIWorkflowNodeType;
96
+ readonly explicitPolicy?: AIWorkflowReusePolicy;
97
+ readonly nodeTypeDefaultPolicy?: AIWorkflowReusePolicy;
98
+ readonly platformDefaultPolicy?: AIWorkflowReusePolicy;
99
+ }
100
+
101
+ export function resolveAIWorkflowReusePolicy(
102
+ input: ResolveAIWorkflowReusePolicyInput,
103
+ ): AIWorkflowEffectiveReusePolicy {
104
+ if (input.explicitPolicy) {
105
+ return {
106
+ policy: input.explicitPolicy,
107
+ source: 'node',
108
+ nodeType: input.nodeType,
109
+ };
110
+ }
111
+
112
+ if (input.nodeTypeDefaultPolicy) {
113
+ return {
114
+ policy: input.nodeTypeDefaultPolicy,
115
+ source: 'node-type-default',
116
+ nodeType: input.nodeType,
117
+ };
118
+ }
119
+
120
+ if (input.nodeType === 'manual') {
121
+ return {
122
+ policy: AI_WORKFLOW_MANUAL_NODE_DEFAULT_REUSE_POLICY,
123
+ source: 'node-type-default',
124
+ nodeType: input.nodeType,
125
+ };
126
+ }
127
+
128
+ if (input.platformDefaultPolicy) {
129
+ return {
130
+ policy: input.platformDefaultPolicy,
131
+ source: 'platform-default',
132
+ nodeType: input.nodeType,
133
+ };
134
+ }
135
+
136
+ return {
137
+ policy: AI_WORKFLOW_MACHINE_NODE_DEFAULT_REUSE_POLICY,
138
+ source: 'node-type-default',
139
+ nodeType: input.nodeType,
140
+ };
141
+ }
142
+
143
+ export function createAIWorkflowRuntimeContext<
144
+ Effects extends AIWorkflowEffectProvider,
145
+ Store extends AIWorkflowStateStore,
146
+ >(
147
+ context: AIWorkflowRuntimeContext<Effects, Store>,
148
+ ): AIWorkflowRuntimeContext<Effects, Store> {
149
+ if (!context.roots.globalRoot) {
150
+ throw new AIWorkflowFoundationError('AI workflow globalRoot is required');
151
+ }
152
+ if (!context.roots.workspaceRoot) {
153
+ throw new AIWorkflowFoundationError('AI workflow workspaceRoot is required');
154
+ }
155
+ if (!context.effects) {
156
+ throw new AIWorkflowFoundationError('AI workflow effects provider is required');
157
+ }
158
+ if (!context.stateStore) {
159
+ throw new AIWorkflowFoundationError('AI workflow state store is required');
160
+ }
161
+ return Object.freeze({
162
+ ...context,
163
+ roots: Object.freeze({ ...context.roots }),
164
+ });
165
+ }
166
+
167
+ export async function invokeAIWorkflowEffect<Input = unknown, Output = unknown>(
168
+ context: AIWorkflowRuntimeContext,
169
+ request: AIWorkflowEffectRequest<Input>,
170
+ ): Promise<Output> {
171
+ return context.effects.invoke<Input, Output>(request);
172
+ }
173
+
174
+ export interface CreateAIWorkflowEffectHandlerRegistryOptions {
175
+ readonly handlers?: AIWorkflowEffectHandlerMap;
176
+ }
177
+
178
+ export function createAIWorkflowEffectHandlerRegistry(
179
+ options: CreateAIWorkflowEffectHandlerRegistryOptions = {},
180
+ ): AIWorkflowMutableEffectHandlerRegistry {
181
+ const handlers = new Map<string, AIWorkflowEffectHandler>(
182
+ Object.entries(options.handlers ?? {}),
183
+ );
184
+ return {
185
+ hasHandler(operation) {
186
+ return handlers.has(operation);
187
+ },
188
+ resolveHandler(operation) {
189
+ return {
190
+ operation,
191
+ ...(handlers.has(operation) ? { handler: handlers.get(operation)! } : {}),
192
+ };
193
+ },
194
+ listOperations() {
195
+ return [...handlers.keys()].sort();
196
+ },
197
+ registerHandler(operation, handler) {
198
+ if (!operation) {
199
+ throw new AIWorkflowFoundationError('AI workflow effect operation is required');
200
+ }
201
+ handlers.set(operation, handler);
202
+ },
203
+ unregisterHandler(operation) {
204
+ return handlers.delete(operation);
205
+ },
206
+ };
207
+ }
208
+
209
+ export interface CreateEmbeddedAIWorkflowEffectProviderOptions {
210
+ readonly roots: AIWorkflowRuntimeRoots;
211
+ readonly registry?: AIWorkflowEffectHandlerRegistry;
212
+ readonly handlers?: AIWorkflowEffectHandlerMap;
213
+ readonly metadata?: Readonly<Record<string, unknown>>;
214
+ readonly onDispatch?: (request: AIWorkflowEffectRequest) => void;
215
+ readonly onResult?: (request: AIWorkflowEffectRequest, result: unknown) => void;
216
+ }
217
+
218
+ export function createEmbeddedAIWorkflowEffectProvider(
219
+ options: CreateEmbeddedAIWorkflowEffectProviderOptions,
220
+ ): AIWorkflowEmbeddedEffectProvider {
221
+ if (!options.roots.globalRoot) {
222
+ throw new AIWorkflowFoundationError('AI workflow embedded effect provider globalRoot is required');
223
+ }
224
+ if (!options.roots.workspaceRoot) {
225
+ throw new AIWorkflowFoundationError('AI workflow embedded effect provider workspaceRoot is required');
226
+ }
227
+ const registry = options.registry ?? createAIWorkflowEffectHandlerRegistry({
228
+ handlers: options.handlers,
229
+ });
230
+ const roots = Object.freeze({ ...options.roots });
231
+ return Object.freeze({
232
+ roots,
233
+ registry,
234
+ async invoke<Input = unknown, Output = unknown>(
235
+ request: AIWorkflowEffectRequest<Input>,
236
+ ): Promise<Output> {
237
+ const resolution = registry.resolveHandler(request.operation);
238
+ if (!resolution.handler) {
239
+ throw new AIWorkflowEffectHandlerNotFoundError(request.operation, request.effectId);
240
+ }
241
+ options.onDispatch?.(request);
242
+ const result = await resolution.handler(request, {
243
+ roots,
244
+ ...(options.metadata ? { metadata: options.metadata } : {}),
245
+ });
246
+ options.onResult?.(request, result);
247
+ return result as Output;
248
+ },
249
+ });
250
+ }
251
+
252
+ export interface ParsedAIWorkflowResourceRef {
253
+ readonly ref: AIWorkflowResourceRef;
254
+ readonly scheme: AIWorkflowResourceRefScheme;
255
+ readonly target: string;
256
+ }
257
+
258
+ const RESOURCE_REF_PATTERN = /^([a-z][a-z0-9+.-]*):\/\/(.+)$/i;
259
+
260
+ const allowedResourceRefSchemes = new Set<string>(AI_WORKFLOW_ALLOWED_RESOURCE_REF_SCHEMES);
261
+
262
+ function hasContainmentEscape(target: string): boolean {
263
+ if (target.toLowerCase().includes('%2e')) return true;
264
+ let decoded: string;
265
+ try {
266
+ decoded = decodeURIComponent(target);
267
+ } catch {
268
+ return true;
269
+ }
270
+ if (decoded.includes('\\')) return true;
271
+ if (decoded.startsWith('/')) return true;
272
+ if (/^[A-Za-z]:/.test(decoded)) return true;
273
+ return decoded.split(/[/?#]+/).some((segment) => segment === '..');
274
+ }
275
+
276
+ export function parseAIWorkflowResourceRef(value: string): ParsedAIWorkflowResourceRef | undefined {
277
+ const match = RESOURCE_REF_PATTERN.exec(value);
278
+ if (!match) return undefined;
279
+ const [, scheme, target] = match;
280
+ if (!allowedResourceRefSchemes.has(scheme)) return undefined;
281
+ if (!target || hasContainmentEscape(target)) return undefined;
282
+ return {
283
+ ref: value as AIWorkflowResourceRef,
284
+ scheme: scheme as AIWorkflowResourceRefScheme,
285
+ target,
286
+ };
287
+ }
288
+
289
+ export function isAIWorkflowResourceRef(value: string): value is AIWorkflowResourceRef {
290
+ return parseAIWorkflowResourceRef(value) !== undefined;
291
+ }
292
+
293
+ export function assertAIWorkflowResourceRef(value: string): AIWorkflowResourceRef {
294
+ const parsed = parseAIWorkflowResourceRef(value);
295
+ if (!parsed) {
296
+ throw new AIWorkflowFoundationError(`invalid AI workflow resource ref: ${value}`);
297
+ }
298
+ return parsed.ref;
299
+ }