ai-workflow-logic 0.1.11 → 0.2.1

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/src/core.ts ADDED
@@ -0,0 +1,563 @@
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 AIWorkflowAppBundleProjection,
10
+ type AIWorkflowBindingProjection,
11
+ type AIWorkflowDefinitionResourceKind,
12
+ type AIWorkflowEffectiveReusePolicy,
13
+ type AIWorkflowEmbeddedEffectProvider,
14
+ type AIWorkflowEffectProvider,
15
+ type AIWorkflowEffectHandler,
16
+ type AIWorkflowEffectHandlerMap,
17
+ type AIWorkflowEffectHandlerRegistry,
18
+ type AIWorkflowEffectRequest,
19
+ type AIWorkflowMutableEffectHandlerRegistry,
20
+ type AIWorkflowKind,
21
+ type AIWorkflowNodeType,
22
+ type AIWorkflowProfileDescriptor,
23
+ type AIWorkflowResourceProjectionDiagnostic,
24
+ type AIWorkflowResourceRegistryView,
25
+ type AIWorkflowResourceRef,
26
+ type AIWorkflowResourceRefScheme,
27
+ type AIWorkflowReusePolicy,
28
+ type AIWorkflowRuntimeRoots,
29
+ type AIWorkflowRuntimeContext,
30
+ type AIWorkflowStateProjection,
31
+ type AIWorkflowSubstrate,
32
+ type ProjectAIWorkflowAppBundlesOptions,
33
+ } from 'ai-workflow-contract';
34
+ import type { ResourceNode, ResourceRecord, ResourceValue } from 'halfcode-compiler.xnl/resource-core';
35
+ import { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './errors.js';
36
+
37
+ export { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './errors.js';
38
+ export { projectAIWorkflowAgentResources } from './agent-resources.js';
39
+ export {
40
+ FlowRunContractValidationError,
41
+ normalizeAIWorkflowProfileDurableState,
42
+ normalizeFlowClosedObject,
43
+ normalizeFlowClosedValue,
44
+ normalizeFlowInstanceDescriptor,
45
+ normalizeFlowRunCheckpoint,
46
+ } from './run-checkpoint.js';
47
+ export {
48
+ AIAgentInvocationError,
49
+ bindAIAgentProcessors,
50
+ normalizeAIAgentEffectConfig,
51
+ normalizeAIAgentInvocation,
52
+ normalizeAIAgentSelector,
53
+ runAgent,
54
+ runTargetedAgent,
55
+ } from './agent-invocation.js';
56
+ export * from './holon-task.js';
57
+ export { assertAIAgentNodeTaskBinding, upgradeLegacyAIWorkflowProfileDurableState } from './agent-state.js';
58
+ export type { AIAgentNodeBindingIdentity } from './agent-state.js';
59
+ export {
60
+ FlowRunCheckpointTransitionError,
61
+ FlowRunCheckpointVersionConflictError,
62
+ InMemoryFlowRunCheckpointStore,
63
+ createWorkCtrlFlowCheckpointStore,
64
+ projectWorkCtrlSnapshotToCheckpointData,
65
+ commitFlowRunCheckpoint,
66
+ loadFlowRunCheckpoint,
67
+ projectFlowRunCheckpoint,
68
+ } from './checkpoint-store.js';
69
+ export type { WorkCtrlFlowCheckpointStoreOptions } from './checkpoint-store.js';
70
+ export {
71
+ createInitialRunStepExtensions,
72
+ validateCheckpointAgainstFrozenProfile,
73
+ } from './checkpoint-profile.js';
74
+ export type {
75
+ FlowRunCheckpointAdmissionRuntime,
76
+ FlowRunProfileAuthorityPort,
77
+ FlowRunProfileAuthorityRequest,
78
+ FrozenFlowRunProfileAuthority,
79
+ FrozenRunStepExtensionDefinition,
80
+ } from './checkpoint-profile.js';
81
+ export { mutateRunStepExtension } from './step-extension.js';
82
+ export type {
83
+ RunStepExtensionMutationConfig,
84
+ RunStepExtensionMutationInvocation,
85
+ RunStepExtensionSelector,
86
+ } from 'ai-workflow-contract';
87
+ export type { RunStepExtensionMutationRuntime } from './step-extension.js';
88
+
89
+ export class AIWorkflowEffectHandlerNotFoundError extends AIWorkflowFoundationError {
90
+ constructor(
91
+ public readonly operation: string,
92
+ public readonly effectId: string,
93
+ ) {
94
+ super(`AI workflow effect handler not found for operation ${operation}`);
95
+ this.name = 'AIWorkflowEffectHandlerNotFoundError';
96
+ }
97
+ }
98
+
99
+ export function descriptorForAIWorkflowKind(
100
+ kind: 'AICtrlWorkflow',
101
+ ): AIWorkflowProfileDescriptor<'AICtrlWorkflow', 'WorkCtrlFlow'>;
102
+ export function descriptorForAIWorkflowKind(
103
+ kind: 'AIDataWorkflow',
104
+ ): AIWorkflowProfileDescriptor<'AIDataWorkflow', 'EagerDataFlow'>;
105
+ export function descriptorForAIWorkflowKind(kind: AIWorkflowKind): AIWorkflowProfileDescriptor {
106
+ switch (kind) {
107
+ case 'AICtrlWorkflow':
108
+ return AI_CTRL_WORKFLOW_DESCRIPTOR;
109
+ case 'AIDataWorkflow':
110
+ return AI_DATA_WORKFLOW_DESCRIPTOR;
111
+ }
112
+ }
113
+
114
+ export function assertAIWorkflowSubstrate(
115
+ descriptor: AIWorkflowProfileDescriptor,
116
+ expected: AIWorkflowSubstrate,
117
+ ): void {
118
+ if (descriptor.substrate !== expected) {
119
+ throw new AIWorkflowFoundationError(
120
+ `${descriptor.kind} must use ${expected}, got ${descriptor.substrate}`,
121
+ );
122
+ }
123
+ }
124
+
125
+ export function defineAICtrlWorkflow(
126
+ definition: AICtrlWorkflowDefinitionBinding['definition'],
127
+ ): AICtrlWorkflowDefinitionBinding {
128
+ return Object.freeze({
129
+ kind: 'AICtrlWorkflow',
130
+ substrate: 'WorkCtrlFlow',
131
+ descriptor: AI_CTRL_WORKFLOW_DESCRIPTOR,
132
+ definition,
133
+ });
134
+ }
135
+
136
+ export function defineAIDataWorkflow(
137
+ definition: AIDataWorkflowDefinitionBinding['definition'],
138
+ ): AIDataWorkflowDefinitionBinding {
139
+ return Object.freeze({
140
+ kind: 'AIDataWorkflow',
141
+ substrate: 'EagerDataFlow',
142
+ descriptor: AI_DATA_WORKFLOW_DESCRIPTOR,
143
+ definition,
144
+ });
145
+ }
146
+
147
+ export function projectAIWorkflowAppBundles(
148
+ registry: AIWorkflowResourceRegistryView,
149
+ options: ProjectAIWorkflowAppBundlesOptions = {},
150
+ ): readonly AIWorkflowAppBundleProjection[] {
151
+ const diagnostics: AIWorkflowResourceProjectionDiagnostic[] = [];
152
+ const workflowRecords = new Map<string, ResourceRecord>();
153
+ for (const kind of AI_WORKFLOW_DEFINITION_RESOURCE_KINDS) {
154
+ for (const resource of registry.byKind.get(kind) ?? []) {
155
+ workflowRecords.set(resource.resourceId, resource);
156
+ }
157
+ }
158
+
159
+ const appBundles = [...(registry.byKind.get('AIWorkflowAppBundle') ?? [])]
160
+ .sort((left, right) => compareCodeUnits(left.resourceId, right.resourceId))
161
+ .map((resource) => projectAppBundle(resource, workflowRecords, options, diagnostics));
162
+
163
+ if (diagnostics.length > 0) {
164
+ throw new AIWorkflowResourceProjectionError(Object.freeze(
165
+ diagnostics.sort((left, right) => compareCodeUnits(left.location, right.location)
166
+ || compareCodeUnits(left.code, right.code)),
167
+ ));
168
+ }
169
+
170
+ return Object.freeze(appBundles.filter((bundle): bundle is AIWorkflowAppBundleProjection => bundle !== undefined));
171
+ }
172
+
173
+ const AI_WORKFLOW_DEFINITION_RESOURCE_KINDS = Object.freeze([
174
+ 'AICtrlWorkflow',
175
+ 'AIDataWorkflow',
176
+ ] as const satisfies readonly AIWorkflowDefinitionResourceKind[]);
177
+
178
+ function projectAppBundle(
179
+ resource: ResourceRecord,
180
+ workflowRecords: ReadonlyMap<string, ResourceRecord>,
181
+ options: ProjectAIWorkflowAppBundlesOptions,
182
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
183
+ ): AIWorkflowAppBundleProjection | undefined {
184
+ const bindingsNode = resource.node.subdomains.WorkflowBindings;
185
+ if (!bindingsNode) {
186
+ diagnostics.push(projectionDiagnostic(
187
+ 'AI_WORKFLOW_BINDINGS_MISSING',
188
+ resource.documentUri,
189
+ `AIWorkflowAppBundle '${resource.resourceId}' must declare WorkflowBindings.`,
190
+ ));
191
+ return undefined;
192
+ }
193
+
194
+ const bindingIds = new Set<string>();
195
+ const workflowBindings: AIWorkflowBindingProjection[] = [];
196
+ for (const [index, value] of bindingsNode.body.entries()) {
197
+ const location = `${resource.documentUri}#WorkflowBindings:${index}`;
198
+ if (!isResourceNode(value) || value.tag !== 'WorkflowBinding') {
199
+ diagnostics.push(projectionDiagnostic(
200
+ 'AI_WORKFLOW_BINDING_NODE_INVALID',
201
+ location,
202
+ 'WorkflowBindings may contain only WorkflowBinding nodes.',
203
+ ));
204
+ continue;
205
+ }
206
+ const binding = projectWorkflowBinding(value, workflowRecords, location, diagnostics);
207
+ if (!binding) continue;
208
+ if (bindingIds.has(binding.id)) {
209
+ diagnostics.push(projectionDiagnostic(
210
+ 'AI_WORKFLOW_BINDING_ID_DUPLICATE',
211
+ location,
212
+ `WorkflowBinding id '${binding.id}' is declared more than once.`,
213
+ ));
214
+ continue;
215
+ }
216
+ bindingIds.add(binding.id);
217
+ workflowBindings.push(binding);
218
+ }
219
+
220
+ const entrypoints = workflowBindings.filter((binding) => binding.entrypoint);
221
+ if (entrypoints.length === 0) {
222
+ diagnostics.push(projectionDiagnostic(
223
+ 'AI_WORKFLOW_APP_ENTRYPOINT_MISSING',
224
+ resource.documentUri,
225
+ `AIWorkflowAppBundle '${resource.resourceId}' must declare at least one workflow entrypoint.`,
226
+ ));
227
+ }
228
+
229
+ const frozenBindings = Object.freeze(workflowBindings);
230
+ return Object.freeze({
231
+ kind: 'AIWorkflowAppBundle',
232
+ fqn: resource.fqn ?? resource.resourceId,
233
+ ...(options.packageResourceId === undefined ? {} : { packageResourceId: options.packageResourceId }),
234
+ resource,
235
+ workflowBindings: frozenBindings,
236
+ entrypoints: Object.freeze(entrypoints),
237
+ });
238
+ }
239
+
240
+ function projectWorkflowBinding(
241
+ node: ResourceNode,
242
+ workflowRecords: ReadonlyMap<string, ResourceRecord>,
243
+ location: string,
244
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
245
+ ): AIWorkflowBindingProjection | undefined {
246
+ const id = node.resourceId;
247
+ const kind = asString(node.properties.kind);
248
+ const ref = asString(node.properties.ref);
249
+ const entrypoint = node.properties.entrypoint;
250
+ let valid = true;
251
+
252
+ if (!id) {
253
+ diagnostics.push(projectionDiagnostic(
254
+ 'AI_WORKFLOW_BINDING_ID_MISSING',
255
+ location,
256
+ 'WorkflowBinding must declare an explicit #id.',
257
+ ));
258
+ valid = false;
259
+ }
260
+ if (!isWorkflowDefinitionResourceKind(kind)) {
261
+ diagnostics.push(projectionDiagnostic(
262
+ 'AI_WORKFLOW_BINDING_KIND_INVALID',
263
+ location,
264
+ 'WorkflowBinding kind must be AICtrlWorkflow or AIDataWorkflow.',
265
+ ));
266
+ valid = false;
267
+ }
268
+ const resourceId = resourceIdFromRef(ref);
269
+ if (!resourceId) {
270
+ diagnostics.push(projectionDiagnostic(
271
+ 'AI_WORKFLOW_BINDING_REF_INVALID',
272
+ location,
273
+ 'WorkflowBinding ref must be a non-empty resource:// identity.',
274
+ ));
275
+ valid = false;
276
+ }
277
+ if (typeof entrypoint !== 'boolean') {
278
+ diagnostics.push(projectionDiagnostic(
279
+ 'AI_WORKFLOW_BINDING_ENTRYPOINT_INVALID',
280
+ location,
281
+ 'WorkflowBinding entrypoint must be an explicit boolean.',
282
+ ));
283
+ valid = false;
284
+ }
285
+ if (
286
+ !valid
287
+ || !id
288
+ || !isWorkflowDefinitionResourceKind(kind)
289
+ || !resourceId
290
+ || typeof entrypoint !== 'boolean'
291
+ ) return undefined;
292
+
293
+ const target = workflowRecords.get(resourceId);
294
+ if (!target) {
295
+ diagnostics.push(projectionDiagnostic(
296
+ 'AI_WORKFLOW_BINDING_TARGET_MISSING',
297
+ location,
298
+ `WorkflowBinding target '${resourceId}' does not exist in the effective resource registry.`,
299
+ ));
300
+ return undefined;
301
+ }
302
+ if (target.kind !== kind) {
303
+ diagnostics.push(projectionDiagnostic(
304
+ 'AI_WORKFLOW_BINDING_TARGET_KIND_MISMATCH',
305
+ location,
306
+ `WorkflowBinding declares kind '${kind}', but target '${resourceId}' has kind '${target.kind}'.`,
307
+ ));
308
+ return undefined;
309
+ }
310
+
311
+ return Object.freeze({
312
+ id,
313
+ kind,
314
+ ref: ref as `resource://${string}`,
315
+ entrypoint,
316
+ resource: target,
317
+ });
318
+ }
319
+
320
+ function isWorkflowDefinitionResourceKind(value: string | undefined): value is AIWorkflowDefinitionResourceKind {
321
+ return value === 'AICtrlWorkflow' || value === 'AIDataWorkflow';
322
+ }
323
+
324
+ function resourceIdFromRef(value: string | undefined): string | undefined {
325
+ const prefix = 'resource://';
326
+ if (!value?.startsWith(prefix)) return undefined;
327
+ const resourceId = value.slice(prefix.length);
328
+ return resourceId.length > 0 && !resourceId.includes('://') ? resourceId : undefined;
329
+ }
330
+
331
+ function asString(value: ResourceValue | undefined): string | undefined {
332
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
333
+ }
334
+
335
+ function isResourceNode(value: ResourceValue): value is ResourceNode {
336
+ return typeof value === 'object'
337
+ && value !== null
338
+ && !Array.isArray(value)
339
+ && typeof (value as ResourceNode).tag === 'string'
340
+ && Array.isArray((value as ResourceNode).body);
341
+ }
342
+
343
+ function projectionDiagnostic(
344
+ code: string,
345
+ location: string,
346
+ message: string,
347
+ ): AIWorkflowResourceProjectionDiagnostic {
348
+ return Object.freeze({ code, location, message });
349
+ }
350
+
351
+ function compareCodeUnits(left: string, right: string): number {
352
+ return left < right ? -1 : left > right ? 1 : 0;
353
+ }
354
+
355
+ export interface ResolveAIWorkflowReusePolicyInput {
356
+ readonly nodeType: AIWorkflowNodeType;
357
+ readonly explicitPolicy?: AIWorkflowReusePolicy;
358
+ readonly nodeTypeDefaultPolicy?: AIWorkflowReusePolicy;
359
+ readonly platformDefaultPolicy?: AIWorkflowReusePolicy;
360
+ }
361
+
362
+ export function resolveAIWorkflowReusePolicy(
363
+ input: ResolveAIWorkflowReusePolicyInput,
364
+ ): AIWorkflowEffectiveReusePolicy {
365
+ if (input.explicitPolicy) {
366
+ return {
367
+ policy: input.explicitPolicy,
368
+ source: 'node',
369
+ nodeType: input.nodeType,
370
+ };
371
+ }
372
+
373
+ if (input.nodeTypeDefaultPolicy) {
374
+ return {
375
+ policy: input.nodeTypeDefaultPolicy,
376
+ source: 'node-type-default',
377
+ nodeType: input.nodeType,
378
+ };
379
+ }
380
+
381
+ if (input.nodeType === 'manual') {
382
+ return {
383
+ policy: AI_WORKFLOW_MANUAL_NODE_DEFAULT_REUSE_POLICY,
384
+ source: 'node-type-default',
385
+ nodeType: input.nodeType,
386
+ };
387
+ }
388
+
389
+ if (input.platformDefaultPolicy) {
390
+ return {
391
+ policy: input.platformDefaultPolicy,
392
+ source: 'platform-default',
393
+ nodeType: input.nodeType,
394
+ };
395
+ }
396
+
397
+ return {
398
+ policy: AI_WORKFLOW_MACHINE_NODE_DEFAULT_REUSE_POLICY,
399
+ source: 'node-type-default',
400
+ nodeType: input.nodeType,
401
+ };
402
+ }
403
+
404
+ export function createAIWorkflowRuntimeContext<
405
+ Effects extends AIWorkflowEffectProvider | import('ai-workflow-contract').AIWorkflowAuthoredEffects,
406
+ Store extends AIWorkflowStateProjection,
407
+ >(
408
+ context: AIWorkflowRuntimeContext<Effects, Store>,
409
+ ): AIWorkflowRuntimeContext<Effects, Store> {
410
+ if (!context.roots.globalRoot) {
411
+ throw new AIWorkflowFoundationError('AI workflow globalRoot is required');
412
+ }
413
+ if (!context.roots.workspaceRoot) {
414
+ throw new AIWorkflowFoundationError('AI workflow workspaceRoot is required');
415
+ }
416
+ if (!context.effects) {
417
+ throw new AIWorkflowFoundationError('AI workflow effects provider is required');
418
+ }
419
+ if (!context.stateStore) {
420
+ throw new AIWorkflowFoundationError('AI workflow state store is required');
421
+ }
422
+ return Object.freeze({
423
+ ...context,
424
+ roots: Object.freeze({ ...context.roots }),
425
+ });
426
+ }
427
+
428
+ export async function invokeAIWorkflowEffect<Input = unknown, Output = unknown>(
429
+ context: AIWorkflowRuntimeContext,
430
+ request: AIWorkflowEffectRequest<Input>,
431
+ ): Promise<Output> {
432
+ return context.effects.invoke<Input, Output>(request);
433
+ }
434
+
435
+ export interface CreateAIWorkflowEffectHandlerRegistryOptions {
436
+ readonly handlers?: AIWorkflowEffectHandlerMap;
437
+ }
438
+
439
+ export function createAIWorkflowEffectHandlerRegistry(
440
+ options: CreateAIWorkflowEffectHandlerRegistryOptions = {},
441
+ ): AIWorkflowMutableEffectHandlerRegistry {
442
+ const handlers = new Map<string, AIWorkflowEffectHandler>(
443
+ Object.entries(options.handlers ?? {}),
444
+ );
445
+ return {
446
+ hasHandler(operation) {
447
+ return handlers.has(operation);
448
+ },
449
+ resolveHandler(operation) {
450
+ return {
451
+ operation,
452
+ ...(handlers.has(operation) ? { handler: handlers.get(operation)! } : {}),
453
+ };
454
+ },
455
+ listOperations() {
456
+ return [...handlers.keys()].sort(compareCodeUnits);
457
+ },
458
+ registerHandler(operation, handler) {
459
+ if (!operation) {
460
+ throw new AIWorkflowFoundationError('AI workflow effect operation is required');
461
+ }
462
+ handlers.set(operation, handler);
463
+ },
464
+ unregisterHandler(operation) {
465
+ return handlers.delete(operation);
466
+ },
467
+ };
468
+ }
469
+
470
+ export interface CreateEmbeddedAIWorkflowEffectProviderOptions {
471
+ readonly roots: AIWorkflowRuntimeRoots;
472
+ readonly registry?: AIWorkflowEffectHandlerRegistry;
473
+ readonly handlers?: AIWorkflowEffectHandlerMap;
474
+ readonly metadata?: Readonly<Record<string, unknown>>;
475
+ readonly onDispatch?: (request: AIWorkflowEffectRequest) => void;
476
+ readonly onResult?: (request: AIWorkflowEffectRequest, result: unknown) => void;
477
+ }
478
+
479
+ export function createEmbeddedAIWorkflowEffectProvider(
480
+ options: CreateEmbeddedAIWorkflowEffectProviderOptions,
481
+ ): AIWorkflowEmbeddedEffectProvider {
482
+ if (!options.roots.globalRoot) {
483
+ throw new AIWorkflowFoundationError('AI workflow embedded effect provider globalRoot is required');
484
+ }
485
+ if (!options.roots.workspaceRoot) {
486
+ throw new AIWorkflowFoundationError('AI workflow embedded effect provider workspaceRoot is required');
487
+ }
488
+ const registry = options.registry ?? createAIWorkflowEffectHandlerRegistry({
489
+ handlers: options.handlers,
490
+ });
491
+ const roots = Object.freeze({ ...options.roots });
492
+ return Object.freeze({
493
+ roots,
494
+ registry,
495
+ async invoke<Input = unknown, Output = unknown>(
496
+ request: AIWorkflowEffectRequest<Input>,
497
+ ): Promise<Output> {
498
+ const resolution = registry.resolveHandler(request.operation);
499
+ if (!resolution.handler) {
500
+ throw new AIWorkflowEffectHandlerNotFoundError(request.operation, request.effectId);
501
+ }
502
+ options.onDispatch?.(request);
503
+ const result = await resolution.handler(request, {
504
+ roots,
505
+ ...(options.metadata ? { metadata: options.metadata } : {}),
506
+ });
507
+ options.onResult?.(request, result);
508
+ return result as Output;
509
+ },
510
+ });
511
+ }
512
+
513
+ export interface ParsedAIWorkflowResourceRef {
514
+ readonly ref: AIWorkflowResourceRef;
515
+ readonly scheme: AIWorkflowResourceRefScheme;
516
+ readonly target: string;
517
+ }
518
+
519
+ const RESOURCE_REF_PATTERN = /^([a-z][a-z0-9+.-]*):\/\/(.+)$/i;
520
+
521
+ const allowedResourceRefSchemes = new Set<string>(AI_WORKFLOW_ALLOWED_RESOURCE_REF_SCHEMES);
522
+
523
+ function hasContainmentEscape(target: string): boolean {
524
+ if (target.toLowerCase().includes('%2e')) return true;
525
+ let decoded: string;
526
+ try {
527
+ decoded = decodeURIComponent(target);
528
+ } catch {
529
+ return true;
530
+ }
531
+ if (decoded.includes('\\')) return true;
532
+ if (decoded.startsWith('/')) return true;
533
+ if (/^[A-Za-z]:/.test(decoded)) return true;
534
+ return decoded.split(/[/?#]+/).some((segment) => segment === '..');
535
+ }
536
+
537
+ /** @deprecated Compatibility helper; Halfcode owns canonical resource-reference validation. */
538
+ export function parseAIWorkflowResourceRef(value: string): ParsedAIWorkflowResourceRef | undefined {
539
+ const match = RESOURCE_REF_PATTERN.exec(value);
540
+ if (!match) return undefined;
541
+ const [, scheme, target] = match;
542
+ if (!allowedResourceRefSchemes.has(scheme)) return undefined;
543
+ if (!target || hasContainmentEscape(target)) return undefined;
544
+ return {
545
+ ref: value as AIWorkflowResourceRef,
546
+ scheme: scheme as AIWorkflowResourceRefScheme,
547
+ target,
548
+ };
549
+ }
550
+
551
+ /** @deprecated Compatibility helper; use typed refs projected from Halfcode ResourceRecord facts. */
552
+ export function isAIWorkflowResourceRef(value: string): value is AIWorkflowResourceRef {
553
+ return parseAIWorkflowResourceRef(value) !== undefined;
554
+ }
555
+
556
+ /** @deprecated Compatibility helper; use typed refs projected from Halfcode ResourceRecord facts. */
557
+ export function assertAIWorkflowResourceRef(value: string): AIWorkflowResourceRef {
558
+ const parsed = parseAIWorkflowResourceRef(value);
559
+ if (!parsed) {
560
+ throw new AIWorkflowFoundationError(`invalid AI workflow resource ref: ${value}`);
561
+ }
562
+ return parsed.ref;
563
+ }
package/src/filesystem.ts CHANGED
@@ -4,12 +4,32 @@ import {
4
4
  } from 'halfcode-compiler.xnl/resource-core';
5
5
  import type { LoadedAIWorkflowResourcePackage } from 'ai-workflow-contract';
6
6
  import { projectAIWorkflowAppBundles } from './index.js';
7
+ import {
8
+ resolveAIWorkflowResourceTree,
9
+ type AIWorkflowResourceResolutionContext,
10
+ } from './resource-resolution.js';
11
+
12
+ export {
13
+ AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR,
14
+ DEPA_AI_RESOURCE_READER_PROFILE_ID,
15
+ createAIWorkflowResourceResolutionContext,
16
+ createResolvedAIWorkflowContentIdentityIndex,
17
+ resolveAIWorkflowResourceTree,
18
+ } from './resource-resolution.js';
19
+ export type { AIWorkflowResourceResolutionContext } from './resource-resolution.js';
20
+
21
+ export interface LoadAIWorkflowResourcePackageOptions extends LoadResourceTreeOptions {
22
+ readonly resolutionContext?: AIWorkflowResourceResolutionContext;
23
+ }
7
24
 
8
25
  export async function loadAIWorkflowResourcePackage(
9
- options: LoadResourceTreeOptions,
26
+ options: LoadAIWorkflowResourcePackageOptions,
10
27
  ): Promise<LoadedAIWorkflowResourcePackage> {
11
- const tree = await loadResourceTree(options);
28
+ const { resolutionContext, ...loadOptions } = options;
29
+ const authoredTree = await loadResourceTree(loadOptions);
30
+ const tree = resolveAIWorkflowResourceTree(authoredTree, resolutionContext);
12
31
  return Object.freeze({
32
+ authoredTree,
13
33
  tree,
14
34
  appBundles: projectAIWorkflowAppBundles(tree.registry, {
15
35
  packageResourceId: tree.manifest.resourceId,