@exellix/graph-engine 6.0.2 → 7.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Breaking
6
+
7
+ - **Model profile aliases only on graph JSON:** `modelConfig` / `taskConfiguration.modelConfig` values must be profile alias names (`strong`, `weak`, `default`, …), not provider model ids. `assertCanonicalGraphDocument` throws `NON_CANONICAL_MODEL_CONFIG` for provider-like strings.
8
+ - **Required `runtime.aliasConfig`:** Every `executeGraph` must supply a non-empty alias → concrete model map covering all aliases used by the graph (including `default`). Missing map or keys throws `MODEL_ALIAS_CONFIG_REQUIRED` / `MODEL_ALIAS_UNRESOLVED`.
9
+ - **No alias pass-through:** Resolved concrete models are sent on `RunTaskRequest.modelConfig` only after strict alias resolution.
10
+ - **Exports:** `collectModelAliasesUsedInGraph`, `assertRuntimeAliasConfigCoversGraph`, `DEFAULT_MODEL_ALIAS`, testkit `createTestExellixGraphRuntime` / `buildTestAliasConfig`.
11
+
3
12
  ## 5.16.0
4
13
 
5
14
  ### Breaking
package/README.md CHANGED
@@ -33,6 +33,7 @@ A minimal, focused SDK for executing graphs in the exellix ecosystem.
33
33
  | Layer 01 / 08 graph entry & response contracts | [`.docs/graph-io-visibility.md`](.docs/graph-io-visibility.md) |
34
34
  | Graph entry `dataFilters` v1 / public evaluator | [`.docs/data-filters-evaluation.md`](.docs/data-filters-evaluation.md) |
35
35
  | Task-node `conditions` + conditional `modelConfig.cases` (runx) | [`.docs/task-node-conditions-evaluation.md`](.docs/task-node-conditions-evaluation.md) |
36
+ | **Model profile aliases** (7.x: graph = profile names, runtime = concrete models) | [`BREAKING-CHANGES.md`](BREAKING-CHANGES.md) §7.0.0, [`.docs/ai-tasks-model-profile-aliases-7x.md`](.docs/ai-tasks-model-profile-aliases-7x.md) (ai-tasks), [`.docs/fr-model-alias-descriptors.md`](.docs/fr-model-alias-descriptors.md) (upstream FRs) |
36
37
  | Platform vs implementation (no domain operators in schema) | [`.docs/platform-generic-vs-implementation.md`](.docs/platform-generic-vs-implementation.md) |
37
38
  | Bundled graph examples & bundle README | [`graphs/README.md`](graphs/README.md) |
38
39
 
@@ -112,10 +113,14 @@ const result = await runtime.executeGraph({
112
113
  jobId: 'job-123',
113
114
  job: { agentId: 'agent-1', input: {} },
114
115
  input: { question: 'Analyze this record' },
115
- modelConfig: { xynthesisModel: 'xynth-default', skillModel: 'skill-default' },
116
+ // 7.x: graph modelConfig values are profile aliases; concrete models bind here (required).
117
+ modelConfig: {
118
+ cases: [{ modelConfig: { xynthesisModel: 'weak', skillModel: 'strong' } }],
119
+ },
116
120
  aliasConfig: {
117
- 'xynth-default': 'openrouter/xynthesis-default',
118
- 'skill-default': 'openrouter/skill-default',
121
+ strong: 'anthropic/claude-sonnet-4',
122
+ weak: 'google/gemini-2.5-flash',
123
+ default: 'google/gemini-2.5-flash',
119
124
  },
120
125
  },
121
126
  });
