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 +14 -12
- package/src/delegation.ts +55 -0
- package/src/filesystem.ts +20 -8
- package/src/index.ts +29 -33
- package/src/source-loader.ts +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-ctrl-workflow-logic",
|
|
3
|
-
"version": "0.1
|
|
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
|
-
"
|
|
15
|
-
"ai-workflow-contract": "0.1
|
|
16
|
-
"ai-workflow-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"instant-ctrl-flow-
|
|
20
|
-
"
|
|
21
|
-
"task-manager-
|
|
22
|
-
"
|
|
23
|
-
"work-ctrl-flow-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|