@exellix/graph-engine 7.7.7 → 7.7.8

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.7.8
4
+
5
+ ### Added (CR-11 execute-time model routing)
6
+
7
+ - **`mergeGraphAiModelConfig`**, **`PartialGraphAiModelConfig`**, **`isPartialGraphAiModelConfig`** — job-default + per-task partial override merge at execute time (`task[slot] ?? graph[slot] ?? engine default`).
8
+ - **`assertCanonicalGraphDocument(..., { mode: 'execute' })`** — accepts partial task `taskConfiguration.modelConfig` and `profile/choice` slot encoding (`cheap/default`, `cyber/deep_forensics`); **`persist`** mode (default) unchanged for save/inspection paths.
9
+ - **`executeGraph`** uses execute-mode validation so hosts can POST authored graph JSON without pre-merging model slots or stripping `/`.
10
+
11
+ ### Changed
12
+
13
+ - **`looksLikeConcreteModelId`** — only provider-prefixed ids are concrete; `profile/choice` strings are valid graph aliases.
14
+ - **`inspectGraphModelSelection`** — reports merged effective routing when task overrides are partial.
15
+
16
+ ### Migration (hosts / graphs-studio)
17
+
18
+ - Remove execute-time `skipCanonicalAssert`, host-side job→node model merge, and slot `/` stripping once on **7.7.8+** (see CR-11 / CR-9).
19
+
3
20
  ## 7.7.3
4
21
 
5
22
  ### Changed
@@ -15,7 +15,7 @@
15
15
  export type { HostExecuteGraphRunOptions, MainReadinessPolicy, ExecutionStepOption, StepRetryPolicy, ActivixNodeActivityExellixConfig, SkillKeyResolutionOptions, RunTaskRequest as ExellixGraphRunTaskRequest, RunTaskResponse as ExellixGraphRunTaskResponse, } from './types/options.js';
16
16
  export type { AiTaskProfileMetadata, AiTaskProfileWebScoping, AiTaskProfileInputSynthesis, } from './types/aiTaskProfile.js';
17
17
  export type { ExecutionStrategyInvocation, ExecutionStrategyPhase, ExecutionStrategyWrapperKey, SmartInputConfig, TaskStrategyItemData, XynthesizedDestinationScope, XynthesizedMemory, XynthesizedOutputConfig, } from './types/aiTasksDerivedTypes.js';
18
- export type { Graph, GraphModelObject, GraphAiModelConfig, GraphModelAliasConfig, GraphRuntimeNodeConfig, TaskNodeRuntimeObject, GraphDocumentMetadata, GraphEntryContract, GraphEntryExecutionInputSpec, GraphEntryInputKind, GraphEntryInputSpecBase, GraphEntryInputSpec, GraphEntryValueInputKind, GraphEntryValueInputSpec, GraphResponseDefinition, GraphResponseMissingBehavior, GraphResponseSelector, GraphResponseShape, GraphResponseContract, GraphResponseMapping, GraphResponseMappingMissingBehavior, GraphResponseMappingSelector, GraphResponseMappingTarget, GraphExecutionDefaults, GraphExecutionMode, GraphOutputMode, GraphFlowOutline, GraphCoreObjective, GraphNodesResponses, GraphNode, TaskNode, TaskNodeConditions, TaskNodeConditionWhen, TaskNodeJsonCondition, TaskNodeJsConditionFunction, TaskNodeAiCondition, TaskNodeConditionParameters, ModelConfigSelection, ModelConfigCase, TaskNodePureMetadata, TaskNodeExecutionPipelineStep, TaskOutputValidation, FinalizerNode, FinalizerInputBinding, OutputSchema, AggregateFinalizerConfig, BundleFinalizerConfig, SelectFinalizerConfig, SynthesizeFinalizerConfig, UtilityExecutionPolicy, Job, CatalogPlanningKind, CatalogRequestStatus, CatalogBinding, ScopedQuestionCatalogRequestEntry, DiscoveryDefinitionCatalogRequestEntry, DiscoveryDefinitionCatalogRequest, DiscoveryDefinitionPlanningFields, CatalogRequestEntry, StructuredDataFiltersV1, ConditionsDataFilters, } from './types/refs.js';
18
+ export type { Graph, GraphModelObject, GraphAiModelConfig, PartialGraphAiModelConfig, GraphModelAliasConfig, GraphRuntimeNodeConfig, TaskNodeRuntimeObject, GraphDocumentMetadata, GraphEntryContract, GraphEntryExecutionInputSpec, GraphEntryInputKind, GraphEntryInputSpecBase, GraphEntryInputSpec, GraphEntryValueInputKind, GraphEntryValueInputSpec, GraphResponseDefinition, GraphResponseMissingBehavior, GraphResponseSelector, GraphResponseShape, GraphResponseContract, GraphResponseMapping, GraphResponseMappingMissingBehavior, GraphResponseMappingSelector, GraphResponseMappingTarget, GraphExecutionDefaults, GraphExecutionMode, GraphOutputMode, GraphFlowOutline, GraphCoreObjective, GraphNodesResponses, GraphNode, TaskNode, TaskNodeConditions, TaskNodeConditionWhen, TaskNodeJsonCondition, TaskNodeJsConditionFunction, TaskNodeAiCondition, TaskNodeConditionParameters, ModelConfigSelection, ModelConfigCase, TaskNodePureMetadata, TaskNodeExecutionPipelineStep, TaskOutputValidation, FinalizerNode, FinalizerInputBinding, OutputSchema, AggregateFinalizerConfig, BundleFinalizerConfig, SelectFinalizerConfig, SynthesizeFinalizerConfig, UtilityExecutionPolicy, Job, CatalogPlanningKind, CatalogRequestStatus, CatalogBinding, ScopedQuestionCatalogRequestEntry, DiscoveryDefinitionCatalogRequestEntry, DiscoveryDefinitionCatalogRequest, DiscoveryDefinitionPlanningFields, CatalogRequestEntry, StructuredDataFiltersV1, ConditionsDataFilters, } from './types/refs.js';
19
19
  export { mergeGraphDocumentModel, EXELLIX_GRAPH_MODEL_VARIABLE_KEY, EXELLIX_STRUCTURED_DATA_FILTERS_V1, } from './types/refs.js';
