@exellix/graph-engine 7.4.2 → 7.5.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/README.md CHANGED
@@ -383,12 +383,25 @@ Implemented deterministic finalizer types:
383
383
 
384
384
  Build an object by mapping named inputs (from `executionMemoryPath` or literals) into output keys.
385
385
 
386
- #### `aggregate` — `strategy: "report-schema"` (multi-section report from execution memory)
386
+ #### `aggregate` — `strategy: "report-schema"` (output object assembled from run memory)
387
387
 
388
- Builds a single object whose keys come from **`config.sections`**: each section specifies a **dot-path** into `executionMemory` (`path`), optional **`title`**, and **`optional`** (when `true`, missing values become `null` instead of throwing).
388
+ Builds the finalizer output object: **`config.sections`** maps each **output field key** to a memory read, using the same `{ type, path }` idiom as finalizer `inputs` and `response.shape` selectors:
389
389
 
390
- - **`collect_tags`**: when `true`, walks all section values and collects unique epistemic strings among `CONFIRMED`, `INFERRED`, `ASSUMED`, `UNKNOWN` into **`collected_tags`** (array).
391
- - **`meta`**: optional literal key/value pairs merged at the top level of the parsed output.
390
+ ```jsonc
391
+ "config": {
392
+ "strategy": "report-schema",
393
+ "sections": {
394
+ "q1": { "type": "executionMemoryPath", "path": "answers.q1", "optional": true },
395
+ "summary": { "type": "outputsMemoryPath", "path": "report.summary" }
396
+ },
397
+ "collectEpistemicTags": true
398
+ }
399
+ ```
400
+
401
+ - Each section: **`type`** (`executionMemoryPath` | `outputsMemoryPath`), **`path`**, and **`optional`** (when `true`, missing values become `null` instead of throwing).
402
+ - **`collectEpistemicTags`**: when `true`, walks all section values and collects unique epistemic strings among `CONFIRMED`, `INFERRED`, `ASSUMED`, `UNKNOWN` into the **`collectedTags`** output field (array).
403
+
404
+ > **Breaking (model v8):** `sections[].path: string` → `sections[].{ type, path }`; `collect_tags` → `collectEpistemicTags`; output `collected_tags` → `collectedTags`. Section `title` and the literal-merge `meta` block were **removed** — authoring titles and studio hints belong in the studio document, and literal output fields belong in `graph.response.shape`. The validator rejects the old keys with a migration message.
392
405
 
393
406
  Bundled graphs under [`graphs/`](graphs/) (see [graphs/README.md](graphs/README.md)) use this strategy for multi-section reports, sometimes after **`scoped-answer-assembler`** and **`scoped-answer-writer`** persistence (typical store: **`x-scoped-data`** in deployments that use that collection). The graph file shape is defined in [.docs/exellix-graph-engine-format.md](.docs/exellix-graph-engine-format.md).
394
407
 