@@ -164,10 +169,14 @@ interface GraphRuntimeObject {
164
169
  taskMemory?: any;
165
170
  executionMemory?: any;
166
171
  variables?: Record<string, any>;
167
- modelConfig?: { xynthesisModel: string; skillModel: string };
168
- aliasConfig?: Record<string, string>;
172
+ /** Run-level model profile override (`cases`); values are alias names, not provider ids. */
173
+ modelConfig?: { cases: Array<{ when?: unknown; modelConfig: { xynthesisModel: string; skillModel: string } }> };
174
+ /** Required (7.x): profile alias → concrete provider model id for this execution. */
175
+ aliasConfig: Record<string, string>;
169
176
  nodes?: Record<string, {
177
+ /** Per-node profile override (alias names). */
170
178
  modelConfig?: { xynthesisModel: string; skillModel: string };
179
+ /** Per-node alias bindings (overlay `aliasConfig`). */
171
180
  aliasConfig?: Record<string, string>;
172
181
  }>;
173
182
  mode?: 'forward' | 'backward' | 'hybrid';
@@ -266,7 +275,11 @@ Graph-engine builds a canonical `RunTaskRequest` for every outbound task call it
266
275
  - **Canonical task payload:** **`input`** object only (merged execution slice + **materialized** `node.inputs`). Root-level **`question`**, **`raw`**, **`jobInput`**, duplicate **`inputs`**, and legacy **`executionType`** are **not** sent on the request object.
267
276
  - **Graph telemetry:** `graphId`, **`nodeId`**, **`coreSkillId`** (node id), `masterSkillId`, `masterSkillActivityId`, `jobId`, `taskId`, optional `identity`.
268
277
 
269
- To build `RunTaskRequest` without fallback defaults, the execution request must provide both sides of the contract: `model.id`, `node.id`, `node.skillKey`, explicit `node.taskConfiguration.taskTypeId` (even when it matches `skillKey`), `node.taskConfiguration.executionStrategies` (use `[]` for plain MAIN), `runtime.jobId`, `runtime.job.agentId`, `runtime.job.jobTypeId` or `runtime.job.jobType`, active input/memory, and any model/LLM/diagnostic options needed by the task. Model selection uses the exact shape `{ xynthesisModel, skillModel }` from `runtime.nodes[nodeId].modelConfig`, `node.taskConfiguration.modelConfig`, `runtime.modelConfig`, or `model.modelConfig`; aliases are runtime-only and resolve through `runtime.aliasConfig` plus `runtime.nodes[nodeId].aliasConfig`. The resolved model config is forwarded to MAIN, engine PRE/POST utility calls, and `synthesize` finalizer calls. Graph-engine still derives correlation fields such as `graphId`, `nodeId`, `coreSkillId`, `masterSkillId`, `taskId`, and `masterSkillActivityId` from those authored values.
278
+ To build `RunTaskRequest` without fallback defaults, the execution request must provide both sides of the contract: `model.id`, `node.id`, `node.skillKey`, explicit `node.taskConfiguration.taskTypeId` (even when it matches `skillKey`), `node.taskConfiguration.executionStrategies` (use `[]` for plain MAIN), `runtime.jobId`, `runtime.job.agentId`, `runtime.job.jobTypeId` or `runtime.job.jobType`, active input/memory, and any model/LLM/diagnostic options needed by the task.
279
+
280
+ **Model profiles (7.x):** Graph and node `modelConfig` carry **profile alias names** only (`strong`, `weak`, `default`, …) — never provider model ids in graph JSON. **`runtime.aliasConfig` is required** and maps every alias used by the run (including `default` when fallback applies) to concrete provider models. Selection order: `runtime.nodes[nodeId].modelConfig` → `node.taskConfiguration.modelConfig` → `runtime.modelConfig` → `model.modelConfig` → implicit `{ default, default }`; then strict resolution through `runtime.aliasConfig` plus `runtime.nodes[nodeId].aliasConfig`. The **resolved** `{ xynthesisModel, skillModel }` is forwarded to MAIN, engine PRE/POST utility calls, and `synthesize` finalizer calls. Snapshot `aliasConfig` on execution records for reproducibility. See [`BREAKING-CHANGES.md`](BREAKING-CHANGES.md) §7.0.0.
281
+
282
+ Graph-engine still derives correlation fields such as `graphId`, `nodeId`, `coreSkillId`, `masterSkillId`, `taskId`, and `masterSkillActivityId` from those authored values.
270
283
 
271
284
  #### Mandatory `executionStrategies` (breaking vs pre–v7 authoring)
272
285
 
@@ -27,5 +27,11 @@ export declare enum ExellixGraphErrorCode {
27
27
  */
28
28
  GRAPH_ENTRY_DATA_FILTERS_REJECTED = "GRAPH_ENTRY_DATA_FILTERS_REJECTED",
29
29
  /** MAIN readiness guard rejected empty execution input or synthesis context before runTask. */
30
- MAIN_READINESS_FAILED = "MAIN_READINESS_FAILED"
30
+ MAIN_READINESS_FAILED = "MAIN_READINESS_FAILED",
31
+ /** `executeGraph` called without a non-empty `runtime.aliasConfig`. */
32
+ MODEL_ALIAS_CONFIG_REQUIRED = "MODEL_ALIAS_CONFIG_REQUIRED",
33
+ /** Selected model alias has no concrete model in the merged runtime alias map. */
34
+ MODEL_ALIAS_UNRESOLVED = "MODEL_ALIAS_UNRESOLVED",
35
+ /** Graph JSON `modelConfig` contains a provider model id instead of an alias name. */
36
+ NON_CANONICAL_MODEL_CONFIG = "NON_CANONICAL_MODEL_CONFIG"
31
37
  }
@@ -29,4 +29,10 @@ export var ExellixGraphErrorCode;
29
29
  ExellixGraphErrorCode["GRAPH_ENTRY_DATA_FILTERS_REJECTED"] = "GRAPH_ENTRY_DATA_FILTERS_REJECTED";
30
30
  /** MAIN readiness guard rejected empty execution input or synthesis context before runTask. */
31
31
  ExellixGraphErrorCode["MAIN_READINESS_FAILED"] = "MAIN_READINESS_FAILED";
32
+ /** `executeGraph` called without a non-empty `runtime.aliasConfig`. */
33
+ ExellixGraphErrorCode["MODEL_ALIAS_CONFIG_REQUIRED"] = "MODEL_ALIAS_CONFIG_REQUIRED";
34
+ /** Selected model alias has no concrete model in the merged runtime alias map. */
35
+ ExellixGraphErrorCode["MODEL_ALIAS_UNRESOLVED"] = "MODEL_ALIAS_UNRESOLVED";
36
+ /** Graph JSON `modelConfig` contains a provider model id instead of an alias name. */
37
+ ExellixGraphErrorCode["NON_CANONICAL_MODEL_CONFIG"] = "NON_CANONICAL_MODEL_CONFIG";
32
38
  })(ExellixGraphErrorCode || (ExellixGraphErrorCode = {}));
@@ -38,7 +38,8 @@ export type { PathSegment } from './runtime/pathExpr.js';
38
38
  export { evaluateStructuredDataFilters, evaluateDataFilterPredicate, getStructuredDataFilterPathViolations, isStructuredDataFiltersV1, } from './runtime/dataFiltersEvaluation.js';
39
39
  export { evaluateTaskNodeConditions, evaluateConditionWhen, applyConditionNegate, } from './runtime/taskNodeConditionsEvaluation.js';
40
40
  export type { TaskNodeConditionsEvalDeps, TaskNodeConditionsEvalResult, TaskNodeConditionEvalContext, } from './runtime/taskNodeConditionsEvaluation.js';