20
20
  export type { TaskNodeTaskConfiguration, TaskNodeScopedDataReaderPackSlot, } from './types/taskNodeConfiguration.js';
21
21
  export { getTaskConfiguration } from './types/taskNodeConfiguration.js';
@@ -39,7 +39,7 @@ export type { PathSegment } from './runtime/pathExpr.js';
39
39
  export { evaluateStructuredDataFilters, evaluateDataFilterPredicate, getStructuredDataFilterPathViolations, isStructuredDataFiltersV1, } from './runtime/dataFiltersEvaluation.js';
40
40
  export { evaluateTaskNodeConditions, evaluateConditionWhen, applyConditionNegate, } from './runtime/taskNodeConditionsEvaluation.js';
41
41
  export type { TaskNodeConditionsEvalDeps, TaskNodeConditionsEvalResult, TaskNodeConditionEvalContext, } from './runtime/taskNodeConditionsEvaluation.js';
42
- export { resolveModelConfigForNode, resolveGraphAiModelConfig, toRunTaskModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './runtime/resolveModelConfigForNode.js';
42
+ export { resolveModelConfigForNode, resolveGraphAiModelConfig, toRunTaskModelConfig, mergeGraphAiModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './runtime/resolveModelConfigForNode.js';
43
43
  export { looksLikeConcreteModelId, isGraphAiProfileName, assertGraphAiProfileNameString, type RunTaskModelConfigWire, type EngineModelPhase, } from './runtime/graphAiModelConfig.js';
44
44
  /** @deprecated Use {@link resolveGraphAiModelConfig}. */
45
45
  export { resolveGraphAiModelConfig as resolveGraphAiModelConfigAliases } from './runtime/graphAiModelConfig.js';
@@ -47,7 +47,7 @@ export { resolveGraphAiModelConfig as resolveGraphAiModelConfigAliases } from '.
47
47
  export { isGraphAiProfileName as isModelAliasString } from './runtime/graphAiModelConfig.js';
48
48
  /** @deprecated Use {@link assertGraphAiProfileNameString}. */
49
49
  export { assertGraphAiProfileNameString as assertGraphModelAliasString } from './runtime/graphAiModelConfig.js';
50
- export { isGraphAiModelConfig, isModelConfigSelection, isEmptyConditionWhen, conditionWhenSignature, countDefaultModelConfigCases, } from './runtime/modelConfigSelection.js';
50
+ export { isGraphAiModelConfig, isPartialGraphAiModelConfig, isModelConfigSelection, isEmptyConditionWhen, conditionWhenSignature, countDefaultModelConfigCases, } from './runtime/modelConfigSelection.js';
51
51
  export { GRAPH_ENGINE_MEMORY_PATH_ROOTS, splitGraphEngineMemoryPath, isAllowedGraphEngineMemoryPath, graphEngineMemoryPathValidationMessage, } from './runtime/graphEngineMemoryPaths.js';
52
52
  export { buildRunTaskMainInput, extractCallerInputsBag, buildGraphEngineMemoryResolutionRoot, buildGraphEngineMemoryResolutionRootFromWorkingMemory, resolveGraphEngineMemoryPathValue, } from './runtime/resolveGraphEngineMemoryPaths.js';
53
53
  export { mirrorStructuredInputOntoExecutionMemory, } from './runtime/materializeStructuredRunTaskInput.js';
@@ -79,6 +79,7 @@ export { buildExellixGraphRuntimeObjects, setRuntimeObjectsLastJobId, summarizeR
79
79
  export type { ActivixQueryableClient, LogxerQueryableClient, LogxerLogLine, PackageRuntimeObjects, RuntimeObjects, BuildExellixGraphRuntimeObjectsInput, RuntimeObjectsObservabilitySummary, } from './runtime/runtimeObjects.js';
80
80
  export type { GetJobLogsInput, GetJobLogsResult, QueryableLogLine } from '@x12i/logxer';
81
81
  export { assertCanonicalGraphDocument, assertCanonicalTaskNode, getCanonicalGraphDocumentViolations, CANONICAL_GRAPH_TOP_LEVEL_KEYS, } from './runtime/validateCanonicalGraphDocument.js';
82
+ export type { AssertCanonicalGraphDocumentOptions, CanonicalGraphDocumentValidationMode, } from './runtime/validateCanonicalGraphDocument.js';
82
83
  export { assertCanonicalGraphRuntimeObject } from './runtime/validateCanonicalGraphRuntime.js';
83
84
  export { GRAPH_ENTRY_STUDIO_ONLY_KEYS, GRAPH_METADATA_STUDIO_ONLY_KEYS, stripGraphModelStudioFields, primaryRuntimeInputFromStudioDocument, getGraphEntryStudioOnlyKeyViolations, getGraphMetadataStudioOnlyKeyViolations, getGraphEntryEmptyInputPathViolations, } from './runtime/graphModelStudioSeparation.js';
84
85
  export type { GraphStudioDocument } from './runtime/graphModelStudioSeparation.js';
package/dist/src/index.js CHANGED
@@ -29,7 +29,7 @@ export { mergeMemory } from './runtime/memory.js';
29
29
  export { selectByPath, writeByPath, parsePath } from './runtime/pathExpr.js';
30
30
  export { evaluateStructuredDataFilters, evaluateDataFilterPredicate, getStructuredDataFilterPathViolations, isStructuredDataFiltersV1, } from './runtime/dataFiltersEvaluation.js';
31
31
  export { evaluateTaskNodeConditions, evaluateConditionWhen, applyConditionNegate, } from './runtime/taskNodeConditionsEvaluation.js';
32
- export { resolveModelConfigForNode, resolveGraphAiModelConfig, toRunTaskModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './runtime/resolveModelConfigForNode.js';
32
+ export { resolveModelConfigForNode, resolveGraphAiModelConfig, toRunTaskModelConfig, mergeGraphAiModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './runtime/resolveModelConfigForNode.js';
33
33
  export { looksLikeConcreteModelId, isGraphAiProfileName, assertGraphAiProfileNameString, } from './runtime/graphAiModelConfig.js';
34
34
  /** @deprecated Use {@link resolveGraphAiModelConfig}. */
35
35
  export { resolveGraphAiModelConfig as resolveGraphAiModelConfigAliases } from './runtime/graphAiModelConfig.js';
@@ -37,7 +37,7 @@ export { resolveGraphAiModelConfig as resolveGraphAiModelConfigAliases } from '.
37
37
  export { isGraphAiProfileName as isModelAliasString } from './runtime/graphAiModelConfig.js';
38
38
  /** @deprecated Use {@link assertGraphAiProfileNameString}. */
39
39
  export { assertGraphAiProfileNameString as assertGraphModelAliasString } from './runtime/graphAiModelConfig.js';
40
- export { isGraphAiModelConfig, isModelConfigSelection, isEmptyConditionWhen, conditionWhenSignature, countDefaultModelConfigCases, } from './runtime/modelConfigSelection.js';
40
+ export { isGraphAiModelConfig, isPartialGraphAiModelConfig, isModelConfigSelection, isEmptyConditionWhen, conditionWhenSignature, countDefaultModelConfigCases, } from './runtime/modelConfigSelection.js';
41
41
  export { GRAPH_ENGINE_MEMORY_PATH_ROOTS, splitGraphEngineMemoryPath, isAllowedGraphEngineMemoryPath, graphEngineMemoryPathValidationMessage, } from './runtime/graphEngineMemoryPaths.js';
42
42
  export { buildRunTaskMainInput, extractCallerInputsBag, buildGraphEngineMemoryResolutionRoot, buildGraphEngineMemoryResolutionRootFromWorkingMemory, resolveGraphEngineMemoryPathValue, } from './runtime/resolveGraphEngineMemoryPaths.js';
43
43
  export { mirrorStructuredInputOntoExecutionMemory, } from './runtime/materializeStructuredRunTaskInput.js';
@@ -1,16 +1,17 @@
1
- import { isEmptyConditionWhen, isModelConfigSelection } from '../runtime/modelConfigSelection.js';
2
- import { DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG } from '../runtime/graphAiModelConfig.js';
1
+ import { isEmptyConditionWhen, isModelConfigSelection, isPartialGraphAiModelConfig } from '../runtime/modelConfigSelection.js';
2
+ import { DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, mergeGraphAiModelConfig } from '../runtime/graphAiModelConfig.js';
3
3
  function asArrayNodes(graph) {
4
4
  return Array.isArray(graph.nodes) ? graph.nodes : Object.values(graph.nodes ?? {});
5
5
  }
6
- /** Returns the no-`when` default case profiles from a selection, if present and valid. */
6
+ /** Returns the no-`when` default case partial profiles from a selection, if present. */
7
7
  function defaultCaseProfiles(selection) {
8
8
  if (!isModelConfigSelection(selection))
9
9
  return undefined;
10
10
  const sel = selection;
11
11
  for (const c of sel.cases ?? []) {
12
- if (isEmptyConditionWhen(c.when))
12
+ if (isEmptyConditionWhen(c.when) && isPartialGraphAiModelConfig(c.modelConfig)) {
13
13
  return c.modelConfig;
14
+ }
14
15
  }
15
16
  return undefined;
16
17
  }
@@ -36,7 +37,10 @@ function finalizerUsesModel(node) {
36
37
  * provider id resolution happens inside `@exellix/ai-tasks`.
37
38
  */
38
39
  export function inspectGraphModelSelection(graph) {
39
- const graphDefaultProfiles = defaultCaseProfiles(graph.modelConfig);
40
+ const graphDefaultPartial = defaultCaseProfiles(graph.modelConfig);
41
+ const graphDefaultProfiles = graphDefaultPartial
42
+ ? mergeGraphAiModelConfig(DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, graphDefaultPartial)
43
+ : undefined;
40
44
  const nodes = [];
41
45
  const aliases = new Set();
42
46
  const addAliases = (cfg) => {
@@ -76,15 +80,12 @@ export function inspectGraphModelSelection(graph) {
76
80
  const taskNode = raw;
77
81
  const nodeSelection = taskNode.taskConfiguration?.modelConfig;
78
82
  const nodeDefault = defaultCaseProfiles(nodeSelection);
83
+ const graphPartial = graphDefaultPartial;
79
84
  let profiles;
80
85
  let source;
81
- if (nodeDefault) {
82
- profiles = nodeDefault;
83
- source = 'node';
84
- }
85
- else if (graphDefaultProfiles) {
86
- profiles = graphDefaultProfiles;
87
- source = 'graph';
86
+ if (nodeDefault != null || graphPartial != null) {
87
+ profiles = mergeGraphAiModelConfig(DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, graphPartial, nodeDefault);
88
+ source = nodeDefault != null ? 'node' : 'graph';
88
89
  }
89
90
  else {
90
91
  profiles = DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG;
@@ -1175,7 +1175,7 @@ export function createExellixGraphRuntime(opts) {
1175
1175
  return runWithAiTasksStackLogging(merged.logging, () => runGraphWithLogContext({ jobId, taskId: graphTaskId, graphId: resolvedGraphId, runId: graphTaskId }, async () => {
1176
1176
  bindGraphEngineRunLogxer(runLogxer);
1177
1177
  try {
1178
- assertCanonicalGraphDocument(graph, { jobId, graphId: resolvedGraphId });
1178
+ assertCanonicalGraphDocument(graph, { jobId, graphId: resolvedGraphId }, { mode: 'execute' });
1179
1179
  assertCanonicalGraphRuntimeObject(runtime, { jobId, graphId: resolvedGraphId });
1180
1180
  let runxClient = opts.runx;
1181
1181
  if (graphNeedsRunxClient(graph)) {
@@ -1,4 +1,4 @@
1
- import type { GraphAiModelConfig } from '../types/refs.js';
1
+ import type { GraphAiModelConfig, PartialGraphAiModelConfig } from '../types/refs.js';
2
2
  /** Wire shape for `@exellix/ai-tasks` `RunTaskRequest.modelConfig` (8.4+). */
3
3
  export type RunTaskModelConfigWire = {
4
4
  preActionModel: string;
@@ -38,3 +38,8 @@ export type EngineModelPhase = 'pre' | 'main' | 'post';
38
38
  * Values are profile alias names from the graph document; ai-tasks resolves and routes slots per phase.
39
39
  */
40
40
  export declare function toRunTaskModelConfig(config: GraphAiModelConfig): RunTaskModelConfigWire;
41
+ /**
42
+ * Merges layered partial configs left-to-right; later layers override earlier slots.
43
+ * Missing slots after all layers fall back to {@link DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG}.
44
+ */
45
+ export declare function mergeGraphAiModelConfig(...layers: Array<PartialGraphAiModelConfig | undefined>): GraphAiModelConfig;
@@ -37,8 +37,6 @@ export function looksLikeConcreteModelId(value) {
37
37
  const trimmed = value.trim();
38
38
  if (trimmed === '')
39
39
  return false;
40
- if (trimmed.includes('/'))
41
- return true;
42
40
  const lower = trimmed.toLowerCase();
43
41
  return CONCRETE_MODEL_PREFIXES.some((p) => lower.startsWith(p));
44
42
  }
@@ -55,7 +53,7 @@ export function isGraphAiProfileName(value) {
55
53
  }
56
54
  export function assertGraphAiProfileNameString(value, path, context) {
57
55
  if (!isGraphAiProfileName(value)) {
58
- throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_MODEL_CONFIG, `${path} must be an AI profile alias (non-empty string without "/" or provider prefixes); got ${JSON.stringify(value)}`, context);
56
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_MODEL_CONFIG, `${path} must be an AI profile alias, profile/choice key, or shortcut (not a concrete provider model id); got ${JSON.stringify(value)}`, context);
59
57
  }
60
58
  }
61
59
  function normalizeModelSlot(slot, context) {
@@ -89,3 +87,26 @@ export function toRunTaskModelConfig(config) {
89
87
  postActionModel: config.postActionModel,
90
88
  };
91
89
  }
90
+ /**
91
+ * Merges layered partial configs left-to-right; later layers override earlier slots.
92
+ * Missing slots after all layers fall back to {@link DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG}.
93
+ */
94
+ export function mergeGraphAiModelConfig(...layers) {
95
+ const merged = {};
96
+ for (const layer of layers) {
97
+ if (layer == null)
98
+ continue;
99
+ if (layer.preActionModel != null)
100
+ merged.preActionModel = layer.preActionModel.trim();
101
+ if (layer.skillModel != null)
102
+ merged.skillModel = layer.skillModel.trim();
103
+ if (layer.postActionModel != null)
104
+ merged.postActionModel = layer.postActionModel.trim();
105
+ }
106
+ const base = DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG;
107
+ return {
108
+ preActionModel: merged.preActionModel ?? base.preActionModel,
109
+ skillModel: merged.skillModel ?? base.skillModel,
110
+ postActionModel: merged.postActionModel ?? base.postActionModel,
111
+ };
112
+ }
@@ -1,4 +1,6 @@
1
- import type { GraphAiModelConfig, ModelConfigCase, ModelConfigSelection, TaskNodeConditionWhen } from '../types/refs.js';
1
+ import type { GraphAiModelConfig, ModelConfigCase, ModelConfigSelection, PartialGraphAiModelConfig, TaskNodeConditionWhen } from '../types/refs.js';
2
+ /** True when `value` is a non-empty subset of the three-phase model slots (partial or complete). */
3
+ export declare function isPartialGraphAiModelConfig(value: unknown): value is PartialGraphAiModelConfig;
2
4
  export declare function isGraphAiModelConfig(value: unknown): value is GraphAiModelConfig;
3
5
  export declare function isEmptyConditionWhen(when: TaskNodeConditionWhen | undefined): boolean;
4
6
  export declare function isModelConfigSelection(value: unknown): value is ModelConfigSelection;
@@ -1,18 +1,23 @@
1
- export function isGraphAiModelConfig(value) {
1
+ const MODEL_CONFIG_SLOT_KEYS = ['preActionModel', 'skillModel', 'postActionModel'];
2
+ /** True when `value` is a non-empty subset of the three-phase model slots (partial or complete). */
3
+ export function isPartialGraphAiModelConfig(value) {
2
4
  if (value == null || typeof value !== 'object' || Array.isArray(value))
3
5
  return false;
4
6
  const o = value;
5
7
  const keys = Object.keys(o);
6
- return (keys.length === 3 &&
7
- keys.includes('preActionModel') &&
8
- keys.includes('skillModel') &&
9
- keys.includes('postActionModel') &&
10
- typeof o.preActionModel === 'string' &&
11
- o.preActionModel.trim() !== '' &&
12
- typeof o.skillModel === 'string' &&
13
- o.skillModel.trim() !== '' &&
14
- typeof o.postActionModel === 'string' &&
15
- o.postActionModel.trim() !== '');
8
+ if (keys.length === 0 || keys.length > MODEL_CONFIG_SLOT_KEYS.length)
9
+ return false;
10
+ if (!keys.every((k) => MODEL_CONFIG_SLOT_KEYS.includes(k)))
11
+ return false;
12
+ return keys.every((k) => typeof o[k] === 'string' && o[k].trim() !== '');
13
+ }
14
+ export function isGraphAiModelConfig(value) {
15
+ if (!isPartialGraphAiModelConfig(value))
16
+ return false;
17
+ const o = value;
18
+ return (o.preActionModel != null &&
19
+ o.skillModel != null &&
20
+ o.postActionModel != null);
16
21
  }
17
22
  export function isEmptyConditionWhen(when) {
18
23
  if (when == null)
@@ -14,8 +14,9 @@ export type ResolveModelConfigForNodeArgs = {
14
14
  };
15
15
  /**
16
16
  * Resolves effective model config for a task node from the graph document only.
17
- * Precedence: `node.taskConfiguration.modelConfig` `graph.modelConfig` engine default profiles.
17
+ * Merges `node.taskConfiguration.modelConfig` (partial override) over `graph.modelConfig` (job default),
18
+ * then engine defaults for any remaining slots.
18
19
  */
19
20
  export declare function resolveModelConfigForNode(args: ResolveModelConfigForNodeArgs): Promise<GraphAiModelConfig>;
20
21
  export { conditionWhenSignature, isEmptyConditionWhen };
21
- export { resolveGraphAiModelConfig, toRunTaskModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './graphAiModelConfig.js';
22
+ export { resolveGraphAiModelConfig, toRunTaskModelConfig, mergeGraphAiModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './graphAiModelConfig.js';
@@ -1,6 +1,6 @@
1
- import { conditionWhenSignature, isEmptyConditionWhen, isGraphAiModelConfig, isModelConfigSelection, } from './modelConfigSelection.js';
1
+ import { conditionWhenSignature, isEmptyConditionWhen, isModelConfigSelection, isPartialGraphAiModelConfig, } from './modelConfigSelection.js';
2
2
  import { evaluateConditionWhen } from './taskNodeConditionsEvaluation.js';
3
- import { DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './graphAiModelConfig.js';
3
+ import { DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, mergeGraphAiModelConfig, resolveGraphAiModelConfig, } from './graphAiModelConfig.js';
4
4
  async function selectFromCases(selection, ctx, executionInput, runx) {
5
5
  const cases = selection.cases;
6
6
  if (!Array.isArray(cases) || cases.length === 0)
@@ -16,17 +16,17 @@ async function selectFromCases(selection, ctx, executionInput, runx) {
16
16
  const ev = await evaluateConditionWhen(c.when, ctx, executionInput, { runx });
17
17
  if (ev.error)
18
18
  continue;
19
- if (ev.ok && isGraphAiModelConfig(c.modelConfig)) {
19
+ if (ev.ok && isPartialGraphAiModelConfig(c.modelConfig)) {
20
20
  return c.modelConfig;
21
21
  }
22
22
  }
23
- if (defaultCase != null && isGraphAiModelConfig(defaultCase.modelConfig)) {
23
+ if (defaultCase != null && isPartialGraphAiModelConfig(defaultCase.modelConfig)) {
24
24
  return defaultCase.modelConfig;
25
25
  }
26
26
  return undefined;
27
27
  }
28
28
  async function resolveTier(value, ctx, executionInput, runx) {
29
- if (isGraphAiModelConfig(value))
29
+ if (isPartialGraphAiModelConfig(value))
30
30
  return value;
31
31
  if (isModelConfigSelection(value)) {
32
32
  return selectFromCases(value, ctx, executionInput, runx);
@@ -35,17 +35,18 @@ async function resolveTier(value, ctx, executionInput, runx) {
35
35
  }
36
36
  /**
37
37
  * Resolves effective model config for a task node from the graph document only.
38
- * Precedence: `node.taskConfiguration.modelConfig` `graph.modelConfig` engine default profiles.
38
+ * Merges `node.taskConfiguration.modelConfig` (partial override) over `graph.modelConfig` (job default),
39
+ * then engine defaults for any remaining slots.
39
40
  */
40
41
  export async function resolveModelConfigForNode(args) {
41
- const tiers = [args.nodeModelConfig, args.graphModelConfig];
42
- for (const tier of tiers) {
43
- const selected = await resolveTier(tier, args.conditionCtx, args.executionInput, args.runx);
44
- if (selected != null) {
45
- return selected;
46
- }
47
- }
48
- return DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG;
42
+ const graphPartial = await resolveTier(args.graphModelConfig, args.conditionCtx, args.executionInput, args.runx);
43
+ const nodePartial = await resolveTier(args.nodeModelConfig, args.conditionCtx, args.executionInput, args.runx);
44
+ const merged = mergeGraphAiModelConfig(DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, graphPartial, nodePartial);
45
+ return resolveGraphAiModelConfig(merged, {
46
+ graphId: args.graphId,
47
+ nodeId: args.nodeId,
48
+ jobId: args.jobId,
49
+ });
49
50
  }
50
51
  export { conditionWhenSignature, isEmptyConditionWhen };
51
- export { resolveGraphAiModelConfig, toRunTaskModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './graphAiModelConfig.js';
52
+ export { resolveGraphAiModelConfig, toRunTaskModelConfig, mergeGraphAiModelConfig, DEFAULT_GRAPH_AI_MODEL_IDS, DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG, } from './graphAiModelConfig.js';
@@ -5,12 +5,17 @@ export declare const CANONICAL_GRAPH_TOP_LEVEL_KEYS: readonly ["id", "version",
5
5
  * Returns top-level keys on `graph` that are not part of the canonical executable document contract.
6
6
  */
7
7
  export declare function getCanonicalGraphDocumentViolations(graph: unknown): string[];
8
+ export type CanonicalGraphDocumentValidationMode = 'persist' | 'execute';
9
+ export type AssertCanonicalGraphDocumentOptions = {
10
+ /** `execute` accepts partial task overrides and profile/choice slot encoding; `persist` stays strict. */
11
+ mode?: CanonicalGraphDocumentValidationMode;
12
+ };
8
13
  /** Single-node validation. Throws `NON_CANONICAL_TASK_NODE` on the first canonical violation. */
9
14
  export declare function assertCanonicalTaskNode(node: unknown, context?: {
10
15
  jobId?: string;
11
16
  graphId?: string;
12
17
  nodeIndex?: number;
13
- }): void;
18
+ }, options?: AssertCanonicalGraphDocumentOptions): void;
14
19
  /**
15
20
  * Throws {@link ExellixGraphError} when the value is not a canonical graph document
16
21
  * (forbidden top-level keys, record-keyed `nodes`, root `outputConstraints`, or any
@@ -22,4 +27,4 @@ export declare function assertCanonicalTaskNode(node: unknown, context?: {
22
27
  export declare function assertCanonicalGraphDocument(graph: Graph, context?: {
23
28
  jobId?: string;
24
29
  graphId?: string;
25
- }): void;
30
+ }, options?: AssertCanonicalGraphDocumentOptions): void;
@@ -3,7 +3,7 @@ import { ExellixGraphError } from '../errors/ExellixGraphError.js';
3
3
  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
- import { conditionWhenSignature, countDefaultModelConfigCases, isEmptyConditionWhen, isGraphAiModelConfig, isModelConfigSelection, } from './modelConfigSelection.js';
6
+ import { conditionWhenSignature, countDefaultModelConfigCases, isEmptyConditionWhen, isGraphAiModelConfig, isModelConfigSelection, isPartialGraphAiModelConfig, } from './modelConfigSelection.js';
7
7
  import { assertGraphAiProfileNameString } from './graphAiModelConfig.js';
8
8
  import { assertCanonicalGraphDocumentMetadata } from './graphModelStudioSeparation.js';
9
9
  /** Top-level keys permitted on a canonical exellix-graph executable graph JSON document. */
@@ -273,13 +273,20 @@ function assertOptionalTaskNodeConditions(value, nodeId, context) {
273
273
  assertTaskNodeJsCondition(value.jsConditionFunction, `Task node "${nodeId}": conditions.jsConditionFunction`, ctx);
274
274
  assertTaskNodeAiCondition(value.aiCondition, `Task node "${nodeId}": conditions.aiCondition`, ctx);
275
275
  }
276
- function assertModelConfigSelection(value, path, context) {
276
+ function assertModelConfigSlotIfPresent(slot, slotPath, context) {
277
+ if (slot === undefined)
278
+ return;
279
+ assertGraphAiProfileNameString(slot, slotPath, context);
280
+ }
281
+ function assertModelConfigSelection(value, path, context, options) {
277
282
  if (value === undefined)
278
283
  return;
284
+ const mode = options?.mode ?? 'persist';
285
+ const allowPartial = mode === 'execute';
279
286
  const code = context?.nodeId
280
287
  ? ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE
281
288
  : ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT;
282
- if (isGraphAiModelConfig(value)) {
289
+ if (isGraphAiModelConfig(value) || (allowPartial && isPartialGraphAiModelConfig(value))) {
283
290
  throw new ExellixGraphError(code, `${path}: flat modelConfig was removed. Use { cases: [{ modelConfig: { preActionModel, skillModel, postActionModel } }] } (one case with no \`when\` for a single default).`, context);
284
291
  }
285
292
  if (!isModelConfigSelection(value)) {
@@ -297,12 +304,24 @@ function assertModelConfigSelection(value, path, context) {
297
304
  if (!isPlainObject(c)) {
298
305
  throw new ExellixGraphError(code, `${casePath} must be a plain object.`, context);
299
306
  }
300
- if (!isGraphAiModelConfig(c.modelConfig)) {
301
- throw new ExellixGraphError(code, `${casePath}.modelConfig must be { preActionModel: string, skillModel: string, postActionModel: string } with non-empty AI profile names.`, context);
307
+ const mc = c.modelConfig;
308
+ const mcValid = allowPartial ? isPartialGraphAiModelConfig(mc) : isGraphAiModelConfig(mc);
309
+ if (!mcValid) {
310
+ throw new ExellixGraphError(code, allowPartial
311
+ ? `${casePath}.modelConfig must include at least one of { preActionModel, skillModel, postActionModel } with non-empty alias or profile/choice values.`
312
+ : `${casePath}.modelConfig must be { preActionModel: string, skillModel: string, postActionModel: string } with non-empty AI profile names.`, context);
313
+ }
314
+ const partial = mc;
315
+ if (!allowPartial) {
316
+ assertGraphAiProfileNameString(partial.preActionModel, `${casePath}.modelConfig.preActionModel`, context);
317
+ assertGraphAiProfileNameString(partial.skillModel, `${casePath}.modelConfig.skillModel`, context);
318
+ assertGraphAiProfileNameString(partial.postActionModel, `${casePath}.modelConfig.postActionModel`, context);
319
+ }
320
+ else {
321
+ assertModelConfigSlotIfPresent(partial.preActionModel, `${casePath}.modelConfig.preActionModel`, context);
322
+ assertModelConfigSlotIfPresent(partial.skillModel, `${casePath}.modelConfig.skillModel`, context);
323
+ assertModelConfigSlotIfPresent(partial.postActionModel, `${casePath}.modelConfig.postActionModel`, context);
302
324
  }
303
- assertGraphAiProfileNameString(c.modelConfig.preActionModel, `${casePath}.modelConfig.preActionModel`, context);
304
- assertGraphAiProfileNameString(c.modelConfig.skillModel, `${casePath}.modelConfig.skillModel`, context);
305
- assertGraphAiProfileNameString(c.modelConfig.postActionModel, `${casePath}.modelConfig.postActionModel`, context);
306
325
  if (isEmptyConditionWhen(c.when)) {
307
326
  continue;
308
327
  }
@@ -424,7 +443,7 @@ function isTaskShape(node) {
424
443
  return true;
425
444
  }
426
445
  /** Single-node validation. Throws `NON_CANONICAL_TASK_NODE` on the first canonical violation. */
427
- export function assertCanonicalTaskNode(node, context) {
446
+ export function assertCanonicalTaskNode(node, context, options) {
428
447
  if (!isPlainObject(node)) {
429
448
  throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `Graph node at index ${context?.nodeIndex ?? '?'} is not a plain object.`, { jobId: context?.jobId, graphId: context?.graphId });
430
449
  }
@@ -479,7 +498,7 @@ export function assertCanonicalTaskNode(node, context) {
479
498
  jobId: context?.jobId,
480
499
  graphId: context?.graphId,
481
500
  nodeId: String(node.id),
482
- });
501
+ }, options);
483
502
  assertContextualKnowledgeScope(taskNode.scope, String(node.id), {
484
503
  jobId: context?.jobId,
485
504
  graphId: context?.graphId,
@@ -548,7 +567,7 @@ export function assertCanonicalTaskNode(node, context) {
548
567
  * `variables`, required `response`, and `metadata`); runtime fields belong under the execution
549
568
  * request `runtime` object.
550
569
  */
551
- export function assertCanonicalGraphDocument(graph, context) {
570
+ export function assertCanonicalGraphDocument(graph, context, options) {
552
571
  const violations = getCanonicalGraphDocumentViolations(graph);
553
572
  if (violations.length > 0) {
554
573
  const graphId = context?.graphId ??
@@ -583,7 +602,7 @@ export function assertCanonicalGraphDocument(graph, context) {
583
602
  assertModelConfigSelection(graph.modelConfig, 'graph.modelConfig', {
584
603
  jobId: context?.jobId,
585
604
  graphId: resolvedGraphId,
586
- });
605
+ }, options);
587
606
  const metadata = isPlainObject(graph.metadata)
588
607
  ? graph.metadata
589
608
  : undefined;
@@ -613,7 +632,7 @@ export function assertCanonicalGraphDocument(graph, context) {
613
632
  // Per-node canonical checks.
614
633
  const nodes = graph.nodes;
615
634
  for (let i = 0; i < nodes.length; i++) {
616
- assertCanonicalTaskNode(nodes[i], { jobId: context?.jobId, graphId: resolvedGraphId, nodeIndex: i });
635
+ assertCanonicalTaskNode(nodes[i], { jobId: context?.jobId, graphId: resolvedGraphId, nodeIndex: i }, options);
617
636
  }
618
637
  if (!Object.prototype.hasOwnProperty.call(graph, 'response')) {
619
638
  throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `Graph "${String(resolvedGraphId ?? '?')}" must declare root graph.response. Final response shaping must not live under metadata.graphResponse.responseMapping or finalizer output selection.`, { jobId: context?.jobId, graphId: resolvedGraphId });
@@ -21,6 +21,12 @@ export type GraphAiModelConfig = {
21
21
  skillModel: string;
22
22
  postActionModel: string;
23
23
  };
24
+ /** Authoring / execute ingress: one or more phase slots; omitted slots inherit at runtime. */
25
+ export type PartialGraphAiModelConfig = {
26
+ preActionModel?: string;
27
+ skillModel?: string;
28
+ postActionModel?: string;
29
+ };
24
30
  /**
25
31
  * @deprecated Legacy alias map — no longer required for execute. Hosts may omit `runtime.aliasConfig`.
26
32
  */
@@ -347,7 +353,7 @@ export type TaskNodeConditions = {
347
353
  };
348
354
  export type ModelConfigCase = {
349
355
  when?: TaskNodeConditionWhen;
350
- modelConfig: GraphAiModelConfig;
356
+ modelConfig: PartialGraphAiModelConfig;
351
357
  };
352
358
  /** Canonical authoring shape for graph and taskConfiguration model selection. */
353
359
  export type ModelConfigSelection = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exellix/graph-engine",
3
- "version": "7.7.7",
3
+ "version": "7.7.8",
4
4
  "type": "module",
5
5
  "description": "Graph executor SDK",
6
6
  "main": "dist/src/index.js",