@@ -80,6 +80,8 @@ export { buildExellixGraphRuntimeObjects, setRuntimeObjectsLastJobId, summarizeR
80
80
  export type { ActivixQueryableClient, LogxerQueryableClient, LogxerLogLine, PackageRuntimeObjects, RuntimeObjects, BuildExellixGraphRuntimeObjectsInput, RuntimeObjectsObservabilitySummary, } from './runtime/runtimeObjects.js';
81
81
  export type { GetJobLogsInput, GetJobLogsResult, QueryableLogLine } from '@x12i/logxer';
82
82
  export { assertCanonicalGraphDocument, getCanonicalGraphDocumentViolations, CANONICAL_GRAPH_TOP_LEVEL_KEYS, } from './runtime/validateCanonicalGraphDocument.js';
83
+ export { GRAPH_ENTRY_STUDIO_ONLY_KEYS, GRAPH_METADATA_STUDIO_ONLY_KEYS, stripGraphModelStudioFields, primaryRuntimeInputFromStudioDocument, getGraphEntryStudioOnlyKeyViolations, getGraphMetadataStudioOnlyKeyViolations, getGraphEntryEmptyInputPathViolations, } from './runtime/graphModelStudioSeparation.js';
84
+ export type { GraphStudioDocument } from './runtime/graphModelStudioSeparation.js';
83
85
  export { computeGraphDocumentContentSha256, stableStringifyGraphDocument, } from './runtime/graphDocumentFingerprint.js';
84
86
  export { migrateLegacyGraphResponse, migrateLegacyGraphResponseDefinition, } from './runtime/graphResponseMigration.js';
85
87
  export type { MigrateGraphResponseResult } from './runtime/graphResponseMigration.js';
@@ -106,7 +108,7 @@ export type { GraphCatalogValidationIssue } from './integrations/cataloxGraphCat
106
108
  export { composeEventEmitters } from './runtime/events.js';
107
109
  export { createPlaygroundReporter } from './playground/index.js';
108
110
  export type { CreatePlaygroundReporterOptions, PlaygroundDebugArtifact, PlaygroundDebugArtifactKind, PlaygroundDebugSnapshot, PlaygroundReporter, PlaygroundStep, } from './playground/PlaygroundReporter.js';
109
- export { getNodeScopingQuestion, getNodeMemoryShape, getNodeNarrixDiscovery, fetchNodeScopingData, inspectNode, resolveNodeSkillKey, getGraphNodes, getGraphCatalogs, inspectGraph, collectPredicatePaths, executionMemoryPathTail, executionMemoryTailsMatch, getNodeExecutionMemoryWriteTails, getNodeControlDependencies, getNodeSideEffects, inspectFinalizer, inspectNodeContract, inspectGraphContracts, EXELLIX_VIRTUAL_GRAPH_ENTRY_NODE_ID, EXELLIX_VIRTUAL_GRAPH_RESPONSE_NODE_ID, EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY, EXELLIX_VIRTUAL_BOUNDARY_EDGE_FLAG_KEY, getVirtualGraphEntryLayer, getVirtualGraphResponseLayer, materializeVirtualBoundaryDiagram, stripMaterializedVirtualBoundaryDiagram, validateCatalogPlanning, isCatalogBinding, isCatalogRequestEntry, } from './inspection/index.js';
110
- export type { NodeScopingQuestion, NodeScopeSource, NodeScopeTarget, NodeScopingData, NodeMemoryShape, MemoryPath, NodeNarrixDiscovery, NodeInspection, GraphInspection, NodeIOEdge, GraphCatalogs, CatalogBindingSummary, CatalogPlanningValidationIssue, GraphVirtualIO, GraphVirtualIONode, GraphVirtualIOEdge, GraphVirtualIONodeRole, VirtualGraphEntryLayer, VirtualGraphResponseLayer, MaterializeVirtualBoundaryDiagramOptions, MaterializedVirtualBoundaryDiagram, MaterializedVirtualBoundaryDiagramMeta, InspectNodeContractOptions, LocalSkillKey, NodeExecutionType, FactProvenance, ProvenancedSection, NodeInputBindingEntry, ScopedDataDependency, NodeInputContract, ExecutionMemoryWriteSpec, NodeOutputContract, NodeSideEffects, ControlBranchEntry, NodeControlContract, NodeValidationContract, FinalizerContractInspection, NodeContractInspection, GraphContractsInspection, } from './inspection/index.js';
111
+ export { getNodeScopingQuestion, getNodeMemoryShape, getNodeNarrixDiscovery, fetchNodeScopingData, inspectNode, resolveNodeSkillKey, getGraphNodes, getGraphCatalogs, inspectGraph, collectPredicatePaths, executionMemoryPathTail, executionMemoryTailsMatch, getNodeExecutionMemoryWriteTails, getNodeControlDependencies, getNodeSideEffects, inspectFinalizer, inspectNodeContract, inspectGraphContracts, EXELLIX_VIRTUAL_GRAPH_ENTRY_NODE_ID, EXELLIX_VIRTUAL_GRAPH_RESPONSE_NODE_ID, EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY, EXELLIX_VIRTUAL_BOUNDARY_EDGE_FLAG_KEY, getVirtualGraphEntryLayer, getVirtualGraphResponseLayer, materializeVirtualBoundaryDiagram, stripMaterializedVirtualBoundaryDiagram, validateCatalogPlanning, isCatalogBinding, isCatalogRequestEntry, inspectGraphModelSelection, } from './inspection/index.js';
112
+ export type { GraphModelSelectionInspection, NodeModelSelection, ModelSelectionSource, NodeScopingQuestion, NodeScopeSource, NodeScopeTarget, NodeScopingData, NodeMemoryShape, MemoryPath, NodeNarrixDiscovery, NodeInspection, GraphInspection, NodeIOEdge, GraphCatalogs, CatalogBindingSummary, CatalogPlanningValidationIssue, GraphVirtualIO, GraphVirtualIONode, GraphVirtualIOEdge, GraphVirtualIONodeRole, VirtualGraphEntryLayer, VirtualGraphResponseLayer, MaterializeVirtualBoundaryDiagramOptions, MaterializedVirtualBoundaryDiagram, MaterializedVirtualBoundaryDiagramMeta, InspectNodeContractOptions, LocalSkillKey, NodeExecutionType, FactProvenance, ProvenancedSection, NodeInputBindingEntry, ScopedDataDependency, NodeInputContract, ExecutionMemoryWriteSpec, NodeOutputContract, NodeSideEffects, ControlBranchEntry, NodeControlContract, NodeValidationContract, FinalizerContractInspection, NodeContractInspection, GraphContractsInspection, } from './inspection/index.js';
111
113
  export type { NarrixPreProcessorConfig, WebScopeQuestion, LocalTaskHandler, LocalTaskContext } from './types/narrix.js';
112
114
  export type { ScopedDataReaderConfig, ScopedDataReaderOutput, ScopedDataReaderPackOutput, ScopedAnswerAssemblerConfig, ScopedAnswerAssemblerOutput, ScopedAnswerWriterConfig, ScopedAnswerWriterOutput, } from './runtime/localSkills/index.js';
package/dist/src/index.js CHANGED
@@ -59,6 +59,7 @@ export { buildTaskNodeRunTaskRequest, validateTaskNodeRunTaskConfig, validateTas
59
59
  export { validateRunTaskConfig, validateRunTaskInvoke, analyzeExpectedRunTaskInput, checkExpectedInputAgainstRequest, collectSmartInputValidationIssues, buildRunTaskValidationContext, analyzeRunTaskRequest, validateParsedOutput, analyzeSkillRequest, buildSkillRequestAnalysisPacket, formatSkillRequestAnalysisMarkdown, listTokens, analyzeTemplateResolution, renderSmartInput, extractTokenNamesFromStrings, getSkillTokens, getSkillContent, } from '@exellix/ai-tasks';
60
60
  export { buildExellixGraphRuntimeObjects, setRuntimeObjectsLastJobId, summarizeRuntimeObjectsForPlayground, EXELLIX_GRAPH_RUNTIME_PACKAGE_NAME, EXELLIX_AI_TASKS_PACKAGE_NAME, } from './runtime/runtimeObjects.js';
61
61
  export { assertCanonicalGraphDocument, getCanonicalGraphDocumentViolations, CANONICAL_GRAPH_TOP_LEVEL_KEYS, } from './runtime/validateCanonicalGraphDocument.js';
62
+ export { GRAPH_ENTRY_STUDIO_ONLY_KEYS, GRAPH_METADATA_STUDIO_ONLY_KEYS, stripGraphModelStudioFields, primaryRuntimeInputFromStudioDocument, getGraphEntryStudioOnlyKeyViolations, getGraphMetadataStudioOnlyKeyViolations, getGraphEntryEmptyInputPathViolations, } from './runtime/graphModelStudioSeparation.js';
62
63
  export { computeGraphDocumentContentSha256, stableStringifyGraphDocument, } from './runtime/graphDocumentFingerprint.js';
63
64
  export { migrateLegacyGraphResponse, migrateLegacyGraphResponseDefinition, } from './runtime/graphResponseMigration.js';
64
65
  export { applyAiTaskProfileWebScopingToNarrix, mapAiTaskProfileQuestionsToWebScopeQuestions, } from './runtime/applyAiTaskProfileWebScopingToNarrix.js';
@@ -81,5 +82,5 @@ export { composeEventEmitters } from './runtime/events.js';
81
82
  // Playground (full-visibility MD report)
82
83
  export { createPlaygroundReporter } from './playground/index.js';
83
84
  // Graph Inspection API
84
- export { getNodeScopingQuestion, getNodeMemoryShape, getNodeNarrixDiscovery, fetchNodeScopingData, inspectNode, resolveNodeSkillKey, getGraphNodes, getGraphCatalogs, inspectGraph, collectPredicatePaths, executionMemoryPathTail, executionMemoryTailsMatch, getNodeExecutionMemoryWriteTails, getNodeControlDependencies, getNodeSideEffects, inspectFinalizer, inspectNodeContract, inspectGraphContracts, EXELLIX_VIRTUAL_GRAPH_ENTRY_NODE_ID, EXELLIX_VIRTUAL_GRAPH_RESPONSE_NODE_ID, EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY, EXELLIX_VIRTUAL_BOUNDARY_EDGE_FLAG_KEY, getVirtualGraphEntryLayer, getVirtualGraphResponseLayer, materializeVirtualBoundaryDiagram, stripMaterializedVirtualBoundaryDiagram, validateCatalogPlanning, isCatalogBinding, isCatalogRequestEntry, } from './inspection/index.js';
85
+ export { getNodeScopingQuestion, getNodeMemoryShape, getNodeNarrixDiscovery, fetchNodeScopingData, inspectNode, resolveNodeSkillKey, getGraphNodes, getGraphCatalogs, inspectGraph, collectPredicatePaths, executionMemoryPathTail, executionMemoryTailsMatch, getNodeExecutionMemoryWriteTails, getNodeControlDependencies, getNodeSideEffects, inspectFinalizer, inspectNodeContract, inspectGraphContracts, EXELLIX_VIRTUAL_GRAPH_ENTRY_NODE_ID, EXELLIX_VIRTUAL_GRAPH_RESPONSE_NODE_ID, EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY, EXELLIX_VIRTUAL_BOUNDARY_EDGE_FLAG_KEY, getVirtualGraphEntryLayer, getVirtualGraphResponseLayer, materializeVirtualBoundaryDiagram, stripMaterializedVirtualBoundaryDiagram, validateCatalogPlanning, isCatalogBinding, isCatalogRequestEntry, inspectGraphModelSelection, } from './inspection/index.js';
85
86
  // Web context rendering — consume execution.webContext → markdown for prompts (Layer 04)
@@ -0,0 +1,33 @@
1
+ import type { Graph, GraphAiModelConfig } from '../types/refs.js';
2
+ /** Where the effective model-profile triplet for a node came from. */
3
+ export type ModelSelectionSource = 'node' | 'graph' | 'default' | 'none';
4
+ export type NodeModelSelection = {
5
+ nodeId: string;
6
+ kind: 'task' | 'finalizer';
7
+ /** False for deterministic finalizers (aggregate / select / bundle); those call no model. */
8
+ usesModel: boolean;
9
+ /** Effective default-case profile aliases for this node (undefined when `usesModel` is false). */
10
+ profiles?: GraphAiModelConfig;
11
+ source: ModelSelectionSource;
12
+ /** Count of conditional (`when`-gated) cases that may override the default at runtime. */
13
+ conditionalCaseCount: number;
14
+ };
15
+ export type GraphModelSelectionInspection = {
16
+ /** Graph-level default profile aliases (undefined when the graph declares none). */
17
+ graphDefaultProfiles?: GraphAiModelConfig;
18
+ nodes: NodeModelSelection[];
19
+ /** Distinct profile alias names referenced anywhere in the model (for `runtime.aliasConfig` wiring). */
20
+ aliasesUsed: string[];
21
+ };
22
+ /**
23
+ * Static, runtime-free view of which model-profile aliases each node would use.
24
+ *
25
+ * Answers "where are the models (aliases) that will be used?" without executing:
26
+ * - task nodes resolve `node.taskConfiguration.modelConfig` (override) → graph `modelConfig` → engine default;
27
+ * - `synthesize` finalizers use the graph default; other finalizers call no model.
28
+ *
29
+ * Only the unconditional (`when`-less) default case is reported per tier; `conditionalCaseCount`
30
+ * flags nodes whose effective model may change at runtime via gated cases. Alias → concrete
31
+ * provider id still happens at runtime through `runtime.aliasConfig`.
32
+ */
33
+ export declare function inspectGraphModelSelection(graph: Graph): GraphModelSelectionInspection;
@@ -0,0 +1,108 @@
1
+ import { isEmptyConditionWhen, isModelConfigSelection } from '../runtime/modelConfigSelection.js';
2
+ import { DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG } from '../runtime/graphAiModelConfig.js';
3
+ function asArrayNodes(graph) {
4
+ return Array.isArray(graph.nodes) ? graph.nodes : Object.values(graph.nodes ?? {});
5
+ }
6
+ /** Returns the no-`when` default case profiles from a selection, if present and valid. */
7
+ function defaultCaseProfiles(selection) {
8
+ if (!isModelConfigSelection(selection))
9
+ return undefined;
10
+ const sel = selection;
11
+ for (const c of sel.cases ?? []) {
12
+ if (isEmptyConditionWhen(c.when))
13
+ return c.modelConfig;
14
+ }
15
+ return undefined;
16
+ }
17
+ function conditionalCaseCount(selection) {
18
+ if (!isModelConfigSelection(selection))
19
+ return 0;
20
+ const sel = selection;
21
+ return (sel.cases ?? []).filter((c) => !isEmptyConditionWhen(c.when)).length;
22
+ }
23
+ /** True for finalizer types that invoke a model (`synthesize`); deterministic kinds do not. */
24
+ function finalizerUsesModel(node) {
25
+ return node.finalizerType === 'synthesize';
26
+ }
27
+ /**
28
+ * Static, runtime-free view of which model-profile aliases each node would use.
29
+ *
30
+ * Answers "where are the models (aliases) that will be used?" without executing:
31
+ * - task nodes resolve `node.taskConfiguration.modelConfig` (override) → graph `modelConfig` → engine default;
32
+ * - `synthesize` finalizers use the graph default; other finalizers call no model.
33
+ *
34
+ * Only the unconditional (`when`-less) default case is reported per tier; `conditionalCaseCount`
35
+ * flags nodes whose effective model may change at runtime via gated cases. Alias → concrete
36
+ * provider id still happens at runtime through `runtime.aliasConfig`.
37
+ */
38
+ export function inspectGraphModelSelection(graph) {
39
+ const graphDefaultProfiles = defaultCaseProfiles(graph.modelConfig);
40
+ const nodes = [];
41
+ const aliases = new Set();
42
+ const addAliases = (cfg) => {
43
+ if (cfg == null)
44
+ return;
45
+ aliases.add(cfg.preActionModel);
46
+ aliases.add(cfg.skillModel);
47
+ aliases.add(cfg.postActionModel);
48
+ };
49
+ for (const raw of asArrayNodes(graph)) {
50
+ const node = raw;
51
+ const isFinalizer = node.type === 'finalizer';
52
+ if (isFinalizer) {
53
+ const fin = raw;
54
+ if (!finalizerUsesModel(fin)) {
55
+ nodes.push({
56
+ nodeId: String(fin.id),
57
+ kind: 'finalizer',
58
+ usesModel: false,
59
+ source: 'none',
60
+ conditionalCaseCount: 0,
61
+ });
62
+ continue;
63
+ }
64
+ const profiles = graphDefaultProfiles ?? DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG;
65
+ addAliases(profiles);
66
+ nodes.push({
67
+ nodeId: String(fin.id),
68
+ kind: 'finalizer',
69
+ usesModel: true,
70
+ profiles,
71
+ source: graphDefaultProfiles ? 'graph' : 'default',
72
+ conditionalCaseCount: 0,
73
+ });
74
+ continue;
75
+ }
76
+ const taskNode = raw;
77
+ const nodeSelection = taskNode.taskConfiguration?.modelConfig;
78
+ const nodeDefault = defaultCaseProfiles(nodeSelection);
79
+ let profiles;
80
+ let source;
81
+ if (nodeDefault) {
82
+ profiles = nodeDefault;
83
+ source = 'node';
84
+ }
85
+ else if (graphDefaultProfiles) {
86
+ profiles = graphDefaultProfiles;
87
+ source = 'graph';
88
+ }
89
+ else {
90
+ profiles = DEFAULT_GRAPH_AI_MODEL_PROFILE_CONFIG;
91
+ source = 'default';
92
+ }
93
+ addAliases(profiles);
94
+ nodes.push({
95
+ nodeId: String(taskNode.id),
96
+ kind: 'task',
97
+ usesModel: true,
98
+ profiles,
99
+ source,
100
+ conditionalCaseCount: conditionalCaseCount(nodeSelection) + conditionalCaseCount(graph.modelConfig),
101
+ });
102
+ }
103
+ return {
104
+ graphDefaultProfiles,
105
+ nodes,
106
+ aliasesUsed: [...aliases].sort(),
107
+ };
108
+ }
@@ -15,6 +15,8 @@ export type { CatalogPlanningValidationIssue } from './validateCatalogPlanning.j
15
15
  export { collectPredicatePaths, executionMemoryPathTail, executionMemoryTailsMatch, getNodeExecutionMemoryWriteTails, getNodeControlDependencies, } from './controlInspection.js';
16
16
  export { getNodeSideEffects, inspectFinalizer, inspectNodeContract, inspectGraphContracts, } from './contractInspection.js';
17
17
  export type { InspectNodeContractOptions } from './contractInspection.js';
18
+ export { inspectGraphModelSelection } from './graphModelSelection.js';
19
+ export type { GraphModelSelectionInspection, NodeModelSelection, ModelSelectionSource, } from './graphModelSelection.js';
18
20
  export type { NodeScopingQuestion, NodeScopeSource, NodeScopeTarget, NodeScopingData, NodeMemoryShape, MemoryPath, NodeNarrixDiscovery, NodeInspection, GraphInspection, NodeIOEdge, GraphCatalogs, CatalogBindingSummary, GraphVirtualIO, GraphVirtualIONode, GraphVirtualIOEdge, GraphVirtualIONodeRole, VirtualGraphEntryLayer, VirtualGraphResponseLayer, MaterializeVirtualBoundaryDiagramOptions, MaterializedVirtualBoundaryDiagram, MaterializedVirtualBoundaryDiagramMeta, } from './types.js';
19
21
  export { EXELLIX_VIRTUAL_GRAPH_ENTRY_NODE_ID, EXELLIX_VIRTUAL_GRAPH_RESPONSE_NODE_ID, EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY, EXELLIX_VIRTUAL_BOUNDARY_EDGE_FLAG_KEY, } from './types.js';
20
22
  export type { GraphDocumentMetadata } from '../types/refs.js';
@@ -14,4 +14,5 @@ export { getGraphNodes, getGraphCatalogs, inspectGraph, getVirtualGraphEntryLaye
14
14
  export { validateCatalogPlanning, isCatalogBinding, isCatalogRequestEntry, } from './validateCatalogPlanning.js';
15
15
  export { collectPredicatePaths, executionMemoryPathTail, executionMemoryTailsMatch, getNodeExecutionMemoryWriteTails, getNodeControlDependencies, } from './controlInspection.js';
16
16
  export { getNodeSideEffects, inspectFinalizer, inspectNodeContract, inspectGraphContracts, } from './contractInspection.js';
17
+ export { inspectGraphModelSelection } from './graphModelSelection.js';
17
18
  export { EXELLIX_VIRTUAL_GRAPH_ENTRY_NODE_ID, EXELLIX_VIRTUAL_GRAPH_RESPONSE_NODE_ID, EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY, EXELLIX_VIRTUAL_BOUNDARY_EDGE_FLAG_KEY, } from './types.js';
@@ -52,7 +52,7 @@ function finalizerRuntimeReadSources(node, graph) {
52
52
  for (const [key, spec] of Object.entries(cfg.sections)) {
53
53
  if (!isPlainObject(spec))
54
54
  continue;
55
- pushDedupedRead(reads, spec.path, `config.sections.${key}`, 'executionMemory');
55
+ pushDedupedRead(reads, spec.path, `config.sections.${key}`, spec.type === 'outputsMemoryPath' ? 'outputsMemory' : 'executionMemory');
56
56
  }
57
57
  return reads;
58
58
  }
@@ -91,6 +91,8 @@ export interface GraphRuntimeObject extends HostExecuteGraphRunOptions {
91
91
  taskVariables?: Record<string, unknown>;
92
92
  /** Runtime default model selection (`cases`); overrides GraphModelObject.modelConfig. */
93
93
  modelConfig?: ModelConfigSelection;
94
+ /** Profile aliases for xynthesis when runtime.modelConfig is host-resolved concrete ids. */
95
+ aiProfiles?: GraphAiModelConfig;
94
96
  /**
95
97
  * @deprecated Unused since 7.1 — profile aliases pass through to downstream resolution.
96
98
  */
@@ -210,6 +212,8 @@ export declare function createExellixGraphRuntime(opts: ExellixGraphRuntimeOptio
210
212
  prevNodeId?: string;
211
213
  /** Overrides graph-level / runtime default for this node only. */
212
214
  modelConfig?: GraphAiModelConfig;
215
+ /** Profile aliases for xynthesis PRE/POST when modelConfig is host-resolved concrete ids. */
216
+ aiProfiles?: GraphAiModelConfig;
213
217
  llmCall?: Record<string, unknown>;
214
218
  runTaskIdentity?: Record<string, unknown>;
215
219
  runTaskExecutionMode?: "default" | "trace";
@@ -397,7 +397,7 @@ export function createExellixGraphRuntime(opts) {
397
397
  },
398
398
  ...(input.modelConfig != null
399
399
  ? {
400
- modelConfig: toRunTaskModelConfigForPhase(input.modelConfig, 'pre'),
400
+ modelConfig: toRunTaskModelConfigForPhase(input.modelConfig, 'pre', input.aiProfiles),
401
401
  }
402
402
  : {}),
403
403
  ...(effectiveLlmCall != null ? { llmCall: effectiveLlmCall } : {}),
@@ -610,7 +610,7 @@ export function createExellixGraphRuntime(opts) {
610
610
  }
611
611
  const effectiveModelConfig = input.modelConfig;
612
612
  const mainRunTaskModelConfig = effectiveModelConfig != null
613
- ? toRunTaskModelConfigForPhase(effectiveModelConfig, 'main')
613
+ ? toRunTaskModelConfigForPhase(effectiveModelConfig, 'main', input.aiProfiles)
614
614
  : undefined;
615
615
  // DEBUG: Verify execution object before sending
616
616
  traceExecutionMemory('executeNode', 'Node execution object before runTask', {
@@ -722,6 +722,7 @@ export function createExellixGraphRuntime(opts) {
722
722
  jobContext,
723
723
  prevNodeId: input.prevNodeId,
724
724
  modelConfig: effectiveModelConfig,
725
+ aiProfiles: input.aiProfiles,
725
726
  llmCall: effectiveLlmCall,
726
727
  runTaskIdentity: runTaskIdentityBase,
727
728
  nodeTaskConfiguration: input.node?.taskConfiguration,
@@ -1023,6 +1024,7 @@ export function createExellixGraphRuntime(opts) {
1023
1024
  jobContext,
1024
1025
  prevNodeId: input.prevNodeId,
1025
1026
  modelConfig: effectiveModelConfig,
1027
+ aiProfiles: input.aiProfiles,
1026
1028
  llmCall: effectiveLlmCall,
1027
1029
  runTaskIdentity: runTaskIdentityBase,
1028
1030
  nodeTaskConfiguration: input.node?.taskConfiguration,
@@ -1616,6 +1618,7 @@ export function createExellixGraphRuntime(opts) {
1616
1618
  variables: runtime.variables,
1617
1619
  taskVariables: runtime.taskVariables,
1618
1620
  modelConfig: effectiveModelConfig,
1621
+ aiProfiles: merged.aiProfiles,
1619
1622
  llmCall: merged.llmCall,
1620
1623
  runTaskIdentity: merged.runTaskIdentity,
1621
1624
  runTaskExecutionMode: merged.runTaskExecutionMode,
@@ -27,6 +27,8 @@ export type RunEngineAiTasksStrategyPhaseArgs = {
27
27
  prevNodeId?: string;
28
28
  /** Resolved three-phase config; routed to the correct ai-tasks slot for this strategy phase. */
29
29
  modelConfig?: GraphAiModelConfig;
30
+ /** Profile aliases for xynthesis slots when modelConfig is host-resolved concrete ids. */
31
+ aiProfiles?: GraphAiModelConfig;
30
32
  llmCall?: RunTaskRequest['llmCall'];
31
33
  runTaskIdentity?: Record<string, unknown>;
32
34
  /** Task node `taskConfiguration` (identity envelope merge). */
@@ -55,7 +55,7 @@ export async function runEngineAiTasksStrategyPhase(args) {
55
55
  executionPipeline: [{ phase: 'main', type: 'direct' }],
56
56
  executionStrategies: [],
57
57
  modelConfig: args.modelConfig != null
58
- ? toRunTaskModelConfigForPhase(args.modelConfig, args.phase)
58
+ ? toRunTaskModelConfigForPhase(args.modelConfig, args.phase, args.aiProfiles)
59
59
  : undefined,
60
60
  llmCall: args.llmCall ?? undefined,
61
61
  identity,
@@ -284,11 +284,12 @@ function collectEpistemicTags(value, found) {
284
284
  }
285
285
  }
286
286
  function executeReportSchema(args) {
287
- const { cfg, executionMemory } = args;
287
+ const { cfg, executionMemory, outputsMemory } = args;
288
288
  const finalizerNodeId = String(args.finalizer.id);
289
289
  const out = {};
290
290
  for (const [key, spec] of Object.entries(cfg.sections ?? {})) {
291
- let v = getByDotPath(executionMemory, spec.path);
291
+ const root = spec.type === 'outputsMemoryPath' ? outputsMemory : executionMemory;
292
+ let v = getByDotPath(root, spec.path);
292
293
  if (isNodeFailureMarker(v)) {
293
294
  if (spec.optional === true) {
294
295
  out[key] = null;
@@ -296,7 +297,7 @@ function executeReportSchema(args) {
296
297
  }
297
298
  throw createFinalizerError({
298
299
  code: 'GRAPH_FINALIZER_NODE_FAILED',
299
- message: `report-schema: section "${key}" points at a failed graph node (path="${spec.path}")`,
300
+ message: `report-schema: section "${key}" points at a failed graph node (${spec.type}="${spec.path}")`,
300
301
  finalizerNodeId,
301
302
  path: `config.sections.${key}`,
302
303
  details: { spec, marker: v },
@@ -305,7 +306,7 @@ function executeReportSchema(args) {
305
306
  if ((v === undefined || v === null) && spec.optional !== true) {
306
307
  throw createFinalizerError({
307
308
  code: 'GRAPH_FINALIZER_INPUT_MISSING',
308
- message: `report-schema: required section "${key}" not found at path "${spec.path}"`,
309
+ message: `report-schema: required section "${key}" not found at ${spec.type} "${spec.path}"`,
309
310
  finalizerNodeId,
310
311
  path: `config.sections.${key}`,
311
312
  details: { spec },
@@ -313,16 +314,11 @@ function executeReportSchema(args) {
313
314
  }
314
315
  out[key] = v ?? null;
315
316
  }
316
- if (cfg.collect_tags) {
317
+ if (cfg.collectEpistemicTags) {
317
318
  const tags = new Set();
318
319
  for (const v of Object.values(out))
319
320
  collectEpistemicTags(v, tags);
320
- out.collected_tags = Array.from(tags);
321
- }
322
- if (cfg.meta) {
323
- for (const [k, v] of Object.entries(cfg.meta)) {
324
- out[k] = v;
325
- }
321
+ out.collectedTags = Array.from(tags);
326
322
  }
327
323
  return out;
328
324
  }
@@ -55,7 +55,8 @@ export function collectRequiredFinalizerExecutionMemoryReads(finalizer, graph) {
55
55
  for (const [key, spec] of Object.entries(cfg.sections)) {
56
56
  if (!isPlainObject(spec))
57
57
  continue;
58
- pushRequiredRead(reads, 'execution', spec.path, `config.sections.${key}`, spec.optional);
58
+ const memory = spec.type === 'outputsMemoryPath' ? 'outputs' : 'execution';
59
+ pushRequiredRead(reads, memory, spec.path, `config.sections.${key}`, spec.optional);
59
60
  }
60
61
  return reads;
61
62
  }
@@ -180,6 +181,46 @@ export function assertFinalizerRequiredReadsResolvable(args) {
180
181
  }
181
182
  }
182
183
  }
184
+ /** Validates a `{ type: executionMemoryPath | outputsMemoryPath, path, optional? }` memory read. */
185
+ function validateMemoryRef(ref, path, finalizerNodeId, label) {
186
+ if (!isPlainObject(ref)) {
187
+ throw createFinalizerError({
188
+ code: 'GRAPH_FINALIZER_INVALID',
189
+ message: `${label} at ${path} must be an object { type, path }`,
190
+ finalizerNodeId,
191
+ path,
192
+ details: { ref },
193
+ });
194
+ }
195
+ const type = ref.type;
196
+ if (type !== 'executionMemoryPath' && type !== 'outputsMemoryPath') {
197
+ throw createFinalizerError({
198
+ code: 'GRAPH_FINALIZER_INVALID',
199
+ message: `${label} at ${path} must declare type "executionMemoryPath" or "outputsMemoryPath" (got ${String(type)})`,
200
+ finalizerNodeId,
201
+ path: `${path}.type`,
202
+ details: { ref },
203
+ });
204
+ }
205
+ const p = ref.path;
206
+ if (typeof p !== 'string' || p.length === 0) {
207
+ throw createFinalizerError({
208
+ code: 'GRAPH_FINALIZER_INVALID',
209
+ message: `${label} at ${path} requires a non-empty path`,
210
+ finalizerNodeId,
211
+ path: `${path}.path`,
212
+ });
213
+ }
214
+ const optional = ref.optional;
215
+ if (optional !== undefined && typeof optional !== 'boolean') {
216
+ throw createFinalizerError({
217
+ code: 'GRAPH_FINALIZER_INVALID',
218
+ message: `${label} at ${path} optional must be boolean when provided`,
219
+ finalizerNodeId,
220
+ path: `${path}.optional`,
221
+ });
222
+ }
223
+ }
183
224
  function validateBinding(name, b, finalizerNodeId) {
184
225
  if (!isPlainObject(b)) {
185
226
  throw createFinalizerError({
@@ -245,54 +286,57 @@ function validateAggregateConfig(cfg, finalizerNodeId) {
245
286
  return;
246
287
  }
247
288
  if (strategy === 'report-schema') {
248
- const sections = cfg.sections;
249
- if (sections !== undefined && !isPlainObject(sections)) {
289
+ const REPORT_SCHEMA_ALLOWED_KEYS = new Set(['strategy', 'sections', 'collectEpistemicTags']);
290
+ const REPORT_SCHEMA_REMOVED_KEYS = {
291
+ collect_tags: 'collectEpistemicTags (boolean)',
292
+ meta: 'removed — put literal output fields in graph.response.shape, and authoring titles in the studio document',
293
+ title: 'removed from sections — authoring titles belong in the studio document',
294
+ schemaVersion: 'removed — not part of the executable model',
295
+ skeletonFromConcept: 'removed — studio authoring hint, not part of the executable model',
296
+ };
297
+ for (const key of Object.keys(cfg)) {
298
+ if (REPORT_SCHEMA_ALLOWED_KEYS.has(key))
299
+ continue;
300
+ const migration = REPORT_SCHEMA_REMOVED_KEYS[key];
250
301
  throw createFinalizerError({
251
302
  code: 'GRAPH_FINALIZER_INVALID',
252
- message: `report-schema aggregate finalizer config.sections must be an object when provided`,
303
+ message: migration
304
+ ? `report-schema finalizer config.${key} is no longer supported. Use: ${migration}.`
305
+ : `report-schema finalizer has unknown config key "${key}". Allowed: ${[...REPORT_SCHEMA_ALLOWED_KEYS].join(', ')}.`,
253
306
  finalizerNodeId,
254
- path: 'config.sections',
307
+ path: `config.${key}`,
255
308
  details: { config: cfg },
256
309
  });
257
310
  }
258
- if (isPlainObject(sections)) {
259
- for (const [k, spec] of Object.entries(sections)) {
260
- if (!isPlainObject(spec) || typeof spec.path !== 'string' || spec.path.length === 0) {
261
- throw createFinalizerError({
262
- code: 'GRAPH_FINALIZER_INVALID',
263
- message: `report-schema section "${k}" must have a non-empty path`,
264
- finalizerNodeId,
265
- path: `config.sections.${k}.path`,
266
- details: { spec },
267
- });
268
- }
269
- const optional = spec.optional;
270
- if (optional !== undefined && typeof optional !== 'boolean') {
271
- throw createFinalizerError({
272
- code: 'GRAPH_FINALIZER_INVALID',
273
- message: `report-schema section "${k}" optional must be boolean when provided`,
274
- finalizerNodeId,
275
- path: `config.sections.${k}.optional`,
276
- });
277
- }
278
- }
279
- }
280
- const collectTags = cfg.collect_tags;
281
- if (collectTags !== undefined && typeof collectTags !== 'boolean') {
311
+ const sections = cfg.sections;
312
+ if (!isPlainObject(sections) || Object.keys(sections).length === 0) {
282
313
  throw createFinalizerError({
283
314
  code: 'GRAPH_FINALIZER_INVALID',
284
- message: `report-schema aggregate finalizer config.collect_tags must be boolean when provided`,
315
+ message: `report-schema aggregate finalizer requires a non-empty config.sections object (output key { type, path }).`,
285
316
  finalizerNodeId,
286
- path: 'config.collect_tags',
317
+ path: 'config.sections',
318
+ details: { config: cfg },
287
319
  });
288
320
  }
289
- const meta = cfg.meta;
290
- if (meta !== undefined && !isPlainObject(meta)) {
321
+ for (const [k, spec] of Object.entries(sections)) {
322
+ validateMemoryRef(spec, `config.sections.${k}`, finalizerNodeId, 'report-schema section');
323
+ const title = spec.title;
324
+ if (title !== undefined) {
325
+ throw createFinalizerError({
326
+ code: 'GRAPH_FINALIZER_INVALID',
327
+ message: `report-schema section "${k}" no longer accepts "title"; move authoring titles to the studio document.`,
328
+ finalizerNodeId,
329
+ path: `config.sections.${k}.title`,
330
+ });
331
+ }
332
+ }
333
+ const collectTags = cfg.collectEpistemicTags;
334
+ if (collectTags !== undefined && typeof collectTags !== 'boolean') {
291
335
  throw createFinalizerError({
292
336
  code: 'GRAPH_FINALIZER_INVALID',
293
- message: `report-schema aggregate finalizer config.meta must be an object when provided`,
337
+ message: `report-schema aggregate finalizer config.collectEpistemicTags must be boolean when provided`,
294
338
  finalizerNodeId,
295
- path: 'config.meta',
339
+ path: 'config.collectEpistemicTags',
296
340
  });
297
341
  }
298
342
  return;
@@ -36,5 +36,9 @@ export type EngineModelPhase = 'pre' | 'main' | 'post';
36
36
  /**
37
37
  * Maps three-phase graph config to ai-tasks slot pair for a single runTask phase.
38
38
  * Values are forwarded as-is (profile aliases or concrete ids).
39
+ *
40
+ * When {@link aiProfiles} is set (studio/playground host pattern), xynthesis slots use profile
41
+ * aliases so PRE synthesis inside ai-tasks resolves via ai-profiles even when {@link config}
42
+ * carries host-resolved concrete provider ids on `runtime.modelConfig`.
39
43
  */
40
- export declare function toRunTaskModelConfigForPhase(config: GraphAiModelConfig, phase: EngineModelPhase): RunTaskModelConfigWire;
44
+ export declare function toRunTaskModelConfigForPhase(config: GraphAiModelConfig, phase: EngineModelPhase, aiProfiles?: GraphAiModelConfig): RunTaskModelConfigWire;
@@ -79,26 +79,37 @@ export async function resolveGraphAiModelConfig(modelConfig, context) {
79
79
  postActionModel: normalizeModelSlot(modelConfig.postActionModel, context),
80
80
  };
81
81
  }
82
+ function xynthesisSlotForWire(config, phase, aiProfiles) {
83
+ if (aiProfiles != null) {
84
+ return phase === 'post' ? aiProfiles.postActionModel : aiProfiles.preActionModel;
85
+ }
86
+ return phase === 'post' ? config.postActionModel : config.preActionModel;
87
+ }
82
88
  /**
83
89
  * Maps three-phase graph config to ai-tasks slot pair for a single runTask phase.
84
90
  * Values are forwarded as-is (profile aliases or concrete ids).
91
+ *
92
+ * When {@link aiProfiles} is set (studio/playground host pattern), xynthesis slots use profile
93
+ * aliases so PRE synthesis inside ai-tasks resolves via ai-profiles even when {@link config}
94
+ * carries host-resolved concrete provider ids on `runtime.modelConfig`.
85
95
  */
86
- export function toRunTaskModelConfigForPhase(config, phase) {
96
+ export function toRunTaskModelConfigForPhase(config, phase, aiProfiles) {
97
+ const xynthesisModel = xynthesisSlotForWire(config, phase, aiProfiles);
87
98
  switch (phase) {
88
99
  case 'pre':
89
100
  return {
90
- xynthesisModel: config.preActionModel,
91
- skillModel: config.preActionModel,
101
+ xynthesisModel,
102
+ skillModel: xynthesisModel,
92
103
  };
93
104
  case 'main':
94
105
  return {
95
- xynthesisModel: config.preActionModel,
106
+ xynthesisModel,
96
107
  skillModel: config.skillModel,
97
108
  };
98
109
  case 'post':
99
110
  return {
100
- xynthesisModel: config.postActionModel,
101
- skillModel: config.postActionModel,
111
+ xynthesisModel,
112
+ skillModel: xynthesisModel,
102
113
  };
103
114
  }
104
115
  }
@@ -0,0 +1,43 @@
1
+ import type { Graph } from '../types/refs.js';
2
+ /** Example payloads and playground request samples — studio DB only, not graph model. */
3
+ export declare const GRAPH_ENTRY_STUDIO_ONLY_KEYS: readonly ["exampleInput", "exampleInputs", "requestExample"];
4
+ /** Planning / UI metadata — studio DB only, not versioned executable graph model. */
5
+ export declare const GRAPH_METADATA_STUDIO_ONLY_KEYS: readonly ["requestId", "graphConcept", "graphsStudio"];
6
+ export type GraphEntryStudioOnlyKey = (typeof GRAPH_ENTRY_STUDIO_ONLY_KEYS)[number];
7
+ export type GraphMetadataStudioOnlyKey = (typeof GRAPH_METADATA_STUDIO_ONLY_KEYS)[number];
8
+ /**
9
+ * Studio-side companion document keyed by graph id (graphs-studio database).
10
+ * Not part of {@link GraphModelObject} / canonical graph JSON.
11
+ */
12
+ export type GraphStudioDocument = {
13
+ graphId: string;
14
+ graphConcept?: Record<string, unknown>;
15
+ graphsStudio?: Record<string, unknown>;
16
+ /** Flat {@link GraphRuntimeObject.input} examples for playground and tests. */
17
+ exampleInputs?: Array<{
18
+ title?: string;
19
+ runtimeInput: Record<string, unknown>;
20
+ }>;
21
+ [key: string]: unknown;
22
+ };
23
+ export declare function getGraphEntryStudioOnlyKeyViolations(graphEntry: unknown): GraphEntryStudioOnlyKey[];
24
+ export declare function getGraphMetadataStudioOnlyKeyViolations(metadata: unknown): GraphMetadataStudioOnlyKey[];
25
+ /** Returns dot-paths under graphEntry.inputs with empty `path` for value kinds. */
26
+ export declare function getGraphEntryEmptyInputPathViolations(graphEntry: unknown): string[];
27
+ /**
28
+ * Deep-clones a graph model and removes studio-only metadata and graphEntry example fields.
29
+ * Use at publish time when upstream serializers still attach playground artifacts.
30
+ */
31
+ export declare function stripGraphModelStudioFields(graph: Graph): Graph;
32
+ export declare function assertCanonicalGraphEntryContract(graphEntry: unknown, context: {
33
+ jobId?: string;
34
+ graphId?: string;
35
+ graphLabel?: string;
36
+ }): void;
37
+ export declare function assertCanonicalGraphDocumentMetadata(metadata: unknown, context: {
38
+ jobId?: string;
39
+ graphId?: string;
40
+ graphLabel?: string;
41
+ }): void;
42
+ /** Primary flat runtime input from a studio companion document. */
43
+ export declare function primaryRuntimeInputFromStudioDocument(studio: GraphStudioDocument | undefined): Record<string, unknown> | undefined;
@@ -0,0 +1,102 @@
1
+ import { ExellixGraphError } from '../errors/ExellixGraphError.js';
2
+ import { ExellixGraphErrorCode } from '../errors/exellixGraphErrorCodes.js';
3
+ /** Example payloads and playground request samples — studio DB only, not graph model. */
4
+ export const GRAPH_ENTRY_STUDIO_ONLY_KEYS = [
5
+ 'exampleInput',
6
+ 'exampleInputs',
7
+ 'requestExample',
8
+ ];
9
+ /** Planning / UI metadata — studio DB only, not versioned executable graph model. */
10
+ export const GRAPH_METADATA_STUDIO_ONLY_KEYS = [
11
+ 'requestId',
12
+ 'graphConcept',
13
+ 'graphsStudio',
14
+ ];
15
+ function isPlainObject(v) {
16
+ return v != null && typeof v === 'object' && !Array.isArray(v);
17
+ }
18
+ function isValueInputSpec(spec) {
19
+ return spec.kind !== 'execution';
20
+ }
21
+ export function getGraphEntryStudioOnlyKeyViolations(graphEntry) {
22
+ if (!isPlainObject(graphEntry))
23
+ return [];
24
+ return GRAPH_ENTRY_STUDIO_ONLY_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(graphEntry, key));
25
+ }
26
+ export function getGraphMetadataStudioOnlyKeyViolations(metadata) {
27
+ if (!isPlainObject(metadata))
28
+ return [];
29
+ return GRAPH_METADATA_STUDIO_ONLY_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(metadata, key));
30
+ }
31
+ /** Returns dot-paths under graphEntry.inputs with empty `path` for value kinds. */
32
+ export function getGraphEntryEmptyInputPathViolations(graphEntry) {
33
+ if (!isPlainObject(graphEntry))
34
+ return [];
35
+ const inputs = graphEntry.inputs;
36
+ if (!Array.isArray(inputs))
37
+ return [];
38
+ const bad = [];
39
+ for (let i = 0; i < inputs.length; i++) {
40
+ const spec = inputs[i];
41
+ if (!isPlainObject(spec))
42
+ continue;
43
+ if (spec.kind === 'execution')
44
+ continue;
45
+ const path = typeof spec.path === 'string' ? spec.path.trim() : '';
46
+ if (!path)
47
+ bad.push(`metadata.graphEntry.inputs[${i}].path`);
48
+ }
49
+ return bad;
50
+ }
51
+ /**
52
+ * Deep-clones a graph model and removes studio-only metadata and graphEntry example fields.
53
+ * Use at publish time when upstream serializers still attach playground artifacts.
54
+ */
55
+ export function stripGraphModelStudioFields(graph) {
56
+ const clone = structuredClone(graph);
57
+ if (isPlainObject(clone.metadata)) {
58
+ const meta = { ...clone.metadata };
59
+ for (const key of GRAPH_METADATA_STUDIO_ONLY_KEYS) {
60
+ delete meta[key];
61
+ }
62
+ if (isPlainObject(meta.graphEntry)) {
63
+ const ge = { ...meta.graphEntry };
64
+ for (const key of GRAPH_ENTRY_STUDIO_ONLY_KEYS) {
65
+ delete ge[key];
66
+ }
67
+ meta.graphEntry = ge;
68
+ }
69
+ clone.metadata = meta;
70
+ }
71
+ return clone;
72
+ }
73
+ export function assertCanonicalGraphEntryContract(graphEntry, context) {
74
+ const label = context.graphLabel ?? `Graph "${String(context.graphId ?? '?')}"`;
75
+ const studioKeys = getGraphEntryStudioOnlyKeyViolations(graphEntry);
76
+ if (studioKeys.length > 0) {
77
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `${label}: metadata.graphEntry must not include studio-only example field(s): ${studioKeys.join(', ')}. Store example payloads in the graphs-studio database (GraphStudioDocument.exampleInputs), not in the executable graph model.`, { ...context, graphEntryStudioOnlyKeys: studioKeys });
78
+ }
79
+ const emptyPaths = getGraphEntryEmptyInputPathViolations(graphEntry);
80
+ if (emptyPaths.length > 0) {
81
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `${label}: metadata.graphEntry.inputs must declare a non-empty dot-path under merged execution (e.g. "input" or "input.subnetId"); empty path at ${emptyPaths.join(', ')}.`, { ...context, graphEntryEmptyInputPaths: emptyPaths });
82
+ }
83
+ }
84
+ export function assertCanonicalGraphDocumentMetadata(metadata, context) {
85
+ const label = context.graphLabel ?? `Graph "${String(context.graphId ?? '?')}"`;
86
+ const studioKeys = getGraphMetadataStudioOnlyKeyViolations(metadata);
87
+ if (studioKeys.length > 0) {
88
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `${label}: metadata must not include studio-only field(s): ${studioKeys.join(', ')}. Move planning and UI metadata to the graphs-studio database (GraphStudioDocument), not the executable graph model.`, { ...context, metadataStudioOnlyKeys: studioKeys });
89
+ }
90
+ if (isPlainObject(metadata) && metadata.graphEntry != null) {
91
+ assertCanonicalGraphEntryContract(metadata.graphEntry, context);
92
+ }
93
+ }
94
+ /** Primary flat runtime input from a studio companion document. */
95
+ export function primaryRuntimeInputFromStudioDocument(studio) {
96
+ const first = studio?.exampleInputs?.[0];
97
+ const runtimeInput = first?.runtimeInput;
98
+ if (runtimeInput != null && isPlainObject(runtimeInput) && !Array.isArray(runtimeInput)) {
99
+ return runtimeInput;
100
+ }
101
+ return undefined;
102
+ }
@@ -16,6 +16,7 @@ export function mergeExellixGraphRuntimeInvocation(input, opts) {
16
16
  playgroundMeta: input.playgroundMeta ?? opts.playgroundMeta,
17
17
  clearSynthesizedContextPerNode: input.clearSynthesizedContextPerNode ?? opts.clearSynthesizedContextPerNode,
18
18
  modelConfig: input.modelConfig ?? opts.modelConfig,
19
+ aiProfiles: input.aiProfiles ?? opts.aiProfiles,
19
20
  aliasConfig: input.aliasConfig ?? opts.aliasConfig,
20
21
  nodes: input.nodes ?? opts.nodes,
21
22
  llmCall: input.llmCall ?? opts.llmCall,
@@ -5,6 +5,7 @@ import { EXELLIX_VIRTUAL_BOUNDARY_NODE_METADATA_KEY } from '../inspection/types.
5
5
  import { getStructuredDataFilterPathViolations } from './dataFiltersEvaluation.js';
6
6
  import { conditionWhenSignature, countDefaultModelConfigCases, isEmptyConditionWhen, isGraphAiModelConfig, isModelConfigSelection, } from './modelConfigSelection.js';
7
7
  import { assertGraphAiProfileNameString } from './graphAiModelConfig.js';
8
+ import { assertCanonicalGraphDocumentMetadata } from './graphModelStudioSeparation.js';
8
9
  /** Top-level keys permitted on a canonical exellix-graph executable graph JSON document. */
9
10
  export const CANONICAL_GRAPH_TOP_LEVEL_KEYS = [
10
11
  'id',
@@ -548,6 +549,10 @@ export function assertCanonicalGraphDocument(graph, context) {
548
549
  throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `Graph "${String(resolvedGraphId ?? '?')}": metadata.${key} is not part of the static metadata block. Move this field to \`${target}\`.`, { jobId: context?.jobId, graphId: resolvedGraphId, metadataKey: key });
549
550
  }
550
551
  }
552
+ assertCanonicalGraphDocumentMetadata(metadata, {
553
+ jobId: context?.jobId,
554
+ graphId: resolvedGraphId,
555
+ });
551
556
  const graphEntry = metadata.graphEntry;
552
557
  if (graphEntry != null && typeof graphEntry === 'object' && !Array.isArray(graphEntry)) {
553
558
  const ge = graphEntry;
@@ -1,7 +1,7 @@
1
1
  import type { Activix } from '@x12i/activix';
2
2
  import type { StackLoggingOptions } from '@x12i/logxer';
3
3
  import type { LlmCallConfig, RunTaskRequest as AiTasksRunTaskRequest, RunTaskResponse as AiTasksRunTaskResponse } from '@exellix/ai-tasks';
4
- import type { GraphModelAliasConfig, ModelConfigSelection, TaskNodeRuntimeObject } from './refs.js';
4
+ import type { GraphAiModelConfig, GraphModelAliasConfig, ModelConfigSelection, TaskNodeRuntimeObject } from './refs.js';
5
5
  import type { RuntimeObjects } from '../runtime/runtimeObjects.js';
6
6
  /**
7
7
  * NOTE: `@exellix/ai-tasks` 5.5+ removed the exported `RunTaskDiagnostics` type.
@@ -63,6 +63,11 @@ export interface HostExecuteGraphRunOptions {
63
63
  playgroundMeta?: Record<string, unknown>;
64
64
  clearSynthesizedContextPerNode?: boolean;
65
65
  modelConfig?: ModelConfigSelection;
66
+ /**
67
+ * Profile aliases for xynthesis PRE/POST when {@link modelConfig} carries host-resolved concrete ids.
68
+ * MAIN `skillModel` still comes from resolved `modelConfig`; xynthesis slots prefer these aliases.
69
+ */
70
+ aiProfiles?: GraphAiModelConfig;
66
71
  aliasConfig?: GraphModelAliasConfig;
67
72
  nodes?: Record<string, TaskNodeRuntimeObject>;
68
73
  llmCall?: LlmCallConfig;
@@ -207,14 +207,28 @@ export type QuestionDrivenItemSpec = {
207
207
  */
208
208
  optional?: boolean;
209
209
  };
210
- export type ReportSchemaSectionSpec = {
211
- /** Dot-path in executionMemory to pull the section value from. */
210
+ /**
211
+ * A single "read one value from run memory" reference. Same idiom as
212
+ * {@link FinalizerInputBinding} (minus `literal`) and {@link GraphResponseSelector}
213
+ * memory selectors — finalizer configs reuse it so every memory read in the model
214
+ * looks identical: `{ type, path }`.
215
+ */
216
+ export type FinalizerMemoryRef = {
217
+ type: 'executionMemoryPath';
218
+ path: string;
219
+ optional?: boolean;
220
+ } | {
221
+ type: 'outputsMemoryPath';
212
222
  path: string;
213
- /** Human-readable title for this section. */
214
- title?: string;
215
223
  optional?: boolean;
216
224
  };
225
+ /**
226
+ * @deprecated Renamed and unified with {@link FinalizerMemoryRef}. Sections are now
227
+ * `Record<outputKey, FinalizerMemoryRef>` instead of `{ path, title }`.
228
+ */
229
+ export type ReportSchemaSectionSpec = FinalizerMemoryRef;
217
230
  export type AggregateFinalizerConfig = {
231
+ /** Build the output object field-by-field from named finalizer `inputs`. */
218
232
  strategy: 'object-map';
219
233
  map: Record<string, AggregateExpr>;
220
234
  omitUndefined?: boolean;
@@ -228,25 +242,23 @@ export type AggregateFinalizerConfig = {
228
242
  contractVersion?: string;
229
243
  /** Map output keys → node specs. */
230
244
  items: Record<string, QuestionDrivenItemSpec>;
231
- /** Optional extra metadata to include at top-level from executionMemory or outputsMemory. */
232
- meta?: Record<string, {
233
- type: 'executionMemoryPath' | 'outputsMemoryPath';
234
- path: string;
235
- optional?: boolean;
236
- }>;
245
+ /** Optional extra top-level fields, each read from run memory. */
246
+ meta?: Record<string, FinalizerMemoryRef>;
237
247
  } | {
238
248
  /**
239
- * Assemble a structured multi-section report from executionMemory.
240
- * Each section key maps to a dot-path. collect_tags gathers all
241
- * epistemic tags across sections.
249
+ * Build the graph output object by reading one run-memory value per output key.
250
+ * This **is** the finalizer's output: `sections` maps each output field to the
251
+ * memory path it is read from. (Authoring titles belong in the studio document,
252
+ * not here.)
242
253
  */
243
254
  strategy: 'report-schema';
244
- /** Map of output key → section spec (path + optional title). */
245
- sections: Record<string, ReportSchemaSectionSpec>;
246
- /** When true, scan all section values for CONFIRMED/INFERRED/ASSUMED/UNKNOWN and emit collected_tags. */
247
- collect_tags?: boolean;
248
- /** Optional literal fields merged at top level. */
249
- meta?: Record<string, unknown>;
255
+ /** Output field key → where its value is read from in run memory. */
256
+ sections: Record<string, FinalizerMemoryRef>;
257
+ /**
258
+ * When true, scan all section values for CONFIRMED/INFERRED/ASSUMED/UNKNOWN
259
+ * and emit them under the `collectedTags` output field.
260
+ */
261
+ collectEpistemicTags?: boolean;
250
262
  };
251
263
  export type BundleFinalizerConfig = {
252
264
  strategy: 'bundle';
@@ -275,8 +287,8 @@ export interface FinalizerNode {
275
287
  type: 'finalizer';
276
288
  finalizerType: 'aggregate' | 'compose' | 'select' | 'bundle' | 'synthesize';
277
289
  inputs: Record<string, FinalizerInputBinding>;
278
- /** Transitional: runtime validation narrows by finalizerType; implemented finalizers export typed configs. */
279
- config: AggregateFinalizerConfig | BundleFinalizerConfig | SelectFinalizerConfig | SynthesizeFinalizerConfig | Record<string, unknown>;
290
+ /** Strict per-`finalizerType` config. The runtime validator rejects unknown keys and legacy field names. */
291
+ config: AggregateFinalizerConfig | BundleFinalizerConfig | SelectFinalizerConfig | SynthesizeFinalizerConfig;
280
292
  /** Writes selected finalizer result fields into graph-owned outputsMemory for final response assembly. */
281
293
  outputMapping?: TaskNodeResultMapping;
282
294
  outputSchema?: OutputSchema;
@@ -612,7 +624,6 @@ export type GraphEntryContract = {
612
624
  * Optional JSON Schema (e.g. draft 2020-12) for the merged `execution` object. Validators may use it; core runtime does not.
613
625
  */
614
626
  executionSchema?: Record<string, unknown>;
615
- [key: string]: unknown;
616
627
  };
617
628
  /**
618
629
  * @deprecated Use root-level {@link GraphModelObject.response}. The final response shape is executable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exellix/graph-engine",
3
- "version": "7.4.2",
3
+ "version": "7.5.0",
4
4
  "type": "module",
5
5
  "description": "Graph executor SDK",
6
6
  "main": "dist/src/index.js",
@@ -28,6 +28,8 @@
28
28
  "test": "npm run build && npm run check:no-legacy && tsx --test --test-force-exit tests/model-alias-contract.test.ts tests/reports-fixtures-pre-synthesis.test.ts tests/passthrough-parity.test.ts tests/step-retry-llm-call.test.ts tests/run-log-diagnostics.test.ts",
29
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 tests/step-retry-llm-call.test.ts",
30
30
  "test:live": "npm run run:pre-synthesis",
31
+ "test:subnets-graph-fixture": "npm run build && node --test tests/subnets-graph.fixture.test.mjs",
32
+ "test:subnets-graph-live": "npm run build && node --env-file=.env --test tests/subnets-graph.live.test.mjs",
31
33
  "run:pre-synthesis": "npm run build && node --env-file=.env scripts/run-pre-synthesis-graph.mjs",
32
34
  "check:no-legacy": "node scripts/check-no-legacy.mjs",
33
35
  "lint": "eslint src testkit --ext .ts",