ai-ctrl-workflow-logic 0.1.10 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-ctrl-workflow-logic",
3
- "version": "0.1.10",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "WorkCtrlFlow-backed AI Ctrl Workflow adapter logic",
6
6
  "type": "module",
@@ -8,19 +8,21 @@
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
10
  "./browser": "./src/browser.ts",
11
- "./filesystem": "./src/filesystem.ts"
11
+ "./filesystem": "./src/filesystem.ts",
12
+ "./sources": "./src/source-loader.ts"
12
13
  },
13
14
  "dependencies": {
14
- "ai-ctrl-workflow-contract": "0.1.6",
15
- "ai-workflow-contract": "0.1.9",
16
- "ai-workflow-logic": "0.1.11",
17
- "flow-step-space-logic": "0.1.1",
18
- "instant-ctrl-flow-contract": "0.1.2",
19
- "instant-ctrl-flow-logic": "0.1.5",
20
- "task-manager-contract": "0.1.3",
21
- "task-manager-logic": "0.1.6",
22
- "work-ctrl-flow-contract": "0.1.3",
23
- "work-ctrl-flow-logic": "0.1.4"
15
+ "depa-behavior-tree": "0.1.2",
16
+ "ai-ctrl-workflow-contract": "0.2.1",
17
+ "ai-workflow-contract": "0.2.2",
18
+ "ai-workflow-logic": "0.2.2",
19
+ "flow-step-space-logic": "0.1.2",
20
+ "instant-ctrl-flow-contract": "0.1.3",
21
+ "instant-ctrl-flow-logic": "0.1.7",
22
+ "task-manager-contract": "0.1.4",
23
+ "task-manager-logic": "0.1.7",
24
+ "work-ctrl-flow-contract": "0.1.4",
25
+ "work-ctrl-flow-logic": "0.1.6"
24
26
  },