41
- export { resolveModelConfigForNode, resolveGraphAiModelConfigAliases, } from './runtime/resolveModelConfigForNode.js';
41
+ export { resolveModelConfigForNode, resolveGraphAiModelConfigAliases, mergeAliasConfigs, } from './runtime/resolveModelConfigForNode.js';
42
+ export { DEFAULT_MODEL_ALIAS, DEFAULT_GRAPH_AI_MODEL_ALIAS_CONFIG, isModelAliasString, looksLikeConcreteModelId, assertGraphModelAliasString, collectModelAliasesUsedInGraph, assertRuntimeAliasConfigPresent, assertRuntimeAliasConfigCoversGraph, } from './runtime/modelAliasContract.js';
42
43
  export { isGraphAiModelConfig, isModelConfigSelection, isEmptyConditionWhen, conditionWhenSignature, countDefaultModelConfigCases, } from './runtime/modelConfigSelection.js';
43
44
  export { GRAPH_ENGINE_MEMORY_PATH_ROOTS, splitGraphEngineMemoryPath, isAllowedGraphEngineMemoryPath, graphEngineMemoryPathValidationMessage, } from './runtime/graphEngineMemoryPaths.js';
44
45
  export { buildRunTaskMainInput, extractCallerInputsBag, buildGraphEngineMemoryResolutionRoot, buildGraphEngineMemoryResolutionRootFromWorkingMemory, resolveGraphEngineMemoryPathValue, } from './runtime/resolveGraphEngineMemoryPaths.js';
package/dist/src/index.js CHANGED
@@ -28,7 +28,8 @@ export { mergeMemory } from './runtime/memory.js';
28
28
  export { selectByPath, writeByPath, parsePath } from './runtime/pathExpr.js';
29
29
  export { evaluateStructuredDataFilters, evaluateDataFilterPredicate, getStructuredDataFilterPathViolations, isStructuredDataFiltersV1, } from './runtime/dataFiltersEvaluation.js';
30
30
  export { evaluateTaskNodeConditions, evaluateConditionWhen, applyConditionNegate, } from './runtime/taskNodeConditionsEvaluation.js';
31
- export { resolveModelConfigForNode, resolveGraphAiModelConfigAliases, } from './runtime/resolveModelConfigForNode.js';
31
+ export { resolveModelConfigForNode, resolveGraphAiModelConfigAliases, mergeAliasConfigs, } from './runtime/resolveModelConfigForNode.js';
32
+ export { DEFAULT_MODEL_ALIAS, DEFAULT_GRAPH_AI_MODEL_ALIAS_CONFIG, isModelAliasString, looksLikeConcreteModelId, assertGraphModelAliasString, collectModelAliasesUsedInGraph, assertRuntimeAliasConfigPresent, assertRuntimeAliasConfigCoversGraph, } from './runtime/modelAliasContract.js';
32
33
  export { isGraphAiModelConfig, isModelConfigSelection, isEmptyConditionWhen, conditionWhenSignature, countDefaultModelConfigCases, } from './runtime/modelConfigSelection.js';
33
34
  export { GRAPH_ENGINE_MEMORY_PATH_ROOTS, splitGraphEngineMemoryPath, isAllowedGraphEngineMemoryPath, graphEngineMemoryPathValidationMessage, } from './runtime/graphEngineMemoryPaths.js';
34
35
  export { buildRunTaskMainInput, extractCallerInputsBag, buildGraphEngineMemoryResolutionRoot, buildGraphEngineMemoryResolutionRootFromWorkingMemory, resolveGraphEngineMemoryPathValue, } from './runtime/resolveGraphEngineMemoryPaths.js';
@@ -34,6 +34,7 @@ import { evaluateGraphPredicate } from "./predicates.js";
34
34
  import { evaluateStructuredDataFilters } from "./dataFiltersEvaluation.js";
35
35
  import { evaluateTaskNodeConditions } from "./taskNodeConditionsEvaluation.js";
36
36
  import { resolveModelConfigForNode } from "./resolveModelConfigForNode.js";
37
+ import { assertRuntimeAliasConfigCoversGraph } from "./modelAliasContract.js";
37
38
  import { createRunx } from "@x12i/runx";
38
39
  import { selectByPath } from "./pathExpr.js";
39
40
  import { createGraphStartEvent, createGraphCompleteEvent, createGraphFailEvent, createNodeStartEvent, createNodeCompleteEvent, createNodeFailEvent, } from "./events.js";
@@ -186,19 +187,6 @@ function readRuntimeNodeConfig(nodes, nodeId) {
186
187
  const candidate = nodes[nodeId];
187
188
  return isPlainRecord(candidate) ? candidate : undefined;
188
189
  }
189
- function mergeAliasConfigs(rootAliases, nodeAliases) {
190
- const aliases = {};
191
- for (const source of [rootAliases, nodeAliases]) {
192
- if (!isPlainRecord(source))
193
- continue;
194
- for (const [alias, model] of Object.entries(source)) {
195
- if (alias.trim() !== "" && typeof model === "string" && model.trim() !== "") {
196
- aliases[alias] = model;
197
- }
198
- }
199
- }
200
- return Object.keys(aliases).length > 0 ? aliases : undefined;
201
- }
202
190
  function taskNodeNeedsRunxClient(conditions) {
203
191
  return conditions?.jsConditionFunction != null || conditions?.aiCondition != null;
204
192
  }
@@ -1164,6 +1152,7 @@ export function createExellixGraphRuntime(opts) {
1164
1152
  }
1165
1153
  const resolvedGraphId = String(resolvedGraphIdRaw);
1166
1154
  assertCanonicalGraphDocument(graph, { jobId, graphId: resolvedGraphId });
1155
+ assertRuntimeAliasConfigCoversGraph(graph, merged.aliasConfig, { modelConfig: merged.modelConfig, nodes: merged.nodes }, { jobId, graphId: resolvedGraphId });
1167
1156
  let runxClient = opts.runx;
