ai-ctrl-workflow-logic 0.2.0 → 0.2.2
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 +16 -13
- package/src/delegation.ts +55 -0
- package/src/filesystem.ts +20 -8
- package/src/index.ts +29 -34
- package/src/source-loader.ts +37 -0
- package/src/holon-task.ts +0 -264
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-ctrl-workflow-logic",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "WorkCtrlFlow-backed AI Ctrl Workflow adapter logic",
|
|
6
6
|
"type": "module",
|
|
@@ -8,24 +8,27 @@
|
|
|
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.2.
|
|
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.2",
|
|
17
|
+
"ai-workflow-contract": "0.2.3",
|
|
18
|
+
"ai-workflow-logic": "0.2.3",
|
|
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"
|
|
27
29
|
],
|
|
28
30
|
"publishConfig": {
|
|
29
|
-
"access": "public"
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.com/"
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -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('#'));
|
|
@@ -331,4 +327,3 @@ export async function getAICtrlWorkflowRunRecord(
|
|
|
331
327
|
return snapshot ? projectAICtrlWorkflowRunRecord(snapshot, await controller.getOutcome(treeId)) : undefined;
|
|
332
328
|
}
|
|
333
329
|
|
|
334
|
-
export * from './holon-task.js';
|
|
@@ -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
|
+
}
|
package/src/holon-task.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createAIOrganizationTaskProfile,
|
|
3
|
-
normalizeHolonTaskSnapshotReceipt,
|
|
4
|
-
normalizeHolonTaskTarget,
|
|
5
|
-
type FrozenHolonTaskTarget,
|
|
6
|
-
type HolonTaskSnapshotAdoptionReceipt,
|
|
7
|
-
type HolonTaskSnapshotReceipt,
|
|
8
|
-
} from 'ai-workflow-contract';
|
|
9
|
-
import { assertFrozenHolonTaskTarget } from 'ai-workflow-logic/run-freeze';
|
|
10
|
-
import {
|
|
11
|
-
createHolonTaskSpace,
|
|
12
|
-
prepareHolonTaskSnapshot,
|
|
13
|
-
type CreateHolonTaskSpaceConfig,
|
|
14
|
-
type CreateHolonTaskSpaceRuntime,
|
|
15
|
-
type HolonOrganizationSnapshotReadPort,
|
|
16
|
-
} from 'ai-workflow-logic';
|
|
17
|
-
import type {
|
|
18
|
-
TaskClaimToken,
|
|
19
|
-
TaskCommandReceipt,
|
|
20
|
-
TaskRecord,
|
|
21
|
-
TaskSettlementReceipt,
|
|
22
|
-
TaskSpaceSnapshot,
|
|
23
|
-
} from 'task-manager-contract';
|
|
24
|
-
import {
|
|
25
|
-
cancelTask,
|
|
26
|
-
replanTask,
|
|
27
|
-
type TaskManagerRuntime,
|
|
28
|
-
type TaskProcessorResult,
|
|
29
|
-
} from 'task-manager-logic';
|
|
30
|
-
|
|
31
|
-
export interface AICtrlHolonTaskRuntime {
|
|
32
|
-
readonly taskManager: TaskManagerRuntime;
|
|
33
|
-
readonly organizationSnapshots: HolonOrganizationSnapshotReadPort;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface OpenAICtrlHolonTaskInput {
|
|
37
|
-
readonly frozenTarget: FrozenHolonTaskTarget;
|
|
38
|
-
readonly commandId: string;
|
|
39
|
-
readonly taskSpaceId: string;
|
|
40
|
-
readonly taskId: string;
|
|
41
|
-
readonly taskName: string;
|
|
42
|
-
readonly createdAt: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface OpenAICtrlHolonTaskResult {
|
|
46
|
-
readonly snapshot: TaskSpaceSnapshot;
|
|
47
|
-
readonly receipt: TaskCommandReceipt;
|
|
48
|
-
readonly snapshotReceipt: HolonTaskSnapshotReceipt;
|
|
49
|
-
readonly created: boolean;
|
|
50
|
-
readonly replayed: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function openAICtrlHolonTask(
|
|
54
|
-
runtime: AICtrlHolonTaskRuntime,
|
|
55
|
-
input: OpenAICtrlHolonTaskInput,
|
|
56
|
-
config: CreateHolonTaskSpaceConfig,
|
|
57
|
-
): Promise<OpenAICtrlHolonTaskResult> {
|
|
58
|
-
const proof = assertFrozenHolonTaskTarget(input.frozenTarget);
|
|
59
|
-
const existing = await runtime.taskManager.owner.readSnapshot(input.taskSpaceId);
|
|
60
|
-
if (existing) {
|
|
61
|
-
const task = exactOrganizationTask(existing, input.taskId);
|
|
62
|
-
const target = normalizeHolonTaskTarget(task.profile.facts.target);
|
|
63
|
-
if (JSON.stringify(target) !== JSON.stringify(proof.target)) {
|
|
64
|
-
throw new Error('AI_CTRL_HOLON_TASK_TARGET_CONFLICT: existing TaskSpace is bound to another frozen target');
|
|
65
|
-
}
|
|
66
|
-
const receipt = await runtime.taskManager.owner.readReceiptByCommand(input.taskSpaceId, input.commandId);
|
|
67
|
-
if (!receipt) throw new Error('AI_CTRL_HOLON_TASK_OPEN_RECEIPT_MISSING: existing TaskSpace has no matching create receipt');
|
|
68
|
-
return Object.freeze({
|
|
69
|
-
snapshot: existing,
|
|
70
|
-
receipt,
|
|
71
|
-
snapshotReceipt: normalizeHolonTaskSnapshotReceipt(task.profile.facts.snapshotReceipt),
|
|
72
|
-
created: false,
|
|
73
|
-
replayed: true,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
const created = await createHolonTaskSpace(runtime as CreateHolonTaskSpaceRuntime, {
|
|
77
|
-
commandId: input.commandId,
|
|
78
|
-
taskSpaceId: input.taskSpaceId,
|
|
79
|
-
taskId: input.taskId,
|
|
80
|
-
taskName: input.taskName,
|
|
81
|
-
target: proof.target,
|
|
82
|
-
createdAt: input.createdAt,
|
|
83
|
-
}, config);
|
|
84
|
-
return Object.freeze({ ...created, created: true });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export type ObserveAICtrlHolonTaskResult =
|
|
88
|
-
| Readonly<{
|
|
89
|
-
readonly kind: 'waiting';
|
|
90
|
-
readonly taskSpaceId: string;
|
|
91
|
-
readonly taskId: string;
|
|
92
|
-
readonly expectedSettlementCommandId: string;
|
|
93
|
-
readonly observedRevision: number;
|
|
94
|
-
}>
|
|
95
|
-
| Readonly<{
|
|
96
|
-
readonly kind: 'settled';
|
|
97
|
-
readonly receipt: TaskSettlementReceipt;
|
|
98
|
-
readonly task: TaskRecord;
|
|
99
|
-
}>;
|
|
100
|
-
|
|
101
|
-
export async function observeAICtrlHolonTask(
|
|
102
|
-
runtime: Pick<AICtrlHolonTaskRuntime, 'taskManager'>,
|
|
103
|
-
input: Readonly<{
|
|
104
|
-
readonly taskSpaceId: string;
|
|
105
|
-
readonly taskId: string;
|
|
106
|
-
readonly settlementCommandId: string;
|
|
107
|
-
}>,
|
|
108
|
-
_config: Readonly<Record<string, never>>,
|
|
109
|
-
): Promise<ObserveAICtrlHolonTaskResult> {
|
|
110
|
-
const snapshot = await runtime.taskManager.owner.readSnapshot(input.taskSpaceId);
|
|
111
|
-
if (!snapshot) throw new Error(`AI_CTRL_HOLON_TASK_SPACE_MISSING: ${input.taskSpaceId}`);
|
|
112
|
-
const task = exactOrganizationTask(snapshot, input.taskId);
|
|
113
|
-
const receipt = await runtime.taskManager.owner.readReceiptByCommand(input.taskSpaceId, input.settlementCommandId);
|
|
114
|
-
if (!receipt) return Object.freeze({
|
|
115
|
-
kind: 'waiting',
|
|
116
|
-
taskSpaceId: input.taskSpaceId,
|
|
117
|
-
taskId: input.taskId,
|
|
118
|
-
expectedSettlementCommandId: input.settlementCommandId,
|
|
119
|
-
observedRevision: snapshot.revision,
|
|
120
|
-
});
|
|
121
|
-
if (receipt.kind !== 'task-settlement-receipt' || receipt.taskId !== input.taskId
|
|
122
|
-
|| !['Succeeded', 'Failed', 'Cancelled'].includes(task.status)
|
|
123
|
-
|| receipt.status !== task.status) {
|
|
124
|
-
throw new Error('AI_CTRL_HOLON_TASK_SETTLEMENT_RECEIPT_MISMATCH: receipt does not bind the terminal task projection');
|
|
125
|
-
}
|
|
126
|
-
return Object.freeze({ kind: 'settled', receipt, task });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface ReplanAICtrlHolonTaskInput {
|
|
130
|
-
readonly frozenSuccessorTarget: FrozenHolonTaskTarget;
|
|
131
|
-
readonly taskSpaceId: string;
|
|
132
|
-
readonly previousTaskId: string;
|
|
133
|
-
readonly successorTaskId: string;
|
|
134
|
-
readonly successorTaskName: string;
|
|
135
|
-
readonly cancelCommandId: string;
|
|
136
|
-
readonly replanCommandId: string;
|
|
137
|
-
readonly planId: string;
|
|
138
|
-
readonly replannedAt: string;
|
|
139
|
-
readonly activeClaim?: TaskClaimToken;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface ReplanAICtrlHolonTaskResult extends TaskProcessorResult {
|
|
143
|
-
readonly adoptionReceipt: HolonTaskSnapshotAdoptionReceipt;
|
|
144
|
-
readonly successorSnapshotReceipt: HolonTaskSnapshotReceipt;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function replanAICtrlHolonTask(
|
|
148
|
-
runtime: AICtrlHolonTaskRuntime,
|
|
149
|
-
input: ReplanAICtrlHolonTaskInput,
|
|
150
|
-
config: CreateHolonTaskSpaceConfig,
|
|
151
|
-
): Promise<ReplanAICtrlHolonTaskResult> {
|
|
152
|
-
const successorProof = assertFrozenHolonTaskTarget(input.frozenSuccessorTarget);
|
|
153
|
-
const initial = await requiredSnapshot(runtime.taskManager, input.taskSpaceId);
|
|
154
|
-
const previous = exactOrganizationTask(initial, input.previousTaskId);
|
|
155
|
-
const previousTarget = normalizeHolonTaskTarget(previous.profile.facts.target);
|
|
156
|
-
const previousSnapshotReceipt = normalizeHolonTaskSnapshotReceipt(previous.profile.facts.snapshotReceipt);
|
|
157
|
-
if (previousTarget.executionBinding.ref !== successorProof.target.executionBinding.ref
|
|
158
|
-
|| previousTarget.executionBinding.digest !== successorProof.target.executionBinding.digest) {
|
|
159
|
-
throw new Error('AI_CTRL_HOLON_TASK_REPLAN_SUCCESSOR_INSTANCE_REQUIRED: execution binding changed');
|
|
160
|
-
}
|
|
161
|
-
if (previousTarget.invocation.workflowKind !== successorProof.target.invocation.workflowKind
|
|
162
|
-
|| previousTarget.invocation.workflowRef !== successorProof.target.invocation.workflowRef
|
|
163
|
-
|| previousTarget.invocation.nodeId !== successorProof.target.invocation.nodeId) {
|
|
164
|
-
throw new Error('AI_CTRL_HOLON_TASK_REPLAN_SUCCESSOR_INSTANCE_REQUIRED: workflow executable identity changed');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Read and validate the successor authority before mutating the current task.
|
|
168
|
-
// The following cancel + replan commands are independently idempotent, so a
|
|
169
|
-
// retry can converge after an interruption between their commits.
|
|
170
|
-
const prepared = await prepareHolonTaskSnapshot(runtime, {
|
|
171
|
-
taskSpaceId: input.taskSpaceId,
|
|
172
|
-
target: successorProof.target,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
let current = initial;
|
|
176
|
-
if (!['Succeeded', 'Failed', 'Cancelled'].includes(previous.status)) {
|
|
177
|
-
if (previous.activeClaim && (!input.activeClaim || JSON.stringify(previous.activeClaim) !== JSON.stringify(input.activeClaim))) {
|
|
178
|
-
throw new Error('AI_CTRL_HOLON_TASK_REPLAN_CLAIM_REQUIRED: active task requires its exact claim capability');
|
|
179
|
-
}
|
|
180
|
-
const cancelled = await cancelTask(runtime.taskManager, {
|
|
181
|
-
kind: 'task.cancel',
|
|
182
|
-
commandId: input.cancelCommandId,
|
|
183
|
-
taskSpaceId: input.taskSpaceId,
|
|
184
|
-
expectedRevision: current.revision,
|
|
185
|
-
taskId: previous.taskId,
|
|
186
|
-
cancelledAt: input.replannedAt,
|
|
187
|
-
reason: 'organization-snapshot-replan',
|
|
188
|
-
...(input.activeClaim === undefined ? {} : { claim: input.activeClaim }),
|
|
189
|
-
}, config);
|
|
190
|
-
current = cancelled.snapshot;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const adoptionReceipt: HolonTaskSnapshotAdoptionReceipt = Object.freeze({
|
|
194
|
-
kind: 'holon-task-snapshot-adoption-receipt',
|
|
195
|
-
schemaVersion: 'ai-workflow.holon-task-snapshot-adoption-receipt/v1',
|
|
196
|
-
taskSpaceId: input.taskSpaceId,
|
|
197
|
-
previousTaskId: input.previousTaskId,
|
|
198
|
-
successorTaskId: input.successorTaskId,
|
|
199
|
-
previousIssuerReceiptId: previousSnapshotReceipt.issuerReceiptId,
|
|
200
|
-
successorIssuerReceiptId: prepared.snapshotReceipt.issuerReceiptId,
|
|
201
|
-
executionBindingDigest: previousTarget.executionBinding.digest,
|
|
202
|
-
adoptedAt: input.replannedAt,
|
|
203
|
-
});
|
|
204
|
-
const priorAdoptions = Array.isArray(previous.profile.facts.snapshotAdoptions)
|
|
205
|
-
? previous.profile.facts.snapshotAdoptions
|
|
206
|
-
: [];
|
|
207
|
-
const successorProfile = createAIOrganizationTaskProfile(
|
|
208
|
-
prepared.target,
|
|
209
|
-
prepared.snapshotReceipt,
|
|
210
|
-
[...priorAdoptions, adoptionReceipt],
|
|
211
|
-
);
|
|
212
|
-
const successorDefinition = Object.freeze({
|
|
213
|
-
kind: 'task' as const,
|
|
214
|
-
taskId: input.successorTaskId,
|
|
215
|
-
name: input.successorTaskName,
|
|
216
|
-
order: current.definition.tasks.length,
|
|
217
|
-
profile: successorProfile,
|
|
218
|
-
inputArtifacts: prepared.artifactRefs,
|
|
219
|
-
});
|
|
220
|
-
const rootCount = current.definition.relations.filter((relation) => relation.kind === 'parent-child' && relation.parentTaskId === null).length;
|
|
221
|
-
const replanned = await replanTask(runtime.taskManager, {
|
|
222
|
-
kind: 'task.replan',
|
|
223
|
-
commandId: input.replanCommandId,
|
|
224
|
-
taskSpaceId: input.taskSpaceId,
|
|
225
|
-
expectedRevision: current.revision,
|
|
226
|
-
replannedAt: input.replannedAt,
|
|
227
|
-
plan: {
|
|
228
|
-
kind: 'task-plan',
|
|
229
|
-
planId: input.planId,
|
|
230
|
-
tasks: [...current.definition.tasks, successorDefinition],
|
|
231
|
-
relations: [
|
|
232
|
-
...current.definition.relations,
|
|
233
|
-
{
|
|
234
|
-
kind: 'parent-child',
|
|
235
|
-
relationId: `root:${input.successorTaskId}`,
|
|
236
|
-
parentTaskId: null,
|
|
237
|
-
childTaskId: input.successorTaskId,
|
|
238
|
-
order: rootCount,
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
reason: 'organization-snapshot-replan',
|
|
242
|
-
},
|
|
243
|
-
artifactBodies: prepared.artifactBodies,
|
|
244
|
-
}, config);
|
|
245
|
-
return Object.freeze({
|
|
246
|
-
...replanned,
|
|
247
|
-
adoptionReceipt,
|
|
248
|
-
successorSnapshotReceipt: prepared.snapshotReceipt,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function exactOrganizationTask(snapshot: TaskSpaceSnapshot, taskId: string): TaskRecord {
|
|
253
|
-
const task = snapshot.tasks.find((candidate) => candidate.taskId === taskId);
|
|
254
|
-
if (!task || task.profile.profileKind !== 'depa.ai.organization-task' || task.profile.schemaVersion !== 1) {
|
|
255
|
-
throw new Error(`AI_CTRL_HOLON_TASK_PROFILE_MISMATCH: ${taskId}`);
|
|
256
|
-
}
|
|
257
|
-
return task;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function requiredSnapshot(runtime: TaskManagerRuntime, taskSpaceId: string): Promise<TaskSpaceSnapshot> {
|
|
261
|
-
const snapshot = await runtime.owner.readSnapshot(taskSpaceId);
|
|
262
|
-
if (!snapshot) throw new Error(`AI_CTRL_HOLON_TASK_SPACE_MISSING: ${taskSpaceId}`);
|
|
263
|
-
return snapshot;
|
|
264
|
-
}
|