25
27
  "files": [
26
28
  "src"
@@ -0,0 +1,55 @@
1
+ import { BehaviorResult, type ActionHandler, type CtrlFlowNodeType, type INodeConfig } from 'depa-behavior-tree';
2
+ import { createFlowHandlers, flowStateOf } from 'instant-ctrl-flow-logic';
3
+ import { WaitNodeHandler, type TickController, type WorkCtrlFlowEngineOptions } from 'work-ctrl-flow-logic';
4
+ import type { FlowBundleSpec, FlowCodeResolver } from 'instant-ctrl-flow-contract';
5
+ import { WorkflowDelegationSuspendedError } from 'ai-workflow-logic';
6
+
7
+ /** Uses the existing scheduler, native WaitHandles and resume token history. */
8
+ export function createAICtrlWorkflowDelegationHandlers(
9
+ spec: FlowBundleSpec,
10
+ resolveCode: FlowCodeResolver,
11
+ bindNodeRuntime: WorkCtrlFlowEngineOptions['bindNodeRuntime'],
12
+ controller: () => TickController,
13
+ ): Record<string, ActionHandler<CtrlFlowNodeType, INodeConfig>> {
14
+ const handlers = createFlowHandlers(spec, resolveCode, {
15
+ begin: () => { throw Error('Delegation Run handler does not own branch decisions'); },
16
+ record: () => { throw Error('Delegation Run handler does not own branch decisions'); },
17
+ runtimeForNode: (nodeId, runtime, state) => bindNodeRuntime?.(runtime, controller().runtimeIdentity(nodeId, state)) ?? runtime,
18
+ });
19
+ return { 'flow.Run': { async execute(ctx) {
20
+ const c = controller();
21
+ if (c.suspended.length) { c.inertKeys.add(ctx.node.Key); return BehaviorResult.Failure; }
22
+ const key = c.invocationKeyFor(ctx.node.Key);
23
+ const recorded = c.snapshot.nodeResults[key];
24
+ if (recorded) {
25
+ if (bindNodeRuntime) c.invocationKeyFor(ctx.node.Key);
26
+ return recorded === 'Success' ? BehaviorResult.Success : BehaviorResult.Failure;
27
+ }
28
+ const result = await handlers['flow.Run']!.execute(ctx);
29
+ const flow = flowStateOf(ctx);
30
+ if (!(flow.fault instanceof WorkflowDelegationSuspendedError)) {
31
+ if (result !== BehaviorResult.Running) c.snapshot.nodeResults[key] = result === BehaviorResult.Success ? 'Success' : 'Failure';
32
+ for (const handle of [...c.snapshot.openWaitHandles]) {
33
+ if (!handle.nodeKey.startsWith(`${ctx.node.Key}:delegation:`) || handle.signalKey !== key) continue;
34
+ handle.status = 'Consumed'; handle.closedAtMs = c.now();
35
+ c.snapshot.appliedResumeSignals.push(`${handle.waitHandleId}:${handle.resumeToken}`);
36
+ c.snapshot.waitResults[handle.invocationKey] = { result: result === BehaviorResult.Success ? 'Success' : 'Failure' };
37
+ c.snapshot.openWaitHandles = c.snapshot.openWaitHandles.filter(item => item !== handle);
38
+ c.snapshot.closedWaitHandles.push(handle);
39
+ }
40
+ return result;
41
+ }
42
+ const delegationInvocationKey = flow.fault.invocationKey;
43
+ const status = flow.fault.status;
44
+ delete flow.fault;
45
+ // Signal payload is only a wakeup; task output always comes from owner observation.
46
+ const prefix = `${ctx.node.Key}:delegation:`;
47
+ const scope=key.slice(0,key.lastIndexOf('/')+1);
48
+ const count = c.snapshot.closedWaitHandles.filter(handle => handle.nodeKey.startsWith(prefix) && handle.invocationKey.startsWith(`${scope}${prefix}`)).length;
49
+ const proxy = Object.create(ctx);
50
+ Object.defineProperty(proxy, 'node', { value: { ...ctx.node, Key: `${prefix}${count}`, Config: { signalKind: 'workflow.delegation.observe', signalKey: key } } });
51
+ return new WaitNodeHandler('external-job', controller, async (_ctx, handle) => {
52
+ handle.metadata = { workflowDelegation: { kind: 'workflow.task-delegation/v1', invocationKey: delegationInvocationKey, actionInvocationKey: key, status } };
53
+ }).execute(proxy);
54
+ } } };
55
+ }
package/src/filesystem.ts CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  } from 'ai-workflow-logic';
22
22
  import { createFilesystemFlowCodeResolver, type FlowModuleImporter } from 'instant-ctrl-flow-logic/filesystem';
23
23
  import type { FlowNodeSpec } from 'instant-ctrl-flow-contract';