1168
1157
  if (graphNeedsRunxClient(graph, merged.modelConfig)) {
1169
1158
  if (!runxClient) {
@@ -1590,6 +1579,9 @@ export function createExellixGraphRuntime(opts) {
1590
1579
  }),
1591
1580
  executionInput: dataFiltersRecord,
1592
1581
  runx: runxClient,
1582
+ graphId: resolvedGraphId,
1583
+ nodeId: typeof node.id === "string" ? node.id : String(node.id),
1584
+ jobId,
1593
1585
  });
1594
1586
  const r = await executeNode({
1595
1587
  graphId: resolvedGraphId,
@@ -0,0 +1,46 @@
1
+ import type { Graph, GraphAiModelConfig, GraphModelAliasConfig, ModelConfigSelection } from '../types/refs.js';
2
+ /** Fallback alias pair when no modelConfig tier resolves. */
3
+ export declare const DEFAULT_MODEL_ALIAS = "default";
4
+ export declare const DEFAULT_GRAPH_AI_MODEL_ALIAS_CONFIG: GraphAiModelConfig;
5
+ /**
6
+ * True when a string is a valid graph-authored model alias (not a provider model id).
7
+ */
8
+ export declare function isModelAliasString(value: unknown): value is string;
9
+ /** True when a value looks like a concrete provider model id (forbidden in graph JSON). */
10
+ export declare function looksLikeConcreteModelId(value: string): boolean;
11
+ export declare function assertGraphModelAliasString(value: unknown, path: string, context?: {
12
+ jobId?: string;
13
+ graphId?: string;
14
+ nodeId?: string;
15
+ }): asserts value is string;
16
+ export declare function mergeAliasConfigs(root?: GraphModelAliasConfig, node?: GraphModelAliasConfig): GraphModelAliasConfig;
17
+ /**
18
+ * Collects every alias name referenced by graph + optional runtime overrides (before resolution).
19
+ * Always includes `default` because resolution may fall back to it.
20
+ */
21
+ export declare function collectModelAliasesUsedInGraph(graph: Pick<Graph, 'modelConfig' | 'nodes'>, runtime?: {
22
+ modelConfig?: ModelConfigSelection;
23
+ nodes?: Record<string, {
24
+ modelConfig?: GraphAiModelConfig;
25
+ aliasConfig?: GraphModelAliasConfig;
26
+ }>;
27
+ }): Set<string>;
28
+ export declare function assertRuntimeAliasConfigPresent(aliasConfig: GraphModelAliasConfig | undefined, context?: {
29
+ jobId?: string;
30
+ graphId?: string;
31
+ }): asserts aliasConfig is GraphModelAliasConfig;
32
+ /**
33
+ * Ensures merged runtime alias map covers every alias used by the graph (+ runtime overrides).
34
+ */
35
+ export declare function assertRuntimeAliasConfigCoversGraph(graph: Pick<Graph, 'id' | 'modelConfig' | 'nodes'>, aliasConfig: GraphModelAliasConfig | undefined, runtime?: Parameters<typeof collectModelAliasesUsedInGraph>[1], context?: {
36
+ jobId?: string;
37
+ graphId?: string;
38
+ }): void;
39
+ /**
40
+ * Resolves alias-only {@link GraphAiModelConfig} to concrete provider model ids via runtime map.
41
+ */
42
+ export declare function resolveGraphAiModelConfigAliases(modelConfig: GraphAiModelConfig | undefined, aliasConfig: GraphModelAliasConfig | undefined, context?: {
43
+ jobId?: string;
44
+ graphId?: string;
45
+ nodeId?: string;
46
+ }): GraphAiModelConfig | undefined;
@@ -0,0 +1,129 @@
1
+ import { ExellixGraphError } from '../errors/ExellixGraphError.js';
2
+ import { ExellixGraphErrorCode } from '../errors/exellixGraphErrorCodes.js';
3
+ import { isGraphAiModelConfig, isModelConfigSelection } from './modelConfigSelection.js';
4
+ /** Fallback alias pair when no modelConfig tier resolves. */
5
+ export const DEFAULT_MODEL_ALIAS = 'default';
6
+ export const DEFAULT_GRAPH_AI_MODEL_ALIAS_CONFIG = {
7
+ xynthesisModel: DEFAULT_MODEL_ALIAS,
8
+ skillModel: DEFAULT_MODEL_ALIAS,
9
+ };
10
+ const CONCRETE_MODEL_PREFIXES = ['openrouter/', 'anthropic/', 'openai/', 'google/', 'x-ai/', 'meta-llama/'];
11
+ /**
12
+ * True when a string is a valid graph-authored model alias (not a provider model id).
13
+ */
14
+ export function isModelAliasString(value) {
15
+ if (typeof value !== 'string')
16
+ return false;
17
+ const trimmed = value.trim();
18
+ if (trimmed === '')
19
+ return false;
20
+ return !looksLikeConcreteModelId(trimmed);
21
+ }
22
+ /** True when a value looks like a concrete provider model id (forbidden in graph JSON). */
23
+ export function looksLikeConcreteModelId(value) {
24
+ const trimmed = value.trim();
25
+ if (trimmed.includes('/'))
26
+ return true;
27
+ const lower = trimmed.toLowerCase();
28
+ return CONCRETE_MODEL_PREFIXES.some((p) => lower.startsWith(p));
29
+ }
30
+ export function assertGraphModelAliasString(value, path, context) {
31
+ if (!isModelAliasString(value)) {
32
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_MODEL_CONFIG, `${path} must be a model alias name (non-empty string without "/" or provider prefixes); got ${JSON.stringify(value)}`, context);
33
+ }
34
+ }
35
+ export function mergeAliasConfigs(root, node) {
36
+ const aliases = {};
37
+ for (const source of [root, node]) {
38
+ if (source == null || typeof source !== 'object' || Array.isArray(source))
39
+ continue;
40
+ for (const [alias, model] of Object.entries(source)) {
41
+ if (alias.trim() !== '' && typeof model === 'string' && model.trim() !== '') {
42
+ aliases[alias] = model;
43
+ }
44
+ }
45
+ }
46
+ return aliases;
47
+ }
48
+ function collectAliasesFromGraphAiModelConfig(config, out) {
49
+ if (!isGraphAiModelConfig(config))
50
+ return;
51
+ out.add(config.xynthesisModel.trim());
52
+ out.add(config.skillModel.trim());
53
+ }
54
+ function collectAliasesFromModelConfigSelection(selection, out) {
55
+ if (!isModelConfigSelection(selection))
56
+ return;
57
+ for (const c of selection.cases) {
58
+ collectAliasesFromGraphAiModelConfig(c.modelConfig, out);
59
+ }
60
+ }
61
+ /**
62
+ * Collects every alias name referenced by graph + optional runtime overrides (before resolution).
63
+ * Always includes `default` because resolution may fall back to it.
64
+ */
65
+ export function collectModelAliasesUsedInGraph(graph, runtime) {
66
+ const out = new Set([DEFAULT_MODEL_ALIAS]);
67
+ collectAliasesFromModelConfigSelection(graph.modelConfig, out);
68
+ for (const node of graph.nodes ?? []) {
69
+ const tn = node;
70
+ if (tn.type === 'finalizer')
71
+ continue;
72
+ collectAliasesFromModelConfigSelection(tn.taskConfiguration?.modelConfig, out);
73
+ }
74
+ if (runtime != null) {
75
+ collectAliasesFromModelConfigSelection(runtime.modelConfig, out);
76
+ if (runtime.nodes != null && typeof runtime.nodes === 'object') {
77
+ for (const nodeRuntime of Object.values(runtime.nodes)) {
78
+ if (nodeRuntime == null || typeof nodeRuntime !== 'object')
79
+ continue;
80
+ collectAliasesFromGraphAiModelConfig(nodeRuntime.modelConfig, out);
81
+ }
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+ export function assertRuntimeAliasConfigPresent(aliasConfig, context) {
87
+ if (aliasConfig == null ||
88
+ typeof aliasConfig !== 'object' ||
89
+ Array.isArray(aliasConfig) ||
90
+ Object.keys(aliasConfig).length === 0) {
91
+ throw new ExellixGraphError(ExellixGraphErrorCode.MODEL_ALIAS_CONFIG_REQUIRED, 'runtime.aliasConfig is required and must be a non-empty map of alias name → concrete model id', context);
92
+ }
93
+ }
94
+ /**
95
+ * Ensures merged runtime alias map covers every alias used by the graph (+ runtime overrides).
96
+ */
97
+ export function assertRuntimeAliasConfigCoversGraph(graph, aliasConfig, runtime, context) {
98
+ assertRuntimeAliasConfigPresent(aliasConfig, context);
99
+ const needed = collectModelAliasesUsedInGraph(graph, runtime);
100
+ const missing = [];
101
+ for (const alias of needed) {
102
+ const concrete = aliasConfig[alias];
103
+ if (typeof concrete !== 'string' || concrete.trim() === '') {
104
+ missing.push(alias);
105
+ }
106
+ }
107
+ if (missing.length > 0) {
108
+ throw new ExellixGraphError(ExellixGraphErrorCode.MODEL_ALIAS_UNRESOLVED, `runtime.aliasConfig is missing concrete models for alias(es): ${missing.join(', ')}`, { ...context, graphId: context?.graphId ?? graph.id, missingAliases: missing });
109
+ }
110
+ }
111
+ function resolveModelAliasStrict(alias, aliasConfig, context) {
112
+ const concrete = aliasConfig[alias];
113
+ if (typeof concrete !== 'string' || concrete.trim() === '') {
114
+ throw new ExellixGraphError(ExellixGraphErrorCode.MODEL_ALIAS_UNRESOLVED, `Unresolved model alias "${alias}" — add it to runtime.aliasConfig (and runtime.nodes[nodeId].aliasConfig when scoped per node)`, { ...context, alias });
115
+ }
116
+ return concrete.trim();
117
+ }
118
+ /**
119
+ * Resolves alias-only {@link GraphAiModelConfig} to concrete provider model ids via runtime map.
120
+ */
121
+ export function resolveGraphAiModelConfigAliases(modelConfig, aliasConfig, context) {
122
+ if (!isGraphAiModelConfig(modelConfig))
123
+ return undefined;
124
+ assertRuntimeAliasConfigPresent(aliasConfig, context);
125
+ return {
126
+ xynthesisModel: resolveModelAliasStrict(modelConfig.xynthesisModel, aliasConfig, context),
127
+ skillModel: resolveModelAliasStrict(modelConfig.skillModel, aliasConfig, context),
128
+ };
129
+ }
@@ -11,10 +11,14 @@ export type ResolveModelConfigForNodeArgs = {
11
11
  conditionCtx: TaskNodeConditionEvalContext;
12
12
  executionInput: Record<string, unknown>;
13
13
  runx?: RunxClient;
14
+ graphId?: string;
15
+ nodeId?: string;
16
+ jobId?: string;
14
17
  };
15
- export declare function resolveGraphAiModelConfigAliases(modelConfig: GraphAiModelConfig | undefined, aliasConfig: GraphModelAliasConfig | undefined): GraphAiModelConfig | undefined;
16
18
  /**
17
- * Resolves effective model config for a task node (precedence + conditional cases + aliases).
19
+ * Resolves effective model config for a task node (precedence + conditional cases + strict alias resolution).
20
+ * Returns concrete provider model ids for `RunTaskRequest.modelConfig`.
18
21
  */
19
- export declare function resolveModelConfigForNode(args: ResolveModelConfigForNodeArgs): Promise<GraphAiModelConfig | undefined>;
22
+ export declare function resolveModelConfigForNode(args: ResolveModelConfigForNodeArgs): Promise<GraphAiModelConfig>;
20
23
  export { conditionWhenSignature, isEmptyConditionWhen };
24
+ export { resolveGraphAiModelConfigAliases, mergeAliasConfigs } from './modelAliasContract.js';
@@ -1,21 +1,6 @@
1
1
  import { conditionWhenSignature, isEmptyConditionWhen, isGraphAiModelConfig, isModelConfigSelection, } from './modelConfigSelection.js';
2
2
  import { evaluateConditionWhen } from './taskNodeConditionsEvaluation.js';
3
- function resolveModelAlias(modelOrAlias, aliases) {
4
- return aliases?.[modelOrAlias] ?? modelOrAlias;
5
- }
6
- function mergeAliasConfigs(root, node) {
7
- if (root == null && node == null)
8
- return undefined;
9
- return { ...(root ?? {}), ...(node ?? {}) };
10
- }
11
- export function resolveGraphAiModelConfigAliases(modelConfig, aliasConfig) {
12
- if (!isGraphAiModelConfig(modelConfig))
13
- return undefined;
14
- return {
15
- xynthesisModel: resolveModelAlias(modelConfig.xynthesisModel, aliasConfig),
16
- skillModel: resolveModelAlias(modelConfig.skillModel, aliasConfig),
17
- };
18
- }
3
+ import { DEFAULT_GRAPH_AI_MODEL_ALIAS_CONFIG, mergeAliasConfigs, resolveGraphAiModelConfigAliases, } from './modelAliasContract.js';
19
4
  async function selectFromCases(selection, ctx, executionInput, runx) {
20
5
  const cases = selection.cases;
21
6
  if (!Array.isArray(cases) || cases.length === 0)
@@ -49,21 +34,24 @@ async function resolveTier(value, ctx, executionInput, runx) {
49
34
  return undefined;
50
35
  }
51
36
  /**
52
- * Resolves effective model config for a task node (precedence + conditional cases + aliases).
37
+ * Resolves effective model config for a task node (precedence + conditional cases + strict alias resolution).
38
+ * Returns concrete provider model ids for `RunTaskRequest.modelConfig`.
53
39
  */
54
40
  export async function resolveModelConfigForNode(args) {
55
41
  const aliasConfig = mergeAliasConfigs(args.runtimeAliasConfig, args.runtimeNodeConfig?.aliasConfig);
42
+ const resolveCtx = { graphId: args.graphId, nodeId: args.nodeId, jobId: args.jobId };
56
43
  const staticHost = args.runtimeNodeConfig?.modelConfig;
57
44
  if (isGraphAiModelConfig(staticHost)) {
58
- return resolveGraphAiModelConfigAliases(staticHost, aliasConfig);
45
+ return resolveGraphAiModelConfigAliases(staticHost, aliasConfig, resolveCtx);
59
46
  }
60
47
  const tiers = [args.nodeModelConfig, args.runtimeModelConfig, args.graphModelConfig];
61
48
  for (const tier of tiers) {
62
49
  const selected = await resolveTier(tier, args.conditionCtx, args.executionInput, args.runx);
63
50
  if (selected != null) {
64
- return resolveGraphAiModelConfigAliases(selected, aliasConfig);
51
+ return resolveGraphAiModelConfigAliases(selected, aliasConfig, resolveCtx);
65
52
  }
66
53
  }
67
- return undefined;
54
+ return resolveGraphAiModelConfigAliases(DEFAULT_GRAPH_AI_MODEL_ALIAS_CONFIG, aliasConfig, resolveCtx);
68
55
  }
69
56
  export { conditionWhenSignature, isEmptyConditionWhen };
57
+ export { resolveGraphAiModelConfigAliases, mergeAliasConfigs } from './modelAliasContract.js';
@@ -5,7 +5,7 @@
5
5
  import type { Catalox } from '@x12i/catalox';
6
6
  import type { RunTaskRequest } from '@exellix/ai-tasks';
7
7
  import { type AnalyzeRunTaskRequestOptions, type AnalyzeRunTaskRequestResult, type RunTaskInvokeValidationResult, type RunTaskValidationResult, type ValidateRunTaskInvokeParams } from '@exellix/ai-tasks';
8
- import type { GraphAiModelConfig, Job, TaskNode } from '../types/refs.js';
8
+ import type { GraphAiModelConfig, GraphModelAliasConfig, Job, TaskNode } from '../types/refs.js';
9
9
  import type { ExecutionStepOption, RunTaskDiagnostics, SkillKeyResolutionOptions } from '../types/options.js';
10
10
  export type TaskNodeRunTaskPreflightSkipReason = 'local_skill' | 'finalizer';
11
11
  export type BuildTaskNodeRunTaskRequestResult = {
@@ -30,7 +30,10 @@ export type BuildTaskNodeRunTaskRequestArgs = {
30
30
  graphRunTaskId?: string;
31
31
  /** Host job id on `job.jobId` / `job.id`; defaults from `job` when omitted. */
32
32
  runTaskJobId?: string;
33
+ /** Alias-only selection; resolved via `aliasConfig` before building the request. */
33
34
  modelConfig?: GraphAiModelConfig;
35
+ /** Required when resolving `modelConfig` aliases for preflight (mirrors `runtime.aliasConfig`). */
36
+ aliasConfig?: GraphModelAliasConfig;
34
37
  llmCall?: Record<string, unknown>;
35
38
  /** Runtime default merged with per-node `taskConfiguration.llmCall`. */
36
39
  runtimeLlmCall?: Record<string, unknown>;
@@ -1,4 +1,5 @@
1
1
  import { analyzeRunTaskRequest, validateRunTaskConfig, validateRunTaskInvoke, } from '@exellix/ai-tasks';
2
+ import { resolveGraphAiModelConfigAliases } from './modelAliasContract.js';
2
3
  import { resolveTaskKey } from './resolveTaskKey.js';
3
4
  import { isLocalSkillKey } from './localSkills/index.js';
4
5
  import { buildAiTasksRunTaskRequest, extractRunTaskStrategyOverrides, } from './buildAiTasksRunTaskRequest.js';
@@ -121,6 +122,13 @@ export function buildTaskNodeRunTaskRequest(args) {
121
122
  });
122
123
  const metaStrats = node.taskConfiguration?.executionStrategies;
123
124
  const ov = node.taskConfiguration?.aiTasksOutputValidation;
125
+ const resolvedModelConfig = args.modelConfig != null && args.aliasConfig != null
126
+ ? resolveGraphAiModelConfigAliases(args.modelConfig, args.aliasConfig, {
127
+ graphId: args.graphId,
128
+ nodeId: String(node.id),
129
+ jobId: lifecycleJobId,
130
+ })
131
+ : args.modelConfig;
124
132
  const request = buildAiTasksRunTaskRequest({
125
133
  skillKey,
126
134
  job: args.job,
@@ -145,7 +153,7 @@ export function buildTaskNodeRunTaskRequest(args) {
145
153
  ? ov
146
154
  : undefined,
147
155
  diagnostics: args.runTaskDiagnostics,
148
- modelConfig: args.modelConfig ?? undefined,
156
+ modelConfig: resolvedModelConfig ?? undefined,
149
157
  llmCall: effectiveLlmCall,
150
158
  identity: buildRunTaskIdentityEnvelope({
151
159
  base: args.runTaskIdentity,
@@ -4,6 +4,7 @@ import { ExellixGraphErrorCode } from '../errors/exellixGraphErrorCodes.js';
4
4
  import { EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY } from '../inspection/types.js';
5
5
  import { getStructuredDataFilterPathViolations } from './dataFiltersEvaluation.js';
6
6
  import { conditionWhenSignature, countDefaultModelConfigCases, isEmptyConditionWhen, isGraphAiModelConfig, isModelConfigSelection, } from './modelConfigSelection.js';
7
+ import { assertGraphModelAliasString } from './modelAliasContract.js';
7
8
  /** Top-level keys permitted on a canonical exellix-graph executable graph JSON document. */
8
9
  export const CANONICAL_GRAPH_TOP_LEVEL_KEYS = [
9
10
  'id',
@@ -296,8 +297,10 @@ function assertModelConfigSelection(value, path, context) {
296
297
  throw new ExellixGraphError(code, `${casePath} must be a plain object.`, context);
297
298
  }
298
299
  if (!isGraphAiModelConfig(c.modelConfig)) {
299
- throw new ExellixGraphError(code, `${casePath}.modelConfig must be { xynthesisModel: string, skillModel: string } with non-empty values.`, context);
300
+ throw new ExellixGraphError(code, `${casePath}.modelConfig must be { xynthesisModel: string, skillModel: string } with non-empty alias values.`, context);
300
301
  }
302
+ assertGraphModelAliasString(c.modelConfig.xynthesisModel, `${casePath}.modelConfig.xynthesisModel`, context);
303
+ assertGraphModelAliasString(c.modelConfig.skillModel, `${casePath}.modelConfig.skillModel`, context);
301
304
  if (isEmptyConditionWhen(c.when)) {
302
305
  continue;
303
306
  }
@@ -318,8 +321,10 @@ function assertOptionalRuntimeFlatModelConfig(value, path, context) {
318
321
  if (value === undefined)
319
322
  return;
320
323
  if (!isGraphAiModelConfig(value)) {
321
- throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `${path} must be exactly { xynthesisModel: string, skillModel: string } (runtime host override does not use cases).`, context);
324
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `${path} must be exactly { xynthesisModel: string, skillModel: string } alias names (runtime host override does not use cases).`, context);
322
325
  }
326
+ assertGraphModelAliasString(value.xynthesisModel, `${path}.xynthesisModel`, context);
327
+ assertGraphModelAliasString(value.skillModel, `${path}.skillModel`, context);
323
328
  }
324
329
  function assertContextualKnowledgeScope(scope, nodeId, context) {
325
330
  if (scope === undefined)
@@ -13,14 +13,14 @@ export type TaskNodeExecutionPipelineStep = {
13
13
  * Type references for graph execution
14
14
  */
15
15
  /**
16
- * Explicit AI model selection carried by graph model/runtime contracts.
17
- * Values may be concrete model ids or runtime aliases resolved before `runTask`.
16
+ * Alias-only AI model slot selection on graph model / node `modelConfig`.
17
+ * Concrete provider model ids are supplied only via {@link GraphModelAliasConfig} at execution.
18
18
  */
19
19
  export type GraphAiModelConfig = {
20
20
  xynthesisModel: string;
21
21
  skillModel: string;
22
22
  };
23
- /** Runtime alias map used to resolve GraphAiModelConfig values before dispatch. */
23
+ /** Runtime map: alias name concrete provider model id (required on every `executeGraph`). */
24
24
  export type GraphModelAliasConfig = Record<string, string>;
25
25
  /** Runtime overrides for a single task node, stored at GraphRuntimeObject.nodes[nodeId]. */
26
26
  export type TaskNodeRuntimeObject = {
@@ -1,4 +1,5 @@
1
1
  export { InMemoryGraphLoader } from './inMemoryGraphLoader.js';
2
2
  export { DepGraphEngineFactory } from './depGraphEngineFactory.js';
3
3
  export { RealTasksClient } from './RealTasksClient.js';
4
+ export { buildTestAliasConfig, withTestAliasRuntime, createTestExellixGraphRuntime, } from './testModelAliasRuntime.js';
4
5
  export { tryLoadExellixAiTasksRuntimeSubtree, loadExellixGraphRuntimeObjects, } from './exellixRuntimeObjects.js';
@@ -1,4 +1,5 @@
1
1
  export { InMemoryGraphLoader } from './inMemoryGraphLoader.js';
2
2
  export { DepGraphEngineFactory } from './depGraphEngineFactory.js';
3
3
  export { RealTasksClient } from './RealTasksClient.js';
4
+ export { buildTestAliasConfig, withTestAliasRuntime, createTestExellixGraphRuntime, } from './testModelAliasRuntime.js';
4
5
  export { tryLoadExellixAiTasksRuntimeSubtree, loadExellixGraphRuntimeObjects, } from './exellixRuntimeObjects.js';
@@ -0,0 +1,14 @@
1
+ import type { Graph } from '../src/types/refs.js';
2
+ import { createExellixGraphRuntime, type GraphRuntimeObject } from '../src/runtime/ExellixGraphRuntime.js';
3
+ /** Maps every alias used by a graph (plus `default`) to `openrouter/<alias>` for unit tests. */
4
+ export declare function buildTestAliasConfig(graph: Pick<Graph, 'modelConfig' | 'nodes'>, runtimePartial?: Pick<GraphRuntimeObject, 'modelConfig' | 'nodes'> & {
5
+ aliasConfig?: Record<string, string>;
6
+ }): Record<string, string>;
7
+ /** Merges test alias bindings into a runtime object (required for executeGraph in 7.x). */
8
+ export declare function withTestAliasRuntime(graph: Pick<Graph, 'id' | 'modelConfig' | 'nodes'>, runtime: Partial<GraphRuntimeObject> & Pick<GraphRuntimeObject, 'jobId' | 'job'>): GraphRuntimeObject;
9
+ type CreateOpts = Parameters<typeof createExellixGraphRuntime>[0];
10
+ /**
11
+ * Testkit runtime that auto-injects `runtime.aliasConfig` on every `executeGraph` call.
12
+ */
13
+ export declare function createTestExellixGraphRuntime(opts: CreateOpts): ReturnType<typeof createExellixGraphRuntime>;
14
+ export {};
@@ -0,0 +1,40 @@
1
+ import { createExellixGraphRuntime, } from '../src/runtime/ExellixGraphRuntime.js';
2
+ import { collectModelAliasesUsedInGraph } from '../src/runtime/modelAliasContract.js';
3
+ /** Maps every alias used by a graph (plus `default`) to `openrouter/<alias>` for unit tests. */
4
+ export function buildTestAliasConfig(graph, runtimePartial) {
5
+ const needed = collectModelAliasesUsedInGraph(graph, runtimePartial);
6
+ const map = {};
7
+ for (const alias of needed) {
8
+ map[alias] = `openrouter/${alias}`;
9
+ }
10
+ return { ...map, ...(runtimePartial?.aliasConfig ?? {}) };
11
+ }
12
+ /** Merges test alias bindings into a runtime object (required for executeGraph in 7.x). */
13
+ export function withTestAliasRuntime(graph, runtime) {
14
+ const aliasConfig = buildTestAliasConfig(graph, runtime);
15
+ return {
16
+ ...runtime,
17
+ aliasConfig,
18
+ };
19
+ }
20
+ /**
21
+ * Testkit runtime that auto-injects `runtime.aliasConfig` on every `executeGraph` call.
22
+ */
23
+ export function createTestExellixGraphRuntime(opts) {
24
+ const runtime = createExellixGraphRuntime(opts);
25
+ const executeGraph = runtime.executeGraph.bind(runtime);
26
+ runtime.executeGraph = async (input) => {
27
+ const base = input.runtime ?? {};
28
+ const jobId = typeof base.jobId === 'string' && base.jobId.trim() !== ''
29
+ ? base.jobId
30
+ : `job-${input.model.id ?? 'graph'}`;
31
+ const job = base.job != null && typeof base.job === 'object'
32
+ ? base.job
33
+ : { agentId: 'test-agent', jobType: 'unit-test' };
34
+ return executeGraph({
35
+ ...input,
36
+ runtime: withTestAliasRuntime(input.model, { ...base, jobId, job }),
37
+ });
38
+ };
39
+ return runtime;
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exellix/graph-engine",
3
- "version": "6.0.2",
3
+ "version": "7.0.1",
4
4
  "type": "module",
5
5
  "description": "Graph executor SDK",
6
6
  "main": "dist/src/index.js",
@@ -25,7 +25,8 @@
25
25
  "scripts": {
26
26
  "prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })\"",
27
27
  "build": "tsc",
28
- "test": "npm run build && npm run check:no-legacy && tsx --test --test-force-exit tests/graph-engine.test.ts tests/passthrough-parity.test.ts tests/task-node-run-task-preflight.test.ts tests/reports-fixtures-pre-synthesis.test.ts",
28
+ "test": "npm run build && npm run check:no-legacy && node --test --test-force-exit tests/model-alias-contract.test.ts tests/reports-fixtures-pre-synthesis.test.ts tests/passthrough-parity.test.ts",
29
+ "test:full": "npm run build && npm run check:no-legacy && tsx --test --test-force-exit tests/graph-engine.test.ts tests/passthrough-parity.test.ts tests/task-node-run-task-preflight.test.ts tests/reports-fixtures-pre-synthesis.test.ts tests/model-alias-contract.test.ts",
29
30
  "run:pre-synthesis": "npm run build && node --env-file=.env scripts/run-pre-synthesis-graph.mjs",
30
31
  "check:no-legacy": "node scripts/check-no-legacy.mjs",
31
32
  "lint": "eslint src testkit --ext .ts",
@@ -48,7 +49,7 @@
48
49
  "access": "public"
49
50
  },
50
51
  "dependencies": {
51
- "@exellix/ai-tasks": "^7.8.2",
52
+ "@exellix/ai-tasks": "^8.0.0",
52
53
  "@x12i/activix": "^7.1.0",
53
54
  "@x12i/catalox": "^5.1.1",
54
55
  "@x12i/env": "^4.0.1",