@exellix/graph-engine 7.8.1 → 8.0.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/CHANGELOG.md +42 -0
- package/README.md +16 -13
- package/dist/src/adapters/compileExellixExecutablePlan.d.ts +8 -0
- package/dist/src/adapters/compileExellixExecutablePlan.js +18 -0
- package/dist/src/adapters/migrateExellixGraphModelToAuthoring.d.ts +6 -0
- package/dist/src/adapters/migrateExellixGraphModelToAuthoring.js +273 -0
- package/dist/src/adapters/patchFinalizerPlans.d.ts +7 -0
- package/dist/src/adapters/patchFinalizerPlans.js +63 -0
- package/dist/src/errors/exellixGraphErrorCodes.d.ts +5 -0
- package/dist/src/errors/exellixGraphErrorCodes.js +5 -0
- package/dist/src/index.d.ts +9 -2
- package/dist/src/index.js +6 -2
- package/dist/src/integrations/ActivixNodeActivityIntegration.js +7 -0
- package/dist/src/plan/aiModelSelectionWire.d.ts +11 -0
- package/dist/src/plan/aiModelSelectionWire.js +39 -0
- package/dist/src/plan/applyNodePlanInvoke.d.ts +10 -0
- package/dist/src/plan/applyNodePlanInvoke.js +67 -0
- package/dist/src/plan/embeddedGraphToExellixGraph.d.ts +5 -0
- package/dist/src/plan/embeddedGraphToExellixGraph.js +131 -0
- package/dist/src/plan/planDeferredGates.d.ts +16 -0
- package/dist/src/plan/planDeferredGates.js +118 -0
- package/dist/src/plan/planExecuteEntry.d.ts +12 -0
- package/dist/src/plan/planExecuteEntry.js +73 -0
- package/dist/src/plan/planExecutionPipeline.d.ts +11 -0
- package/dist/src/plan/planExecutionPipeline.js +54 -0
- package/dist/src/plan/planModelConfig.d.ts +10 -0
- package/dist/src/plan/planModelConfig.js +46 -0
- package/dist/src/runtime/ExellixGraphRuntime.d.ts +9 -6
- package/dist/src/runtime/ExellixGraphRuntime.js +138 -98
- package/dist/src/runtime/executionMatrixHost.js +2 -1
- package/dist/src/runtime/studioGraphExecuteRequest.d.ts +51 -0
- package/dist/src/runtime/studioGraphExecuteRequest.js +78 -0
- package/dist/testkit/buildExecuteGraphInput.d.ts +4 -0
- package/dist/testkit/buildExecuteGraphInput.js +8 -0
- package/dist/testkit/index.d.ts +1 -0
- package/dist/testkit/index.js +1 -0
- package/dist/testkit/testModelAliasRuntime.js +2 -2
- package/package.json +9 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps plan {@link AiModelSelection} to ai-tasks 8.4+ wire profile strings.
|
|
3
|
+
*/
|
|
4
|
+
export function aiModelSelectionToWireString(selection) {
|
|
5
|
+
if (selection == null)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (selection.kind === 'profileChoice')
|
|
8
|
+
return selection.key.trim();
|
|
9
|
+
if (selection.kind === 'profile') {
|
|
10
|
+
const profile = selection.profile.trim();
|
|
11
|
+
const choice = typeof selection.choice === 'string' ? selection.choice.trim() : '';
|
|
12
|
+
return choice ? `${profile}/${choice}` : profile;
|
|
13
|
+
}
|
|
14
|
+
if (selection.kind === 'model') {
|
|
15
|
+
const modelId = typeof selection.modelId === 'string'
|
|
16
|
+
? selection.modelId
|
|
17
|
+
: typeof selection.model === 'string'
|
|
18
|
+
? selection.model
|
|
19
|
+
: '';
|
|
20
|
+
return `${selection.provider}/${modelId}`.replace(/\/+/g, '/');
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
export function resolvedSnapshotToWireString(snapshot) {
|
|
25
|
+
if (snapshot == null)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (snapshot.purpose === 'strictReplay' || snapshot.purpose === 'debugReplay') {
|
|
28
|
+
return `${snapshot.provider}/${snapshot.modelId}`;
|
|
29
|
+
}
|
|
30
|
+
if (snapshot.profileChoiceKey)
|
|
31
|
+
return snapshot.profileChoiceKey;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export function unitModelWireString(args) {
|
|
35
|
+
const fromSnapshot = resolvedSnapshotToWireString(args.resolvedInvocationSnapshot);
|
|
36
|
+
if (fromSnapshot)
|
|
37
|
+
return fromSnapshot;
|
|
38
|
+
return aiModelSelectionToWireString(args.modelSelection);
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FinalizerExecutionPlan, NodeExecutionPlan } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { TaskNode } from '../types/refs.js';
|
|
3
|
+
/** Merge frozen plan invoke contract onto exellix task node before execution. */
|
|
4
|
+
export declare function applyNodePlanInvoke(node: TaskNode, nodePlan: NodeExecutionPlan): TaskNode;
|
|
5
|
+
export declare function finalizerPlanForId(plan: {
|
|
6
|
+
finalizerPlans: Record<string, FinalizerExecutionPlan>;
|
|
7
|
+
}, nodeId: string): FinalizerExecutionPlan | undefined;
|
|
8
|
+
export declare function nodePlanForId(plan: {
|
|
9
|
+
nodePlans: Record<string, NodeExecutionPlan>;
|
|
10
|
+
}, nodeId: string): NodeExecutionPlan | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function isPlainRecord(v) {
|
|
2
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
/** Merge frozen plan invoke contract onto exellix task node before execution. */
|
|
5
|
+
export function applyNodePlanInvoke(node, nodePlan) {
|
|
6
|
+
const patched = structuredClone(node);
|
|
7
|
+
const ic = nodePlan.invokeContract;
|
|
8
|
+
if (!ic)
|
|
9
|
+
return patched;
|
|
10
|
+
if (ic.taskVariable != null)
|
|
11
|
+
patched.taskVariable = structuredClone(ic.taskVariable);
|
|
12
|
+
if (ic.inputsConfig != null)
|
|
13
|
+
patched.inputsConfig = structuredClone(ic.inputsConfig);
|
|
14
|
+
if (ic.executionMapping != null)
|
|
15
|
+
patched.executionMapping = structuredClone(ic.executionMapping);
|
|
16
|
+
if (ic.jobContextMapping != null)
|
|
17
|
+
patched.jobContextMapping = structuredClone(ic.jobContextMapping);
|
|
18
|
+
if (ic.smartInput != null)
|
|
19
|
+
patched.smartInput = structuredClone(ic.smartInput);
|
|
20
|
+
if (ic.scope != null)
|
|
21
|
+
patched.scope = structuredClone(ic.scope);
|
|
22
|
+
if (ic.memory != null)
|
|
23
|
+
patched.memory = structuredClone(ic.memory);
|
|
24
|
+
if (Array.isArray(ic.knowledgeRefs?.taskKnowledge)) {
|
|
25
|
+
patched.taskKnowledge = ic.knowledgeRefs.taskKnowledge.map((ref) => {
|
|
26
|
+
if (typeof ref === 'string')
|
|
27
|
+
return ref;
|
|
28
|
+
if (isPlainRecord(ref) && typeof ref.id === 'string')
|
|
29
|
+
return ref.id;
|
|
30
|
+
return String(ref);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const tc = { ...(patched.taskConfiguration ?? {}) };
|
|
34
|
+
const pipeline = ic.pipeline;
|
|
35
|
+
if (isPlainRecord(pipeline)) {
|
|
36
|
+
if (pipeline.executionStrategies !== undefined)
|
|
37
|
+
tc.executionStrategies = pipeline.executionStrategies;
|
|
38
|
+
if (pipeline.aiTaskStrategies !== undefined)
|
|
39
|
+
tc.aiTaskStrategies = pipeline.aiTaskStrategies;
|
|
40
|
+
if (pipeline.aiTaskProfile !== undefined)
|
|
41
|
+
tc.aiTaskProfile = pipeline.aiTaskProfile;
|
|
42
|
+
if (pipeline.narrix !== undefined)
|
|
43
|
+
tc.narrix = pipeline.narrix;
|
|
44
|
+
if (pipeline.narrixInput !== undefined)
|
|
45
|
+
tc.narrixInput = pipeline.narrixInput;
|
|
46
|
+
if (pipeline.narrixMode !== undefined)
|
|
47
|
+
tc.narrixMode = pipeline.narrixMode;
|
|
48
|
+
if (pipeline.llmCall !== undefined)
|
|
49
|
+
tc.llmCall = pipeline.llmCall;
|
|
50
|
+
}
|
|
51
|
+
if (ic.validation?.aiTasksOutputValidation != null) {
|
|
52
|
+
tc.aiTasksOutputValidation = ic.validation.aiTasksOutputValidation;
|
|
53
|
+
}
|
|
54
|
+
if (ic.validation?.outputValidation != null) {
|
|
55
|
+
patched.outputValidation = ic.validation.outputValidation;
|
|
56
|
+
}
|
|
57
|
+
if (ic.localConfig != null)
|
|
58
|
+
Object.assign(tc, ic.localConfig);
|
|
59
|
+
patched.taskConfiguration = tc;
|
|
60
|
+
return patched;
|
|
61
|
+
}
|
|
62
|
+
export function finalizerPlanForId(plan, nodeId) {
|
|
63
|
+
return plan.finalizerPlans[nodeId];
|
|
64
|
+
}
|
|
65
|
+
export function nodePlanForId(plan, nodeId) {
|
|
66
|
+
return plan.nodePlans[nodeId];
|
|
67
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExecutableGraphPlanV2, NormalizedExecutableGraphDocument } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { Graph } from '../types/refs.js';
|
|
3
|
+
/** Materialize exellix {@link Graph} from plan embedded normalized authoring graph. */
|
|
4
|
+
export declare function embeddedGraphToExellixGraph(plan: ExecutableGraphPlanV2): Graph;
|
|
5
|
+
export declare function getEmbeddedExecutableDocument(plan: ExecutableGraphPlanV2): NormalizedExecutableGraphDocument;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getFinalizerNodeConfig, getTaskNodeConfig, isExecutableFinalizerNode, isExecutableTaskNode, } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
function isPlainRecord(v) {
|
|
3
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
4
|
+
}
|
|
5
|
+
function taskNodeFromExecutableNode(node) {
|
|
6
|
+
const params = getTaskNodeConfig(node);
|
|
7
|
+
if (!params) {
|
|
8
|
+
throw new Error(`Expected executable task node "${node.id}"`);
|
|
9
|
+
}
|
|
10
|
+
const tc = isPlainRecord(params.taskConfiguration) ? { ...params.taskConfiguration } : {};
|
|
11
|
+
const out = {
|
|
12
|
+
id: node.id,
|
|
13
|
+
type: 'task',
|
|
14
|
+
skillKey: typeof params.skillKey === 'string' ? params.skillKey : undefined,
|
|
15
|
+
};
|
|
16
|
+
if (params.taskVariable !== undefined)
|
|
17
|
+
out.taskVariable = structuredClone(params.taskVariable);
|
|
18
|
+
if (params.inputsConfig !== undefined)
|
|
19
|
+
out.inputsConfig = structuredClone(params.inputsConfig);
|
|
20
|
+
if (params.executionMapping !== undefined) {
|
|
21
|
+
out.executionMapping = structuredClone(params.executionMapping);
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(params.taskKnowledge)) {
|
|
24
|
+
out.taskKnowledge = params.taskKnowledge.map((ref) => {
|
|
25
|
+
if (typeof ref === 'string')
|
|
26
|
+
return ref;
|
|
27
|
+
if (isPlainRecord(ref) && typeof ref.id === 'string')
|
|
28
|
+
return ref.id;
|
|
29
|
+
return String(ref);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (params.conditions !== undefined)
|
|
33
|
+
out.conditions = structuredClone(params.conditions);
|
|
34
|
+
if (isPlainRecord(params.variables))
|
|
35
|
+
out.variables = structuredClone(params.variables);
|
|
36
|
+
if (isPlainRecord(params.metadata))
|
|
37
|
+
out.metadata = structuredClone(params.metadata);
|
|
38
|
+
out.taskConfiguration = tc;
|
|
39
|
+
const pipeline = params.executionPipeline;
|
|
40
|
+
if (Array.isArray(pipeline))
|
|
41
|
+
out.executionPipeline = structuredClone(pipeline);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function finalizerNodeFromExecutableNode(node) {
|
|
45
|
+
const params = getFinalizerNodeConfig(node);
|
|
46
|
+
if (!params) {
|
|
47
|
+
throw new Error(`Expected executable finalizer node "${node.id}"`);
|
|
48
|
+
}
|
|
49
|
+
const paramsRecord = params;
|
|
50
|
+
return {
|
|
51
|
+
id: node.id,
|
|
52
|
+
type: 'finalizer',
|
|
53
|
+
finalizerType: params.finalizerType,
|
|
54
|
+
config: isPlainRecord(params.config) ? structuredClone(params.config) : {},
|
|
55
|
+
inputs: isPlainRecord(params.inputs) ? structuredClone(params.inputs) : undefined,
|
|
56
|
+
...(isPlainRecord(paramsRecord.outputMapping)
|
|
57
|
+
? { outputMapping: structuredClone(paramsRecord.outputMapping) }
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function knowledgeRefsToExellixStrings(refs) {
|
|
62
|
+
if (!Array.isArray(refs))
|
|
63
|
+
return undefined;
|
|
64
|
+
return refs.map((ref) => {
|
|
65
|
+
if (typeof ref === 'string')
|
|
66
|
+
return ref;
|
|
67
|
+
if (isPlainRecord(ref) && typeof ref.id === 'string')
|
|
68
|
+
return ref.id;
|
|
69
|
+
return String(ref);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function edgesFromPlan(plan) {
|
|
73
|
+
return plan.topology.edges.map((edgeRef) => {
|
|
74
|
+
const deferred = plan.deferredGates.edgeGates[edgeRef.edgeId];
|
|
75
|
+
const out = {
|
|
76
|
+
from: edgeRef.from.nodeId,
|
|
77
|
+
to: edgeRef.to.nodeId,
|
|
78
|
+
};
|
|
79
|
+
if (deferred?.when != null)
|
|
80
|
+
out.when = deferred.when;
|
|
81
|
+
return out;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function responseFromEmbedded(doc, plan) {
|
|
85
|
+
const metaResponse = doc.graph.metadata?.graphResponse;
|
|
86
|
+
if (isPlainRecord(metaResponse)) {
|
|
87
|
+
const out = {
|
|
88
|
+
shape: isPlainRecord(metaResponse.shape) ? structuredClone(metaResponse.shape) : {},
|
|
89
|
+
};
|
|
90
|
+
if (metaResponse.missing !== undefined) {
|
|
91
|
+
out.missing = metaResponse.missing;
|
|
92
|
+
}
|
|
93
|
+
if (metaResponse.version !== undefined) {
|
|
94
|
+
out.version = metaResponse.version;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
if (isPlainRecord(plan.contracts?.response?.finalOutputSchema)) {
|
|
99
|
+
return { shape: { type: 'literal', value: {} } };
|
|
100
|
+
}
|
|
101
|
+
return { shape: {} };
|
|
102
|
+
}
|
|
103
|
+
/** Materialize exellix {@link Graph} from plan embedded normalized authoring graph. */
|
|
104
|
+
export function embeddedGraphToExellixGraph(plan) {
|
|
105
|
+
if (plan.normalizedGraph.mode !== 'embedded') {
|
|
106
|
+
throw new Error('embeddedGraphToExellixGraph requires embedded normalizedGraph');
|
|
107
|
+
}
|
|
108
|
+
const doc = plan.normalizedGraph.graph;
|
|
109
|
+
const nodes = [];
|
|
110
|
+
for (const node of doc.graph.nodes) {
|
|
111
|
+
if (isExecutableTaskNode(node))
|
|
112
|
+
nodes.push(taskNodeFromExecutableNode(node));
|
|
113
|
+
else if (isExecutableFinalizerNode(node))
|
|
114
|
+
nodes.push(finalizerNodeFromExecutableNode(node));
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: plan.source.graphId,
|
|
118
|
+
version: plan.source.graphRevision,
|
|
119
|
+
nodes,
|
|
120
|
+
edges: edgesFromPlan(plan),
|
|
121
|
+
response: responseFromEmbedded(doc, plan),
|
|
122
|
+
metadata: isPlainRecord(doc.graph.metadata) ? structuredClone(doc.graph.metadata) : undefined,
|
|
123
|
+
jobKnowledge: knowledgeRefsToExellixStrings(isPlainRecord(doc.graph.metadata) ? doc.graph.metadata.jobKnowledge : undefined),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function getEmbeddedExecutableDocument(plan) {
|
|
127
|
+
if (plan.normalizedGraph.mode !== 'embedded') {
|
|
128
|
+
throw new Error('getEmbeddedExecutableDocument requires embedded normalizedGraph');
|
|
129
|
+
}
|
|
130
|
+
return plan.normalizedGraph.graph;
|
|
131
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DeferredEdgeGate, DeferredGateSet, DeferredNodeGate, ExecutableGraphPlanV2 } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { RunxClient } from '@x12i/runx';
|
|
3
|
+
import type { TaskNodeConditionEvalContext } from '../runtime/taskNodeConditionsEvaluation.js';
|
|
4
|
+
export declare function evaluateDeferredNodeGate(gate: DeferredNodeGate | undefined, ctx: TaskNodeConditionEvalContext, executionInput: Record<string, unknown>, runx?: RunxClient): Promise<{
|
|
5
|
+
ok: boolean;
|
|
6
|
+
skipReason?: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function evaluateDeferredEdgeGate(gate: DeferredEdgeGate | undefined, ctx: TaskNodeConditionEvalContext): boolean;
|
|
9
|
+
export declare function buildIncomingEdgeGateMap(plan: ExecutableGraphPlanV2): Map<string, DeferredEdgeGate[]>;
|
|
10
|
+
export declare function nodeDeferredGate(plan: ExecutableGraphPlanV2, nodeId: string): DeferredNodeGate | undefined;
|
|
11
|
+
export declare function hasPlanConditionalEdges(plan: ExecutableGraphPlanV2): boolean;
|
|
12
|
+
export declare function evaluatePlanEntryGates(gates: DeferredGateSet['entryGates'] | undefined, executionInput: Record<string, unknown>, ctx: TaskNodeConditionEvalContext, runx?: RunxClient): Promise<{
|
|
13
|
+
ok: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function planNeedsRunx(plan: ExecutableGraphPlanV2): boolean;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { evaluateGraphPredicate } from '../runtime/predicates.js';
|
|
2
|
+
import { evaluateConditionWhen } from '../runtime/taskNodeConditionsEvaluation.js';
|
|
3
|
+
function isPlainRecord(v) {
|
|
4
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
5
|
+
}
|
|
6
|
+
function gateConditionToWhen(cond) {
|
|
7
|
+
if (!isPlainRecord(cond)) {
|
|
8
|
+
return { jsonConditions: { condition: cond, parameters: {} } };
|
|
9
|
+
}
|
|
10
|
+
if (cond.jsonConditions != null || cond.jsConditionFunction != null || cond.aiCondition != null) {
|
|
11
|
+
return cond;
|
|
12
|
+
}
|
|
13
|
+
if (cond.ai != null && isPlainRecord(cond.ai)) {
|
|
14
|
+
return { aiCondition: cond.ai };
|
|
15
|
+
}
|
|
16
|
+
if (typeof cond.js === 'string') {
|
|
17
|
+
return { jsConditionFunction: { conditionFunctionId: cond.js, parameters: {} } };
|
|
18
|
+
}
|
|
19
|
+
if (cond.condition != null) {
|
|
20
|
+
return {
|
|
21
|
+
jsonConditions: {
|
|
22
|
+
condition: cond.condition,
|
|
23
|
+
parameters: isPlainRecord(cond.parameters) ? cond.parameters : {},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { jsonConditions: { condition: cond, parameters: {} } };
|
|
28
|
+
}
|
|
29
|
+
async function evalGateConditions(conditions, ctx, executionInput, runx, mode = 'runWhen') {
|
|
30
|
+
if (!conditions?.length)
|
|
31
|
+
return { ok: true };
|
|
32
|
+
for (const cond of conditions) {
|
|
33
|
+
const ev = await evaluateConditionWhen(gateConditionToWhen(cond), ctx, executionInput, { runx });
|
|
34
|
+
if (ev.error)
|
|
35
|
+
return { ok: false, skipReason: ev.skipReason ?? 'gate_eval_error' };
|
|
36
|
+
const pass = ev.ok === true;
|
|
37
|
+
if (mode === 'skipWhen' ? pass : !pass) {
|
|
38
|
+
return { ok: false, skipReason: ev.skipReason ?? 'gate_failed' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
export async function evaluateDeferredNodeGate(gate, ctx, executionInput, runx) {
|
|
44
|
+
if (!gate)
|
|
45
|
+
return { ok: true };
|
|
46
|
+
if (gate.skipWhen?.length) {
|
|
47
|
+
const skip = await evalGateConditions(gate.skipWhen, ctx, executionInput, runx, 'skipWhen');
|
|
48
|
+
if (!skip.ok)
|
|
49
|
+
return { ok: false, skipReason: skip.skipReason ?? 'skip_when' };
|
|
50
|
+
}
|
|
51
|
+
if (gate.runWhen?.length) {
|
|
52
|
+
return evalGateConditions(gate.runWhen, ctx, executionInput, runx, 'runWhen');
|
|
53
|
+
}
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
export function evaluateDeferredEdgeGate(gate, ctx) {
|
|
57
|
+
if (!gate?.when)
|
|
58
|
+
return true;
|
|
59
|
+
return evaluateGraphPredicate(gate.when, ctx);
|
|
60
|
+
}
|
|
61
|
+
export function buildIncomingEdgeGateMap(plan) {
|
|
62
|
+
const byTo = new Map();
|
|
63
|
+
for (const edgeRef of plan.topology.edges) {
|
|
64
|
+
if (!edgeRef.hasDeferredGate || !edgeRef.deferredGateId)
|
|
65
|
+
continue;
|
|
66
|
+
const gate = plan.deferredGates.edgeGates[edgeRef.edgeId];
|
|
67
|
+
if (!gate)
|
|
68
|
+
continue;
|
|
69
|
+
const to = edgeRef.to.nodeId;
|
|
70
|
+
const list = byTo.get(to) ?? [];
|
|
71
|
+
list.push(gate);
|
|
72
|
+
byTo.set(to, list);
|
|
73
|
+
}
|
|
74
|
+
return byTo;
|
|
75
|
+
}
|
|
76
|
+
export function nodeDeferredGate(plan, nodeId) {
|
|
77
|
+
return plan.deferredGates.nodeGates[nodeId];
|
|
78
|
+
}
|
|
79
|
+
export function hasPlanConditionalEdges(plan) {
|
|
80
|
+
return Object.keys(plan.deferredGates.edgeGates).length > 0;
|
|
81
|
+
}
|
|
82
|
+
export async function evaluatePlanEntryGates(gates, executionInput, ctx, runx) {
|
|
83
|
+
if (!gates?.length)
|
|
84
|
+
return { ok: true };
|
|
85
|
+
for (const gate of gates) {
|
|
86
|
+
const ev = await evaluateConditionWhen(gateConditionToWhen(gate.condition), ctx, executionInput, { runx });
|
|
87
|
+
if (ev.error || !ev.ok) {
|
|
88
|
+
return { ok: false, reason: ev.skipReason ?? 'entry_gate_rejected' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
export function planNeedsRunx(plan) {
|
|
94
|
+
const scan = (c) => {
|
|
95
|
+
if (!isPlainRecord(c))
|
|
96
|
+
return false;
|
|
97
|
+
if (c.js != null || c.ai != null)
|
|
98
|
+
return true;
|
|
99
|
+
if (c.jsConditionFunction != null || c.aiCondition != null)
|
|
100
|
+
return true;
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
for (const g of Object.values(plan.deferredGates.nodeGates)) {
|
|
104
|
+
for (const list of [g.runWhen, g.skipWhen]) {
|
|
105
|
+
if (list?.some((c) => scan(c)))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const g of Object.values(plan.deferredGates.edgeGates)) {
|
|
110
|
+
if (scan(g.when))
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
for (const g of plan.deferredGates.entryGates ?? []) {
|
|
114
|
+
if (scan(g.condition))
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExecutableGraphPlanV2 } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { GraphRuntimeObject } from '../runtime/ExellixGraphRuntime.js';
|
|
3
|
+
export type PlanExecuteEntryResult = {
|
|
4
|
+
plan: ExecutableGraphPlanV2;
|
|
5
|
+
};
|
|
6
|
+
export declare function requireEmbeddedNormalizedGraph(plan: ExecutableGraphPlanV2): void;
|
|
7
|
+
export declare function assertRuntimeBinding(plan: ExecutableGraphPlanV2, runtime: GraphRuntimeObject): void;
|
|
8
|
+
export declare function preparePlanExecuteEntry(planInput: unknown, runtime: GraphRuntimeObject): PlanExecuteEntryResult;
|
|
9
|
+
export declare function computePlanAudit(plan: ExecutableGraphPlanV2): {
|
|
10
|
+
planHash: string;
|
|
11
|
+
source: ExecutableGraphPlanV2['source'];
|
|
12
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { validateExecutablePlan } from '@x12i/graphenix-plan-format';
|
|
2
|
+
import { defaultHashFunction } from '@x12i/graphenix-executable-contracts';
|
|
3
|
+
import { ExellixGraphError } from '../errors/ExellixGraphError.js';
|
|
4
|
+
import { ExellixGraphErrorCode } from '../errors/exellixGraphErrorCodes.js';
|
|
5
|
+
function isPlainRecord(v) {
|
|
6
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
export function requireEmbeddedNormalizedGraph(plan) {
|
|
9
|
+
if (plan.normalizedGraph?.mode !== 'embedded') {
|
|
10
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, 'ExecutableGraphPlan.normalizedGraph.mode must be "embedded" for graph-engine execution', { graphId: plan.source?.graphId });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function assertRuntimeBinding(plan, runtime) {
|
|
14
|
+
const binding = plan.runtimeBinding;
|
|
15
|
+
const jobId = typeof runtime.jobId === 'string' ? runtime.jobId.trim() : '';
|
|
16
|
+
if (!jobId) {
|
|
17
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.JOB_ID_REQUIRED, 'runtime.jobId is required', {
|
|
18
|
+
graphId: plan.source?.graphId,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (binding.jobId !== jobId) {
|
|
22
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_RUNTIME, `Plan runtimeBinding.jobId "${binding.jobId}" does not match runtime.jobId "${jobId}"`, { graphId: plan.source?.graphId, jobId });
|
|
23
|
+
}
|
|
24
|
+
const hash = defaultHashFunction;
|
|
25
|
+
if (binding.inputHash != null && runtime.input !== undefined) {
|
|
26
|
+
const actual = hash(runtime.input);
|
|
27
|
+
if (actual !== binding.inputHash) {
|
|
28
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_RUNTIME, 'Plan runtimeBinding.inputHash does not match runtime.input', { graphId: plan.source?.graphId, jobId });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (binding.variablesHash != null && runtime.variables !== undefined) {
|
|
32
|
+
const actual = hash(runtime.variables);
|
|
33
|
+
if (actual !== binding.variablesHash) {
|
|
34
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_RUNTIME, 'Plan runtimeBinding.variablesHash does not match runtime.variables', { graphId: plan.source?.graphId, jobId });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (binding.jobHash != null && isPlainRecord(runtime.job)) {
|
|
38
|
+
const actual = hash(runtime.job);
|
|
39
|
+
if (actual !== binding.jobHash) {
|
|
40
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_RUNTIME, 'Plan runtimeBinding.jobHash does not match runtime.job', { graphId: plan.source?.graphId, jobId });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const expectations = plan.runtimeExpectations;
|
|
44
|
+
if (expectations?.requiresAgentId) {
|
|
45
|
+
const agentId = runtime.job?.agentId;
|
|
46
|
+
if (typeof agentId !== 'string' || agentId.trim() === '') {
|
|
47
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_RUNTIME, 'Plan runtimeExpectations.requiresAgentId but runtime.job.agentId is missing', { graphId: plan.source?.graphId, jobId });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (expectations?.requiresJobTypeId) {
|
|
51
|
+
const jt = runtime.job?.jobTypeId ?? runtime.job?.jobType;
|
|
52
|
+
if (typeof jt !== 'string' || jt.trim() === '') {
|
|
53
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_RUNTIME, 'Plan runtimeExpectations.requiresJobTypeId but runtime.job.jobTypeId/jobType is missing', { graphId: plan.source?.graphId, jobId });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function preparePlanExecuteEntry(planInput, runtime) {
|
|
58
|
+
const validation = validateExecutablePlan(planInput);
|
|
59
|
+
if (!validation.valid) {
|
|
60
|
+
const summary = validation.errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
61
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `Invalid ExecutableGraphPlan: ${summary}`, { jobId: runtime.jobId });
|
|
62
|
+
}
|
|
63
|
+
const plan = planInput;
|
|
64
|
+
requireEmbeddedNormalizedGraph(plan);
|
|
65
|
+
assertRuntimeBinding(plan, runtime);
|
|
66
|
+
return { plan };
|
|
67
|
+
}
|
|
68
|
+
export function computePlanAudit(plan) {
|
|
69
|
+
return {
|
|
70
|
+
planHash: defaultHashFunction(plan),
|
|
71
|
+
source: plan.source,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExecutionUnitPlanV2, NodeExecutionPlan } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { ExecutionStepOption } from '../types/options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build ai-tasks executionPipeline from plan units (pipelinePhase) merged with invoke contract.
|
|
5
|
+
*/
|
|
6
|
+
export declare function executionPipelineFromNodePlan(nodePlan: NodeExecutionPlan): ExecutionStepOption[] | undefined;
|
|
7
|
+
export declare function externalUtilityUnits(nodePlan: NodeExecutionPlan): {
|
|
8
|
+
pre: ExecutionUnitPlanV2[];
|
|
9
|
+
post: ExecutionUnitPlanV2[];
|
|
10
|
+
};
|
|
11
|
+
export declare function isLocalSkillNodePlan(nodePlan: NodeExecutionPlan): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function isPlainRecord(v) {
|
|
2
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Build ai-tasks executionPipeline from plan units (pipelinePhase) merged with invoke contract.
|
|
6
|
+
*/
|
|
7
|
+
export function executionPipelineFromNodePlan(nodePlan) {
|
|
8
|
+
const pipelineUnits = [...nodePlan.executionUnits]
|
|
9
|
+
.filter((u) => u.unitKind === 'pipelinePhase')
|
|
10
|
+
.sort((a, b) => a.order - b.order);
|
|
11
|
+
if (pipelineUnits.length === 0) {
|
|
12
|
+
const fromContract = nodePlan.invokeContract?.pipeline;
|
|
13
|
+
const synth = isPlainRecord(fromContract?.aiTaskProfile)
|
|
14
|
+
? fromContract.aiTaskProfile.inputSynthesis
|
|
15
|
+
: undefined;
|
|
16
|
+
if (isPlainRecord(synth) && synth.enabled === true) {
|
|
17
|
+
return [{ phase: 'pre', type: 'synthesized-context', config: synth }];
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const steps = [];
|
|
22
|
+
for (const unit of pipelineUnits) {
|
|
23
|
+
if (unit.strategyKey === 'inputSynthesis') {
|
|
24
|
+
const synth = nodePlan.invokeContract?.pipeline?.aiTaskProfile;
|
|
25
|
+
const cfg = isPlainRecord(synth) && isPlainRecord(synth.inputSynthesis)
|
|
26
|
+
? synth.inputSynthesis
|
|
27
|
+
: isPlainRecord(unit.invokeContract)
|
|
28
|
+
? unit.invokeContract
|
|
29
|
+
: {};
|
|
30
|
+
steps.push({ phase: 'pre', type: 'synthesized-context', config: cfg });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
steps.push({
|
|
34
|
+
phase: 'pre',
|
|
35
|
+
type: unit.strategyKey ?? unit.actionKey ?? 'pipeline',
|
|
36
|
+
config: isPlainRecord(unit.invokeContract) ? unit.invokeContract : {},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const hasMain = nodePlan.executionUnits.some((u) => u.unitKind === 'mainSkill');
|
|
40
|
+
if (hasMain && !steps.some((s) => s.phase === 'main')) {
|
|
41
|
+
steps.push({ phase: 'main', type: 'direct' });
|
|
42
|
+
}
|
|
43
|
+
return steps.length ? steps : undefined;
|
|
44
|
+
}
|
|
45
|
+
export function externalUtilityUnits(nodePlan) {
|
|
46
|
+
const units = [...nodePlan.executionUnits].sort((a, b) => a.order - b.order);
|
|
47
|
+
return {
|
|
48
|
+
pre: units.filter((u) => u.unitKind === 'externalPreUtility'),
|
|
49
|
+
post: units.filter((u) => u.unitKind === 'externalPostUtility'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function isLocalSkillNodePlan(nodePlan) {
|
|
53
|
+
return nodePlan.executionClass === 'localTask' || nodePlan.executionUnits.some((u) => u.unitKind === 'localSkill');
|
|
54
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExecutionUnitPlanV2, FinalizerExecutionPlan, NodeExecutionPlan, ResolvedModelSlots } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { GraphAiModelConfig } from '../types/refs.js';
|
|
3
|
+
/** Effective triplet from frozen plan model slots (task node or AI finalizer). */
|
|
4
|
+
export declare function graphAiModelConfigFromPlanSlots(slots: ResolvedModelSlots | null | undefined): GraphAiModelConfig;
|
|
5
|
+
/** Effective triplet for a task node from frozen plan slots. */
|
|
6
|
+
export declare function graphAiModelConfigFromNodePlan(nodePlan: NodeExecutionPlan): GraphAiModelConfig;
|
|
7
|
+
/** Effective triplet for an AI finalizer from frozen plan slots. */
|
|
8
|
+
export declare function graphAiModelConfigFromFinalizerPlan(finalizerPlan: FinalizerExecutionPlan): GraphAiModelConfig;
|
|
9
|
+
/** Per-unit model override when the unit carries its own selection. */
|
|
10
|
+
export declare function modelConfigForUnit(nodePlan: NodeExecutionPlan, unit: ExecutionUnitPlanV2): GraphAiModelConfig;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG } from '../runtime/graphAiModelConfig.js';
|
|
2
|
+
import { unitModelWireString } from './aiModelSelectionWire.js';
|
|
3
|
+
function slotWire(slots, slot, fallback) {
|
|
4
|
+
const entry = slots?.[slot];
|
|
5
|
+
if (entry?.selection) {
|
|
6
|
+
const wire = unitModelWireString({ modelSelection: entry.selection });
|
|
7
|
+
if (wire)
|
|
8
|
+
return wire;
|
|
9
|
+
}
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
/** Effective triplet from frozen plan model slots (task node or AI finalizer). */
|
|
13
|
+
export function graphAiModelConfigFromPlanSlots(slots) {
|
|
14
|
+
const base = DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG;
|
|
15
|
+
return {
|
|
16
|
+
preActionModel: slotWire(slots, 'preActionModel', base.preActionModel),
|
|
17
|
+
skillModel: slotWire(slots, 'skillModel', base.skillModel),
|
|
18
|
+
postActionModel: slotWire(slots, 'postActionModel', base.postActionModel),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Effective triplet for a task node from frozen plan slots. */
|
|
22
|
+
export function graphAiModelConfigFromNodePlan(nodePlan) {
|
|
23
|
+
return graphAiModelConfigFromPlanSlots(nodePlan.modelSlots);
|
|
24
|
+
}
|
|
25
|
+
/** Effective triplet for an AI finalizer from frozen plan slots. */
|
|
26
|
+
export function graphAiModelConfigFromFinalizerPlan(finalizerPlan) {
|
|
27
|
+
return graphAiModelConfigFromPlanSlots(finalizerPlan.modelSlots);
|
|
28
|
+
}
|
|
29
|
+
/** Per-unit model override when the unit carries its own selection. */
|
|
30
|
+
export function modelConfigForUnit(nodePlan, unit) {
|
|
31
|
+
const base = graphAiModelConfigFromNodePlan(nodePlan);
|
|
32
|
+
const slot = unit.modelSlot;
|
|
33
|
+
const unitWire = unitModelWireString({
|
|
34
|
+
modelSelection: unit.modelSelection,
|
|
35
|
+
resolvedInvocationSnapshot: unit.resolvedInvocationSnapshot,
|
|
36
|
+
});
|
|
37
|
+
if (!unitWire || !slot || slot === 'finalizerModel')
|
|
38
|
+
return base;
|
|
39
|
+
if (slot === 'preActionModel')
|
|
40
|
+
return { ...base, preActionModel: unitWire };
|
|
41
|
+
if (slot === 'skillModel')
|
|
42
|
+
return { ...base, skillModel: unitWire };
|
|
43
|
+
if (slot === 'postActionModel')
|
|
44
|
+
return { ...base, postActionModel: unitWire };
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GraphEngineFactory } from "./GraphEngine.js";
|
|
2
2
|
import type { ExecutionStep } from "@exellix/ai-tasks";
|
|
3
|
+
import type { ExecutableGraphPlanV2, GraphExecutionTrace } from "@x12i/graphenix-executable-contracts";
|
|
3
4
|
import type { Graph, GraphModelObject, GraphNode, GraphAiModelConfig, TaskNodeRuntimeObject } from "../types/refs.js";
|
|
4
5
|
import type { RunLogEntry } from "../types/runLog.js";
|
|
5
6
|
import type { RunTaskDiagnostics } from "../types/options.js";
|
|
@@ -62,7 +63,7 @@ export interface GraphLoader {
|
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
65
|
* Dynamic state and run-scoped options for one graph execution.
|
|
65
|
-
* Static
|
|
66
|
+
* Static execution contract belongs on {@link GraphExecutionRequest.plan}; this object
|
|
66
67
|
* is the per-run state that may be populated and mutated during execution.
|
|
67
68
|
*/
|
|
68
69
|
export interface GraphRuntimeObject extends HostExecuteGraphRunOptions {
|
|
@@ -104,7 +105,7 @@ export interface GraphRuntimeObject extends HostExecuteGraphRunOptions {
|
|
|
104
105
|
}
|
|
105
106
|
/** Required public graph execution request shape. */
|
|
106
107
|
export interface GraphExecutionRequest {
|
|
107
|
-
|
|
108
|
+
plan: ExecutableGraphPlanV2;
|
|
108
109
|
runtime: GraphRuntimeObject;
|
|
109
110
|
}
|
|
110
111
|
/** Canonical public execution request shape for `runtime.executeGraph(...)`. */
|
|
@@ -144,13 +145,15 @@ export interface ExecuteGraphResult {
|
|
|
144
145
|
nodes: ExecuteGraphDebugNodeEntry[];
|
|
145
146
|
};
|
|
146
147
|
/**
|
|
147
|
-
* Audit fingerprint of the supplied
|
|
148
|
+
* Audit fingerprint of the supplied executable plan.
|
|
148
149
|
* Hosts (e.g. execution matrix) may persist this with catalog `graphVersion` for replay forensics.
|
|
149
150
|
*/
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
planAudit?: {
|
|
152
|
+
planHash: string;
|
|
153
|
+
source: ExecutableGraphPlanV2['source'];
|
|
153
154
|
};
|
|
155
|
+
/** Append-only execution trace bound to the plan (when trace-format is used). */
|
|
156
|
+
trace?: GraphExecutionTrace;
|
|
154
157
|
}
|
|
155
158
|
export type GraphKnowledgeResolverContext = {
|
|
156
159
|
model: GraphModelObject;
|