24
+ import { projectWorkCtrlFlowInvocationOwners } from 'work-ctrl-flow-logic';
25
+ import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
24
26
  import {
25
27
  assembleDefinitionStepProfileDirectory,
26
28
  createFilesystemDefinitionStepProfileRuntime,
@@ -135,24 +137,29 @@ export function createMaterializedAICtrlWorkflowControllerFromDirectory(
135
137
  agentTaskProofs: _agentTaskProofs,
136
138
  ...controllerOptions
137
139
  } = options;
140
+ const bindAgentNode=options.agentEffects?createAICtrlWorkflowAgentNodeRuntimeBinder({
141
+ flowInstanceId:options.instanceId,
142
+ checkpointRuntime:{checkpointStore},
143
+ effects:options.agentEffects,
144
+ workflowRef:`resource://${binding.definition.fqn}`,
145
+ taskProofs:options.agentTaskProofs??{},
146
+ }):undefined;
138
147
  const controller = createAICtrlWorkflowController({
139
148
  ...controllerOptions,
140
149
  binding,
141
150
  store,
142
151
  resolveCode: createFilesystemFlowCodeResolver(options.importModule),
143
- ...(options.agentEffects ? {
144
- bindNodeRuntime: createAICtrlWorkflowAgentNodeRuntimeBinder({
145
- flowInstanceId: options.instanceId,
146
- checkpointRuntime: { checkpointStore },
147
- effects: options.agentEffects,
148
- workflowRef: `resource://${binding.definition.fqn}`,
149
- taskProofs: options.agentTaskProofs ?? {},
150
- }),
152
+ suspendDelegations: options.agentEffects?.delegation !== undefined,
153
+ ...(bindAgentNode ? {
154
+ // Pure branch/loop predicates have no Agent task. Keep the Agent binder
155
+ // exact for the nodes whose frozen proof actually grants that facade.
156
+ bindNodeRuntime:(runtime,identity)=>options.agentTaskProofs?.[identity.nodeId]?bindAgentNode(runtime,identity):runtime,
151
157
  } : {}),
152
158
  });
153
159
  const materializedController: AICtrlWorkflowController = {
154
160
  ...controller,
155
161
  async start(treeId, startOptions) {
162
+ for (const proof of Object.values(options.agentTaskProofs ?? {})) assertFrozenAIAgentTaskBinding(proof);
156
163
  const existing = await controller.store.load(treeId);
157
164
  if (existing) {
158
165
  if (startOptions?.input !== undefined
@@ -165,6 +172,10 @@ export function createMaterializedAICtrlWorkflowControllerFromDirectory(
165
172
  }
166
173
  return controller.start(treeId, startOptions);
167
174
  },
175
+ async resume(treeId, signal) {
176
+ for (const proof of Object.values(options.agentTaskProofs ?? {})) assertFrozenAIAgentTaskBinding(proof);
177
+ return controller.resume(treeId, signal);
178
+ },
168
179
  };
169
180
  return Object.assign(materializedController, {
170
181
  checkpointStore,
@@ -211,6 +222,7 @@ function profileAuthorityFromBinding(
211
222
  return Object.freeze({
212
223
  profileKind: 'AICtrlWorkflow' as const,
213
224
  definitionStepIds: Object.freeze([...definitionStepIds]),
225
+ definitionInvocationOwners: projectWorkCtrlFlowInvocationOwners(binding.definition),
214
226
  extensionsByStepId: Object.freeze(Object.fromEntries((forest?.stepOrder ?? []).flatMap((stepId) => {
215
227
  const extensions = Object.fromEntries(Object.entries(forest!.steps[stepId].extensions).map(([kind, extension]) => [kind, Object.freeze({
216
228
  ...(extension.schemaRef === undefined ? {} : { schemaRef: extension.schemaRef }),
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { createAICtrlWorkflowDelegationHandlers } from './delegation.js';
2
+ export { createAICtrlWorkflowDelegationHandlers } from './delegation.js';
1
3
  import type {
2
4
  AICtrlWorkflowController,
3
5
  AICtrlWorkflowNodeRecord,
@@ -8,6 +10,7 @@ import type {
8
10
  AICtrlWorkflowRunProfileState,
9
11
  AICtrlWorkflowRuntime,
10
12
  AIAgentHostEffectPort,
13
+ AIAgentProcessorRuntime,
11
14
  FrozenAIAgentTaskBinding,
12
15
  AIWorkflowAuthoredEffects,
13
16
  AIWorkflowEffectProvider,
@@ -25,10 +28,11 @@ import type {
25
28
  import {
26
29
  commitFlowRunCheckpoint,
27
30
  createAIWorkflowRuntimeContext,
28
- defineAICtrlWorkflow,
29
31
  loadFlowRunCheckpoint,
30
32
  projectWorkCtrlSnapshotToCheckpointData,
31
33
  bindAIAgentProcessors,
34
+ bindWorkflowNodeTasks,
35
+ bindWorkflowTaskDelegation,
32
36
  } from 'ai-workflow-logic';
33
37
  import { assertFrozenAIAgentTaskBinding } from 'ai-workflow-logic/run-freeze';
34
38
  import type {
@@ -37,7 +41,6 @@ import type {
37
41
  FlowSourceCollection,
38
42
  LoadFlowSourcesOptions,
39
43
  } from 'instant-ctrl-flow-contract';
40
- import { loadCtrlFlowProfileFromSources } from 'instant-ctrl-flow-logic';
41
44
  import type {
42
45
  ResumeSignal,
43
46
  TickOutcome,
@@ -184,23 +187,39 @@ export function createAICtrlWorkflowAgentNodeRuntimeBinder(
184
187
  const proof = options.taskProofs[identity.nodeId];
185
188
  if (!proof) throw new Error(`AI_AGENT_TASK_PROOF_REQUIRED: no frozen proof for node ${identity.nodeId}.`);
186
189
  const taskBinding = assertFrozenAIAgentTaskBinding(proof).task;
187
- const effects: AIWorkflowAuthoredEffects = bindAIAgentProcessors({
190
+ const visit = identity.invocationKey.slice(identity.invocationKey.lastIndexOf('#') + 1);
191
+ const scope = identity.invocationKey.includes('/') ? identity.invocationKey.slice(0, identity.invocationKey.lastIndexOf('/')) : '';
192
+ const agentRuntime = {
188
193
  checkpointRuntime: options.checkpointRuntime,
189
194
  checkpointKey: { instanceId: options.flowInstanceId, runId: identity.treeId },
190
195
  nodeId: identity.nodeId,
191
196
  invocationKey: identity.invocationKey,
197
+ nodeContext:{generation:identity.generation??Number(visit),scope},
192
198
  workflowKind: 'AICtrlWorkflow',
193
199
  workflowRef: options.workflowRef,
194
200
  taskBinding,
195
201
  effects: options.effects,
196
202
  ...(options.metadata === undefined ? {} : { metadata: options.metadata }),
197
- });
198
- return Object.freeze({ ...runtime, ai: Object.freeze({ ...workflowRuntime.ai, effects }) });
203
+ } satisfies AIAgentProcessorRuntime;
204
+ const effects: AIWorkflowAuthoredEffects = bindAIAgentProcessors(agentRuntime);
205
+ const delegation=options.effects.delegation ? bindWorkflowTaskDelegation({
206
+ checkpointRuntime: options.checkpointRuntime,
207
+ checkpointKey: { instanceId: options.flowInstanceId, runId: identity.treeId },
208
+ nodeId: identity.nodeId, invocationKey: identity.invocationKey,
209
+ workflowKind: 'AICtrlWorkflow', workflowRef: options.workflowRef, taskBinding,
210
+ visit: identity.visit ?? `${scope}#${visit}`, generation: identity.generation ?? Number(visit), effects: options.effects.delegation,
211
+ }) : undefined;
212
+ return Object.freeze({ ...runtime, ai: Object.freeze({ ...workflowRuntime.ai, effects,
213
+ tasks:bindWorkflowNodeTasks(agentRuntime,delegation),
214
+ ...(delegation?{delegation}:{}),
215
+ }) });
199
216
  };
200
217
  }
201
218
 
202
219
  export interface CreateAICtrlWorkflowControllerOptions
203
220
  extends Omit<WorkCtrlFlowEngineOptions, 'definition' | 'runtime' | 'expectedForm'> {
221
+ /** Set by composition when a native delegation owner is bound. */
222
+ readonly suspendDelegations?: boolean;
204
223
  readonly binding: AICtrlWorkflowDefinitionBinding;
205
224
  readonly resolveCode: FlowCodeResolver<AICtrlWorkflowRuntime>;
206
225
  readonly resolveFlow?: WorkCtrlFlowDefinition['resolveFlow'];
@@ -218,6 +237,10 @@ export function createAICtrlWorkflowController(
218
237
  };
219
238
  const engine = new WorkCtrlFlowEngine({
220
239
  ...options,
240
+ ...(options.suspendDelegations ? { extraHandlers: controller => ({
241
+ ...createAICtrlWorkflowDelegationHandlers(options.binding.definition, options.resolveCode as FlowCodeResolver, options.bindNodeRuntime, controller),
242
+ ...options.extraHandlers?.(controller),
243
+ }) } : {}),
221
244
  expectedForm: 'WorkCtrlFlow',
222
245
  definition,
223
246
  runtime: createAICtrlWorkflowRuntime({ runtime: options.runtime, ai: options.ai }),
@@ -231,34 +254,7 @@ export function createAICtrlWorkflowController(
231
254
  };
232
255
  }
233
256
 
234
- export function defineWorkCtrlFlowBackedAICtrlWorkflow(
235
- definition: AICtrlWorkflowDefinitionBinding['definition'],
236
- ): AICtrlWorkflowDefinitionBinding {
237
- return defineAICtrlWorkflow(definition);
238
- }
239
-
240
- export interface AICtrlWorkflowSourceLoadResult {
241
- readonly binding?: AICtrlWorkflowDefinitionBinding;
242
- readonly diagnostics: readonly FlowDiagnostic[];
243
- }
244
-
245
- export function loadAICtrlWorkflowSources(
246
- sources: FlowSourceCollection,
247
- options: LoadFlowSourcesOptions = {},
248
- ): AICtrlWorkflowSourceLoadResult {
249
- const loaded = loadCtrlFlowProfileFromSources(sources, {
250
- ...options,
251
- profileRoot: 'AICtrlWorkflow',
252
- substrateForm: 'WorkCtrlFlow',
253
- });
254
- if (!loaded.spec || loaded.diagnostics.length > 0) {
255
- return { diagnostics: loaded.diagnostics };
256
- }
257
- return {
258
- binding: defineWorkCtrlFlowBackedAICtrlWorkflow(loaded.spec),
259
- diagnostics: loaded.diagnostics,
260
- };
261
- }
257
+ export * from './source-loader.js';
262
258
 
263
259
  function nodeIdFromInvocationKey(invocationKey: string): string {
264
260
  const withoutVisit = invocationKey.slice(0, invocationKey.lastIndexOf('#'));
@@ -0,0 +1,37 @@
1
+ import type { AICtrlWorkflowDefinitionBinding } from "ai-workflow-contract";
2
+ import { defineAICtrlWorkflow } from "ai-workflow-logic/definitions";
3
+ import type {
4
+ FlowDiagnostic,
5
+ FlowSourceCollection,
6
+ LoadFlowSourcesOptions,
7
+ } from "instant-ctrl-flow-contract";
8
+ import { loadCtrlFlowProfileFromSources } from "instant-ctrl-flow-logic/browser";
9
+
10
+ export function defineWorkCtrlFlowBackedAICtrlWorkflow(
11
+ definition: AICtrlWorkflowDefinitionBinding["definition"],
12
+ ): AICtrlWorkflowDefinitionBinding {
13
+ return defineAICtrlWorkflow(definition);
14
+ }
15
+
16
+ export interface AICtrlWorkflowSourceLoadResult {
17
+ readonly binding?: AICtrlWorkflowDefinitionBinding;
18
+ readonly diagnostics: readonly FlowDiagnostic[];
19
+ }
20
+
21
+ export function loadAICtrlWorkflowSources(
22
+ sources: FlowSourceCollection,
23
+ options: LoadFlowSourcesOptions = {},
24
+ ): AICtrlWorkflowSourceLoadResult {
25
+ const loaded = loadCtrlFlowProfileFromSources(sources, {
26
+ ...options,
27
+ profileRoot: "AICtrlWorkflow",
28
+ substrateForm: "WorkCtrlFlow",
29
+ });
30
+ if (!loaded.spec || loaded.diagnostics.length > 0) {
31
+ return { diagnostics: loaded.diagnostics };
32
+ }
33
+ return {
34
+ binding: defineWorkCtrlFlowBackedAICtrlWorkflow(loaded.spec),
35
+ diagnostics: loaded.diagnostics,
36
+ };
37
+ }