@exellix/graph-engine 8.7.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/src/compile/compileExellixExecutablePlan.d.ts +4 -1
- package/dist/src/compile/compileExellixExecutablePlan.js +18 -0
- package/dist/src/contract/graphRunContract.js +3 -6
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.js +4 -0
- package/dist/src/inspection/graphInspection.js +4 -0
- package/dist/src/inspection/types.d.ts +5 -0
- package/dist/src/plan/embeddedGraphToExellixGraph.js +25 -8
- package/dist/src/runtime/ExellixGraphRuntime.d.ts +2 -2
- package/dist/src/runtime/ExellixGraphRuntime.js +52 -19
- package/dist/src/runtime/coerceStringArray.d.ts +5 -0
- package/dist/src/runtime/coerceStringArray.js +34 -0
- package/dist/src/runtime/graphResponseMapping.d.ts +11 -0
- package/dist/src/runtime/graphResponseMapping.js +61 -8
- package/dist/src/runtime/validateCanonicalGraphDocument.js +26 -1
- package/dist/src/types/refs.d.ts +13 -8
- package/dist/src/validation/authoringGraphResponse.d.ts +21 -0
- package/dist/src/validation/authoringGraphResponse.js +88 -0
- package/dist/src/validation/validateGraphResponseWiring.d.ts +26 -0
- package/dist/src/validation/validateGraphResponseWiring.js +233 -0
- package/dist/testkit/authoringGraphFixtures.js +4 -1
- package/dist/testkit/buildExecuteGraphInput.js +3 -1
- package/dist/testkit/flatGraphToAuthoring.js +5 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.0.0 — Response contract & finalOutput (FR-GE / CR-GE pack)
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **Single canonical response:** Executable mapping reads **`graph.response` only** (authoring: `graph.response`; flat model: root `response`). `metadata.graphResponse` is rejected as executable truth (`GRAPH_RESPONSE_LEGACY_SOURCE`).
|
|
8
|
+
- **`validateGraphResponseWiring`** runs on compile by default (`strictResponseValidation !== false`): path writers, flat map targets, legacy paths/shapes, empty shape + finalizer.
|
|
9
|
+
- **`GRAPH_RESPONSE_INCOMPLETE`** when non-empty `graph.response.shape` resolves empty on a completed run.
|
|
10
|
+
- **Legacy removed:** `inference.conceptSketch.*` execution paths, `subnetAnalysis` / top-level `taskSections` shape keys, `metadata.responsePreset.id === 'subnetAnalysis'`, task `outputMapping`.
|
|
11
|
+
- **`resolveGraphRunContract`** reads **`graph.response.persistency` only** (no `metadata.graphResponse.persistency` fallback).
|
|
12
|
+
- **`ExecuteGraphResult.execution`** is always present on graph runs.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`coerce: 'stringArray'`** on response selectors; **`coerceStringArray`**, **`resolveGraphResponse`**, **`assessGraphResponseCompleteness`** exports.
|
|
17
|
+
- **`migrateLegacyGraphResponseToAuthoring`** (import-only).
|
|
18
|
+
- **`inspectGraph`**: `response.source === 'graph.response'`.
|
|
19
|
+
|
|
20
|
+
### Downstream
|
|
21
|
+
|
|
22
|
+
- **graphs-playground:** remove `normalizeQaAnswersArrayFinalOutput`, `normalizeTaskSectionsFinalOutput`, `coerceResponsePresetTaggedGraph`, `buildTaskSectionsFinalOutputFromAnswers`; add `coerce: 'stringArray'` in studio preset builders at save time.
|
|
23
|
+
|
|
3
24
|
## 8.6.0 — Graphenix 2.7.3 (no-legacy web + narrix pipeline)
|
|
4
25
|
|
|
5
26
|
### Breaking
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { AuthoringGraphDocument, CompileExecutablePlanV2Options, ExecutableGraphPlanV2 } from '@x12i/graphenix-executable-contracts';
|
|
2
2
|
import type { GraphRuntimeObject } from '../runtime/ExellixGraphRuntime.js';
|
|
3
|
-
export type CompileExellixExecutablePlanOptions = CompileExecutablePlanV2Options
|
|
3
|
+
export type CompileExellixExecutablePlanOptions = CompileExecutablePlanV2Options & {
|
|
4
|
+
/** When true (default in 9.x), reject unsatisfiable graph.response wiring before returning a plan. */
|
|
5
|
+
strictResponseValidation?: boolean;
|
|
6
|
+
};
|
|
4
7
|
/**
|
|
5
8
|
* Host/test helper: canonical {@link AuthoringGraphDocument} + runtime → validated v2 executable plan.
|
|
6
9
|
* No legacy migration — graphs must already be Graphenix 2.x authoring shape.
|
|
@@ -3,6 +3,12 @@ import { compileExecutablePlanV2 } from '@x12i/graphenix-plan-compiler';
|
|
|
3
3
|
import { validateExecutablePlanV2 } from '@x12i/graphenix-plan-format';
|
|
4
4
|
import { resolveGraphEntryFromAuthoringDocument } from './authoringDocumentHelpers.js';
|
|
5
5
|
import { EXELLIX_STRUCTURED_DATA_FILTERS_V1 } from '../types/refs.js';
|
|
6
|
+
import { assertGraphResponseWiringOk, validateGraphResponseWiring, } from '../validation/validateGraphResponseWiring.js';
|
|
7
|
+
import { resolveAuthoringGraphResponse } from '../validation/authoringGraphResponse.js';
|
|
8
|
+
import { assertGraphResponseDefinition } from '../runtime/graphResponseMapping.js';
|
|
9
|
+
function isPlainRecord(v) {
|
|
10
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
11
|
+
}
|
|
6
12
|
function patchEntryGatesFromGraphEntryDataFilters(plan, doc) {
|
|
7
13
|
const graphEntry = resolveGraphEntryFromAuthoringDocument(doc);
|
|
8
14
|
const dataFilters = graphEntry?.dataFilters;
|
|
@@ -30,6 +36,14 @@ function patchEntryGatesFromGraphEntryDataFilters(plan, doc) {
|
|
|
30
36
|
* No legacy migration — graphs must already be Graphenix 2.x authoring shape.
|
|
31
37
|
*/
|
|
32
38
|
export function compileExellixExecutablePlan(doc, runtime, options) {
|
|
39
|
+
const strictResponseValidation = options?.strictResponseValidation !== false;
|
|
40
|
+
// Ensure canonical response is present on authoring graph before plan compile (FR-GE-001).
|
|
41
|
+
const resolvedResponse = resolveAuthoringGraphResponse(doc);
|
|
42
|
+
assertGraphResponseDefinition(resolvedResponse);
|
|
43
|
+
const graphRecord = doc.graph;
|
|
44
|
+
if (!isPlainRecord(graphRecord.response)) {
|
|
45
|
+
graphRecord.response = resolvedResponse;
|
|
46
|
+
}
|
|
33
47
|
const authoringValidation = validateAuthoringGraph(doc);
|
|
34
48
|
if (!authoringValidation.valid) {
|
|
35
49
|
const summary = authoringValidation.errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
@@ -41,5 +55,9 @@ export function compileExellixExecutablePlan(doc, runtime, options) {
|
|
|
41
55
|
const summary = planValidation.errors.map((e) => `${e.path}: ${e.message}`).join('; ');
|
|
42
56
|
throw new Error(`compileExellixExecutablePlan: invalid plan: ${summary}`);
|
|
43
57
|
}
|
|
58
|
+
if (strictResponseValidation) {
|
|
59
|
+
const planWiring = validateGraphResponseWiring(plan);
|
|
60
|
+
assertGraphResponseWiringOk(planWiring, { graphId: plan.source.graphId });
|
|
61
|
+
}
|
|
44
62
|
return plan;
|
|
45
63
|
}
|
|
@@ -65,6 +65,9 @@ export function mergeGraphRunPersistency(base, override) {
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
function mergeRawPersistency(base, rawOverride) {
|
|
68
|
+
// Explicit null on a run/work/job disables result writeback (simulation).
|
|
69
|
+
if (rawOverride === null)
|
|
70
|
+
return null;
|
|
68
71
|
const overrideRec = asRecord(rawOverride);
|
|
69
72
|
if (!overrideRec)
|
|
70
73
|
return base;
|
|
@@ -98,12 +101,6 @@ function readPersistency(graph) {
|
|
|
98
101
|
if (fromResponse) {
|
|
99
102
|
return normalizePersistency(fromResponse);
|
|
100
103
|
}
|
|
101
|
-
const metadata = asRecord(graph.metadata);
|
|
102
|
-
const graphResponse = asRecord(metadata?.graphResponse);
|
|
103
|
-
const legacy = asRecord(graphResponse?.persistency);
|
|
104
|
-
if (legacy) {
|
|
105
|
-
return normalizePersistency(legacy);
|
|
106
|
-
}
|
|
107
104
|
return null;
|
|
108
105
|
}
|
|
109
106
|
/** Read graph document version from published graph JSON (`version` or `metadata.version`). */
|
package/dist/src/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type { HostExecuteGraphRunOptions, MainReadinessPolicy, ExecutionStepOpti
|
|
|
16
16
|
export type { AiTaskProfileMetadata, AiTaskProfileInputSynthesis, } from './types/aiTaskProfile.js';
|
|
17
17
|
export { hasWebScopeAuthoring } from './types/aiTaskProfile.js';
|
|
18
18
|
export type { ExecutionStrategyInvocation, ExecutionStrategyPhase, ExecutionStrategyWrapperKey, SmartInputConfig, TaskStrategyItemData, XynthesizedDestinationScope, XynthesizedMemory, XynthesizedOutputConfig, } from './types/aiTasksDerivedTypes.js';
|
|
19
|
-
export type { Graph, GraphModelObject, GraphAiModelConfig, PartialGraphAiModelConfig, GraphModelAliasConfig, GraphRuntimeNodeConfig, TaskNodeRuntimeObject, GraphDocumentMetadata, GraphEntryContract, 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
|
+
export type { Graph, GraphModelObject, GraphAiModelConfig, PartialGraphAiModelConfig, GraphModelAliasConfig, GraphRuntimeNodeConfig, TaskNodeRuntimeObject, GraphDocumentMetadata, GraphEntryContract, GraphResponseDefinition, GraphResponseMissingBehavior, GraphResponseSelector, GraphResponseCoercion, 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';
|
|
20
20
|
export { mergeGraphDocumentModel, EXELLIX_GRAPH_MODEL_VARIABLE_KEY, EXELLIX_STRUCTURED_DATA_FILTERS_V1, } from './types/refs.js';
|
|
21
21
|
export type { TaskNodeTaskConfiguration, TaskNodeScopedDataReaderPackSlot, } from './types/taskNodeConfiguration.js';
|
|
22
22
|
export { getTaskConfiguration } from './types/taskNodeConfiguration.js';
|
|
@@ -95,6 +95,13 @@ export type { ExecutableGraphPlanV2, NodeExecutionPlan, ExecutionUnitPlanV2, } f
|
|
|
95
95
|
export type { AuthoringGraphDocument } from '@x12i/graphenix-executable-contracts';
|
|
96
96
|
export { compileExellixExecutablePlan } from './compile/compileExellixExecutablePlan.js';
|
|
97
97
|
export type { CompileExellixExecutablePlanOptions } from './compile/compileExellixExecutablePlan.js';
|
|
98
|
+
export { applyGraphResponseDefinition, resolveGraphResponse, assessGraphResponseCompleteness, GRAPH_RESPONSE_INCOMPLETE, } from './runtime/graphResponseMapping.js';
|
|
99
|
+
export type { GraphResponseMappingContext, GraphResponseMappingError } from './runtime/graphResponseMapping.js';
|
|
100
|
+
export { coerceStringArray } from './runtime/coerceStringArray.js';
|
|
101
|
+
export { validateGraphResponseWiring, assertGraphResponseWiringOk, RESPONSE_PATH_NO_WRITER, TASK_MAP_TARGET_INVALID, TASK_MAP_SOURCE_INVALID, RESPONSE_LEGACY_SHAPE_KEY, EXECUTION_MAPPING_LEGACY_PATH, RESPONSE_EMPTY_SHAPE, RESPONSE_LEGACY_PRESET_ID, RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR, } from './validation/validateGraphResponseWiring.js';
|
|
102
|
+
export type { GraphResponseWiringIssue, GraphResponseWiringValidation, } from './validation/validateGraphResponseWiring.js';
|
|
103
|
+
export { detectAuthoringResponseSourceIssues, resolveAuthoringGraphResponse, migrateLegacyGraphResponseToAuthoring, GRAPH_RESPONSE_LEGACY_SOURCE, GRAPH_RESPONSE_DUAL_SOURCE, } from './validation/authoringGraphResponse.js';
|
|
104
|
+
export type { AuthoringResponseSourceIssue } from './validation/authoringGraphResponse.js';
|
|
98
105
|
export { buildGraphExecutionRequestFromStudioExecute as buildAuthoringStudioGraphExecutionRequest } from '@x12i/graphenix-execute-envelope';
|
|
99
106
|
export type { RunTaskRequest, RunTaskResponse, TasksClientLike, GraphLoader as RuntimeGraphLoader, ExecuteGraphInput, GraphExecutionRequest, GraphRuntimeObject, GraphKnowledgeResolver, GraphKnowledgeResolverContext, ExecuteGraphResult, ExellixGraphRuntimeOptions, } from './runtime/ExellixGraphRuntime.js';
|
|
100
107
|
export type { PlanStatus, GraphPlan, GraphEngine, GraphEngineFactory, } from './runtime/GraphEngine.js';
|
package/dist/src/index.js
CHANGED
|
@@ -69,6 +69,10 @@ export { computeGraphDocumentContentSha256, stableStringifyGraphDocument, } from
|
|
|
69
69
|
// New runtime with injection seam
|
|
70
70
|
export { createExellixGraphRuntime } from './runtime/ExellixGraphRuntime.js';
|
|
71
71
|
export { compileExellixExecutablePlan } from './compile/compileExellixExecutablePlan.js';
|
|
72
|
+
export { applyGraphResponseDefinition, resolveGraphResponse, assessGraphResponseCompleteness, GRAPH_RESPONSE_INCOMPLETE, } from './runtime/graphResponseMapping.js';
|
|
73
|
+
export { coerceStringArray } from './runtime/coerceStringArray.js';
|
|
74
|
+
export { validateGraphResponseWiring, assertGraphResponseWiringOk, RESPONSE_PATH_NO_WRITER, TASK_MAP_TARGET_INVALID, TASK_MAP_SOURCE_INVALID, RESPONSE_LEGACY_SHAPE_KEY, EXECUTION_MAPPING_LEGACY_PATH, RESPONSE_EMPTY_SHAPE, RESPONSE_LEGACY_PRESET_ID, RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR, } from './validation/validateGraphResponseWiring.js';
|
|
75
|
+
export { detectAuthoringResponseSourceIssues, resolveAuthoringGraphResponse, migrateLegacyGraphResponseToAuthoring, GRAPH_RESPONSE_LEGACY_SOURCE, GRAPH_RESPONSE_DUAL_SOURCE, } from './validation/authoringGraphResponse.js';
|
|
72
76
|
export { buildGraphExecutionRequestFromStudioExecute as buildAuthoringStudioGraphExecutionRequest } from '@x12i/graphenix-execute-envelope';
|
|
73
77
|
// Testkit (in-memory loader, dep engine, recording client — sample NARRIX tasks: `@exellix/graph-engine/testkit`)
|
|
74
78
|
export { InMemoryGraphLoader, DepGraphEngineFactory } from '../testkit/index.js';
|
|
@@ -264,6 +264,11 @@ export type GraphInspection = {
|
|
|
264
264
|
* Graph `metadata` (same shape as `variables.__graphModel` during execution).
|
|
265
265
|
*/
|
|
266
266
|
graphDocumentModel: GraphDocumentMetadata;
|
|
267
|
+
/** Executable response definition source — always `graph.response` in 9.x. */
|
|
268
|
+
response: {
|
|
269
|
+
source: 'graph.response';
|
|
270
|
+
definition: import('../types/refs.js').GraphResponseDefinition;
|
|
271
|
+
};
|
|
267
272
|
/**
|
|
268
273
|
* Virtual Layer 01 / 08 nodes and edges for visualization; not part of executable `nodes` / `edges`.
|
|
269
274
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getFinalizerNodeConfig, getTaskNodeConfig, isExecutableFinalizerNode, isExecutableTaskNode, } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import { GRAPH_RESPONSE_LEGACY_SOURCE } from '../validation/authoringGraphResponse.js';
|
|
2
3
|
function isPlainRecord(v) {
|
|
3
4
|
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
4
5
|
}
|
|
@@ -82,24 +83,40 @@ function edgesFromPlan(plan) {
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
function responseFromEmbedded(doc, plan) {
|
|
85
|
-
const
|
|
86
|
-
|
|
86
|
+
const graphRecord = doc.graph;
|
|
87
|
+
const graphLevel = graphRecord.response;
|
|
88
|
+
if (isPlainRecord(graphLevel) && 'shape' in graphLevel) {
|
|
87
89
|
const out = {
|
|
88
|
-
shape: isPlainRecord(
|
|
90
|
+
shape: isPlainRecord(graphLevel.shape) || Array.isArray(graphLevel.shape)
|
|
91
|
+
? structuredClone(graphLevel.shape)
|
|
92
|
+
: graphLevel.shape ?? {},
|
|
89
93
|
};
|
|
90
|
-
if (
|
|
91
|
-
out.missing =
|
|
92
|
-
}
|
|
93
|
-
if (metaResponse.version !== undefined) {
|
|
94
|
-
out.version = metaResponse.version;
|
|
94
|
+
if (graphLevel.missing !== undefined) {
|
|
95
|
+
out.missing = graphLevel.missing;
|
|
95
96
|
}
|
|
96
97
|
return out;
|
|
97
98
|
}
|
|
99
|
+
const metaResponse = doc.graph.metadata?.graphResponse;
|
|
100
|
+
if (isPlainRecord(metaResponse) && hasExecutableMetadataResponse(metaResponse)) {
|
|
101
|
+
const err = new Error('Executable response mapping exists only under metadata.graphResponse. Move shape to graph.response — studio metadata is UI-only and is not read at compile or execute.');
|
|
102
|
+
err.code = GRAPH_RESPONSE_LEGACY_SOURCE;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
98
105
|
if (isPlainRecord(plan.contracts?.response?.finalOutputSchema)) {
|
|
99
106
|
return { shape: { type: 'literal', value: {} } };
|
|
100
107
|
}
|
|
101
108
|
return { shape: {} };
|
|
102
109
|
}
|
|
110
|
+
function hasExecutableMetadataResponse(metaResponse) {
|
|
111
|
+
const shape = metaResponse.shape;
|
|
112
|
+
if (shape === undefined)
|
|
113
|
+
return false;
|
|
114
|
+
if (isPlainRecord(shape))
|
|
115
|
+
return Object.keys(shape).length > 0;
|
|
116
|
+
if (Array.isArray(shape))
|
|
117
|
+
return shape.length > 0;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
103
120
|
/** Materialize exellix {@link Graph} from plan embedded normalized authoring graph. */
|
|
104
121
|
export function embeddedGraphToExellixGraph(plan) {
|
|
105
122
|
if (plan.normalizedGraph.mode !== 'embedded') {
|
|
@@ -100,8 +100,8 @@ export interface ExecuteGraphResult {
|
|
|
100
100
|
nodeId?: string;
|
|
101
101
|
error: any;
|
|
102
102
|
}>;
|
|
103
|
-
/** Final execution object (includes `_trace.nodes` for per-node timing). */
|
|
104
|
-
execution
|
|
103
|
+
/** Final execution object (includes `_trace.nodes` for per-node timing). Always present on graph runs (FR-GE-005). */
|
|
104
|
+
execution: Record<string, unknown>;
|
|
105
105
|
/** Final graph-engine-owned output memory accumulated from finalizer `outputMapping` writes. */
|
|
106
106
|
outputsMemory?: unknown;
|
|
107
107
|
runLog?: RunLogEntry[];
|
|
@@ -19,7 +19,7 @@ import { assertFinalizerRequiredReadsResolvable, validateGraphFinalizer, } from
|
|
|
19
19
|
import { executeDeterministicFinalizer, executeSynthesizeFinalizer } from "./finalizers/executeFinalizer.js";
|
|
20
20
|
import { createFinalizerError } from "./finalizers/errors.js";
|
|
21
21
|
import { assertCanonicalGraphRuntimeObject } from "./validateCanonicalGraphRuntime.js";
|
|
22
|
-
import { applyGraphResponseDefinition } from "./graphResponseMapping.js";
|
|
22
|
+
import { applyGraphResponseDefinition, assessGraphResponseCompleteness, GRAPH_RESPONSE_INCOMPLETE } from "./graphResponseMapping.js";
|
|
23
23
|
import { buildAiTasksObservabilityRecord } from "./aiTasksObservability.js";
|
|
24
24
|
import { buildRunTaskIdentityEnvelope, shouldForwardRunTaskTraceMode, } from "./runTaskAugments.js";
|
|
25
25
|
import { buildRunLog, extractLogxerCorrelationFromMetadata, extractTaskRunLogFromMetadata, resolveRunLogLimits, } from "./buildRunLog.js";
|
|
@@ -1172,7 +1172,7 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1172
1172
|
logxerCorrelationId: logxerCorrelationIdLast,
|
|
1173
1173
|
});
|
|
1174
1174
|
return {
|
|
1175
|
-
execution: currentExecution,
|
|
1175
|
+
execution: (isPlainRecord(currentExecution) ? currentExecution : {}),
|
|
1176
1176
|
outputsMemory: currentOutputsMemory,
|
|
1177
1177
|
...built,
|
|
1178
1178
|
};
|
|
@@ -1256,7 +1256,7 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1256
1256
|
const executionMemoryForResponse = isPlainRecord(currentExecution)
|
|
1257
1257
|
? structuredClone(currentExecution)
|
|
1258
1258
|
: currentExecution;
|
|
1259
|
-
|
|
1259
|
+
const finalOutput = applyGraphResponseDefinition({
|
|
1260
1260
|
response: graph.response,
|
|
1261
1261
|
context: {
|
|
1262
1262
|
graph,
|
|
@@ -1268,6 +1268,38 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1268
1268
|
stepsResponses,
|
|
1269
1269
|
},
|
|
1270
1270
|
});
|
|
1271
|
+
const completeness = assessGraphResponseCompleteness(graph.response, finalOutput);
|
|
1272
|
+
if (completeness.incomplete) {
|
|
1273
|
+
return {
|
|
1274
|
+
responseErrors: [
|
|
1275
|
+
{
|
|
1276
|
+
code: completeness.code ?? GRAPH_RESPONSE_INCOMPLETE,
|
|
1277
|
+
message: completeness.message ?? 'graph.response resolved empty.',
|
|
1278
|
+
},
|
|
1279
|
+
],
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
return finalOutput !== undefined ? { finalOutput } : {};
|
|
1283
|
+
}
|
|
1284
|
+
function resolveGraphRunFinalOutput(baseErrors = errors) {
|
|
1285
|
+
const resolved = buildFinalOutputFromGraphResponse();
|
|
1286
|
+
if (resolved.responseErrors?.length) {
|
|
1287
|
+
return {
|
|
1288
|
+
errors: [
|
|
1289
|
+
...baseErrors,
|
|
1290
|
+
...resolved.responseErrors.map((re) => {
|
|
1291
|
+
const e = new Error(re.message);
|
|
1292
|
+
e.code = re.code;
|
|
1293
|
+
return { error: e };
|
|
1294
|
+
}),
|
|
1295
|
+
],
|
|
1296
|
+
responseFailed: true,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
return {
|
|
1300
|
+
finalOutput: resolved.finalOutput,
|
|
1301
|
+
errors: baseErrors,
|
|
1302
|
+
};
|
|
1271
1303
|
}
|
|
1272
1304
|
const playgroundReporter = runtime.playgroundReporter ?? opts.playgroundReporter;
|
|
1273
1305
|
if (playgroundReporter) {
|
|
@@ -1314,7 +1346,7 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1314
1346
|
taskId: graphTaskId,
|
|
1315
1347
|
error: err,
|
|
1316
1348
|
});
|
|
1317
|
-
const
|
|
1349
|
+
const responseResolution = resolveGraphRunFinalOutput([...errors, { error: err }]);
|
|
1318
1350
|
appendExecutionEvent(executionTrace, {
|
|
1319
1351
|
id: `evt:${graphTaskId}:graph.failed`,
|
|
1320
1352
|
ts: new Date().toISOString(),
|
|
@@ -1330,8 +1362,8 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1330
1362
|
outputsByNodeId,
|
|
1331
1363
|
stepsResponses,
|
|
1332
1364
|
engineSnapshot: engine.snapshot(),
|
|
1333
|
-
...(finalOutput !== undefined ? { finalOutput } : {}),
|
|
1334
|
-
errors:
|
|
1365
|
+
...(responseResolution.finalOutput !== undefined ? { finalOutput: responseResolution.finalOutput } : {}),
|
|
1366
|
+
errors: responseResolution.errors,
|
|
1335
1367
|
planAudit,
|
|
1336
1368
|
trace: executionTrace,
|
|
1337
1369
|
...finalizeGraphPayload("failed"),
|
|
@@ -1350,8 +1382,8 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1350
1382
|
while (true) {
|
|
1351
1383
|
const wavePlan = engine.plan();
|
|
1352
1384
|
if (wavePlan.status === "completed") {
|
|
1353
|
-
const
|
|
1354
|
-
const
|
|
1385
|
+
const responseResolution = resolveGraphRunFinalOutput();
|
|
1386
|
+
const graphStatus = errors.length || responseResolution.responseFailed ? "failed" : "completed";
|
|
1355
1387
|
appendExecutionEvent(executionTrace, {
|
|
1356
1388
|
id: `evt:${graphTaskId}:graph.${graphStatus}`,
|
|
1357
1389
|
ts: new Date().toISOString(),
|
|
@@ -1360,6 +1392,7 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1360
1392
|
message: `Graph execution ${graphStatus}.`,
|
|
1361
1393
|
});
|
|
1362
1394
|
validateExecutionTrace(executionTrace, plan);
|
|
1395
|
+
const resultErrors = responseResolution.errors.length ? responseResolution.errors : undefined;
|
|
1363
1396
|
const result = {
|
|
1364
1397
|
jobId,
|
|
1365
1398
|
taskId: graphTaskId,
|
|
@@ -1368,9 +1401,9 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1368
1401
|
outputsByNodeId,
|
|
1369
1402
|
stepsResponses,
|
|
1370
1403
|
engineSnapshot: engine.snapshot(),
|
|
1371
|
-
...(finalOutput !== undefined ? { finalOutput } : {}),
|
|
1372
|
-
...(
|
|
1373
|
-
errors:
|
|
1404
|
+
...(responseResolution.finalOutput !== undefined ? { finalOutput: responseResolution.finalOutput } : {}),
|
|
1405
|
+
...(graphStatus === "completed" ? { finalizerNodeId, finalizerType: finalizer.finalizerType } : {}),
|
|
1406
|
+
errors: resultErrors,
|
|
1374
1407
|
planAudit,
|
|
1375
1408
|
trace: executionTrace,
|
|
1376
1409
|
...finalizeGraphPayload(graphStatus),
|
|
@@ -1381,14 +1414,14 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1381
1414
|
if (eventEmitter) {
|
|
1382
1415
|
if (graphStatus === "completed") {
|
|
1383
1416
|
eventEmitter.emit(createGraphCompleteEvent(jobId, resolvedGraphId, graphTaskId, {
|
|
1384
|
-
output: finalOutput,
|
|
1417
|
+
output: responseResolution.finalOutput,
|
|
1385
1418
|
nodesExecuted: Object.keys(outputsByNodeId).length,
|
|
1386
1419
|
finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
|
|
1387
1420
|
...(jobCorrelation ?? {}),
|
|
1388
1421
|
}));
|
|
1389
1422
|
}
|
|
1390
1423
|
else {
|
|
1391
|
-
eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, errors[0]?.error, {
|
|
1424
|
+
eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, responseResolution.errors[0]?.error ?? errors[0]?.error, {
|
|
1392
1425
|
finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
|
|
1393
1426
|
...(jobCorrelation ?? {}),
|
|
1394
1427
|
}));
|
|
@@ -1399,7 +1432,7 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1399
1432
|
if (wavePlan.status !== "continue") {
|
|
1400
1433
|
const err = new Error(`GRAPH_BLOCKED: status=${wavePlan.status}`);
|
|
1401
1434
|
err.code = "GRAPH_BLOCKED";
|
|
1402
|
-
const
|
|
1435
|
+
const responseResolution = resolveGraphRunFinalOutput([...errors, { error: err }]);
|
|
1403
1436
|
const result = {
|
|
1404
1437
|
jobId,
|
|
1405
1438
|
taskId: graphTaskId,
|
|
@@ -1408,8 +1441,8 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1408
1441
|
outputsByNodeId,
|
|
1409
1442
|
stepsResponses,
|
|
1410
1443
|
engineSnapshot: engine.snapshot(),
|
|
1411
|
-
...(finalOutput !== undefined ? { finalOutput } : {}),
|
|
1412
|
-
errors:
|
|
1444
|
+
...(responseResolution.finalOutput !== undefined ? { finalOutput: responseResolution.finalOutput } : {}),
|
|
1445
|
+
errors: responseResolution.errors,
|
|
1413
1446
|
planAudit,
|
|
1414
1447
|
trace: executionTrace,
|
|
1415
1448
|
...finalizeGraphPayload("failed"),
|
|
@@ -1651,7 +1684,7 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1651
1684
|
throw e;
|
|
1652
1685
|
});
|
|
1653
1686
|
if (failFast && errors.length) {
|
|
1654
|
-
const
|
|
1687
|
+
const responseResolution = resolveGraphRunFinalOutput();
|
|
1655
1688
|
const result = {
|
|
1656
1689
|
jobId,
|
|
1657
1690
|
taskId: graphTaskId,
|
|
@@ -1660,8 +1693,8 @@ export function createExellixGraphRuntime(opts) {
|
|
|
1660
1693
|
outputsByNodeId,
|
|
1661
1694
|
stepsResponses,
|
|
1662
1695
|
engineSnapshot: engine.snapshot(),
|
|
1663
|
-
...(finalOutput !== undefined ? { finalOutput } : {}),
|
|
1664
|
-
errors,
|
|
1696
|
+
...(responseResolution.finalOutput !== undefined ? { finalOutput: responseResolution.finalOutput } : {}),
|
|
1697
|
+
errors: responseResolution.errors,
|
|
1665
1698
|
planAudit,
|
|
1666
1699
|
trace: executionTrace,
|
|
1667
1700
|
...finalizeGraphPayload("failed"),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-only coercion for graph.response selectors with `coerce: 'stringArray'`.
|
|
3
|
+
* Same semantics as graphs-playground normalizeQaAnswersArrayFinalOutput field rules.
|
|
4
|
+
*/
|
|
5
|
+
export function coerceStringArray(value) {
|
|
6
|
+
if (value === null || value === undefined)
|
|
7
|
+
return value;
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value
|
|
10
|
+
.filter((item) => item != null)
|
|
11
|
+
.map((item) => String(item).trim())
|
|
12
|
+
.filter((item) => item.length > 0);
|
|
13
|
+
}
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return value;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (trimmed.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
if (trimmed.startsWith('[')) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(trimmed);
|
|
22
|
+
if (Array.isArray(parsed)) {
|
|
23
|
+
return parsed
|
|
24
|
+
.filter((item) => item != null)
|
|
25
|
+
.map((item) => String(item).trim())
|
|
26
|
+
.filter((item) => item.length > 0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// fall through to single-element wrap
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [trimmed];
|
|
34
|
+
}
|
|
@@ -18,6 +18,17 @@ export declare function applyGraphResponseDefinition(args: {
|
|
|
18
18
|
response: GraphResponseDefinition;
|
|
19
19
|
context: GraphResponseMappingContext;
|
|
20
20
|
}): unknown;
|
|
21
|
+
/** Resolves root `graph.response` against accumulated execution memory (FR-GE-002). */
|
|
22
|
+
export declare const resolveGraphResponse: typeof applyGraphResponseDefinition;
|
|
21
23
|
/** @deprecated Use applyGraphResponseDefinition with root-level graph.response. */
|
|
22
24
|
export declare const applyGraphResponseMapping: typeof applyGraphResponseDefinition;
|
|
25
|
+
export declare const GRAPH_RESPONSE_INCOMPLETE: "GRAPH_RESPONSE_INCOMPLETE";
|
|
26
|
+
/**
|
|
27
|
+
* When graph.response.shape is non-empty but resolution yields an empty object, surface FR-GE-006.
|
|
28
|
+
*/
|
|
29
|
+
export declare function assessGraphResponseCompleteness(response: GraphResponseDefinition, finalOutput: unknown): {
|
|
30
|
+
incomplete: boolean;
|
|
31
|
+
code?: typeof GRAPH_RESPONSE_INCOMPLETE;
|
|
32
|
+
message?: string;
|
|
33
|
+
};
|
|
23
34
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { coerceStringArray } from './coerceStringArray.js';
|
|
1
2
|
import { selectByPath } from './pathExpr.js';
|
|
2
3
|
import { readTaskNodeModelInputSurface, } from './readTaskNodeInputsConfig.js';
|
|
3
4
|
const GRAPH_RESPONSE_MAPPING_ERROR_CODE = 'GRAPH_RESPONSE_MAPPING_INVALID';
|
|
@@ -69,20 +70,31 @@ function materializeMissing(missing) {
|
|
|
69
70
|
function presentOrMissing(value, missing) {
|
|
70
71
|
return value === undefined || value === null ? materializeMissing(missing) : value;
|
|
71
72
|
}
|
|
73
|
+
function applySelectorCoercion(value, selector, path) {
|
|
74
|
+
const coerce = selector.coerce;
|
|
75
|
+
if (coerce === undefined)
|
|
76
|
+
return value;
|
|
77
|
+
if (coerce !== 'stringArray') {
|
|
78
|
+
throw createGraphResponseMappingError(`Unsupported graph.response selector coercion "${String(coerce)}".`, path, { coerce });
|
|
79
|
+
}
|
|
80
|
+
return coerceStringArray(value);
|
|
81
|
+
}
|
|
72
82
|
function resolveSelector(selector, context, path, missing) {
|
|
73
83
|
if (selector.type === 'literal') {
|
|
74
84
|
if (!Object.prototype.hasOwnProperty.call(selector, 'value')) {
|
|
75
85
|
throw createGraphResponseMappingError('literal selector requires a value field.', `${path}.value`);
|
|
76
86
|
}
|
|
77
|
-
return selector.value;
|
|
87
|
+
return applySelectorCoercion(selector.value, selector, path);
|
|
78
88
|
}
|
|
79
89
|
if (selector.type === 'outputsMemoryPath') {
|
|
80
90
|
const sourcePath = assertNonEmptyString(selector.path, `${path}.path`, `${selector.type}.path`);
|
|
81
|
-
|
|
91
|
+
const resolved = presentOrMissing(selectByPath(context.outputsMemory, sourcePath), missing);
|
|
92
|
+
return resolved === OMIT ? resolved : applySelectorCoercion(resolved, selector, path);
|
|
82
93
|
}
|
|
83
94
|
if (selector.type === 'executionMemoryPath' || selector.type === 'executionPath') {
|
|
84
95
|
const sourcePath = assertNonEmptyString(selector.path, `${path}.path`, `${selector.type}.path`);
|
|
85
|
-
|
|
96
|
+
const resolved = presentOrMissing(selectByPath(context.executionMemory, sourcePath), missing);
|
|
97
|
+
return resolved === OMIT ? resolved : applySelectorCoercion(resolved, selector, path);
|
|
86
98
|
}
|
|
87
99
|
if (selector.type === 'nodeMetadata') {
|
|
88
100
|
const nodeId = assertNonEmptyString(selector.nodeId, `${path}.nodeId`, `${selector.type}.nodeId`);
|
|
@@ -91,7 +103,8 @@ function resolveSelector(selector, context, path, missing) {
|
|
|
91
103
|
if (!node) {
|
|
92
104
|
throw createGraphResponseMappingError(`${selector.type} selector references missing nodeId "${nodeId}".`, `${path}.nodeId`, { nodeId });
|
|
93
105
|
}
|
|
94
|
-
|
|
106
|
+
const resolved = presentOrMissing(selectByPath(node.metadata, sourcePath), missing);
|
|
107
|
+
return resolved === OMIT ? resolved : applySelectorCoercion(resolved, selector, path);
|
|
95
108
|
}
|
|
96
109
|
if (selector.type === 'nodeInputsConfig') {
|
|
97
110
|
const inputSelector = selector;
|
|
@@ -102,7 +115,8 @@ function resolveSelector(selector, context, path, missing) {
|
|
|
102
115
|
throw createGraphResponseMappingError(`${selector.type} selector references missing nodeId "${nodeId}".`, `${path}.nodeId`, { nodeId });
|
|
103
116
|
}
|
|
104
117
|
const root = readTaskNodeModelInputSurface(node);
|
|
105
|
-
|
|
118
|
+
const resolved = presentOrMissing(selectByPath(root, sourcePath), missing);
|
|
119
|
+
return resolved === OMIT ? resolved : applySelectorCoercion(resolved, selector, path);
|
|
106
120
|
}
|
|
107
121
|
if (selector.type === 'firstPresent') {
|
|
108
122
|
if (!Array.isArray(selector.sources) || selector.sources.length === 0) {
|
|
@@ -114,10 +128,12 @@ function resolveSelector(selector, context, path, missing) {
|
|
|
114
128
|
throw createGraphResponseMappingError('firstPresent.sources entries must be graph response mapping selectors.', `${path}.sources.${i}`);
|
|
115
129
|
}
|
|
116
130
|
const value = resolveSelector(source, context, `${path}.sources.${i}`, 'omit');
|
|
117
|
-
if (value !== OMIT && value !== null)
|
|
118
|
-
return value;
|
|
131
|
+
if (value !== OMIT && value !== null) {
|
|
132
|
+
return applySelectorCoercion(value, selector, path);
|
|
133
|
+
}
|
|
119
134
|
}
|
|
120
|
-
|
|
135
|
+
const missingValue = materializeMissing(missing);
|
|
136
|
+
return missingValue === OMIT ? missingValue : applySelectorCoercion(missingValue, selector, path);
|
|
121
137
|
}
|
|
122
138
|
throw createGraphResponseMappingError(`Unsupported graph response mapping selector type "${String(selector.type)}".`, `${path}.type`);
|
|
123
139
|
}
|
|
@@ -151,5 +167,42 @@ export function applyGraphResponseDefinition(args) {
|
|
|
151
167
|
const mapped = resolveShape(args.response.shape, args.context, 'graph.response.shape', missing);
|
|
152
168
|
return mapped === OMIT ? undefined : mapped;
|
|
153
169
|
}
|
|
170
|
+
/** Resolves root `graph.response` against accumulated execution memory (FR-GE-002). */
|
|
171
|
+
export const resolveGraphResponse = applyGraphResponseDefinition;
|
|
154
172
|
/** @deprecated Use applyGraphResponseDefinition with root-level graph.response. */
|
|
155
173
|
export const applyGraphResponseMapping = applyGraphResponseDefinition;
|
|
174
|
+
export const GRAPH_RESPONSE_INCOMPLETE = 'GRAPH_RESPONSE_INCOMPLETE';
|
|
175
|
+
function isNonEmptyResponseShape(shape) {
|
|
176
|
+
if (shape == null)
|
|
177
|
+
return false;
|
|
178
|
+
if (Array.isArray(shape))
|
|
179
|
+
return shape.length > 0;
|
|
180
|
+
if (isPlainObject(shape))
|
|
181
|
+
return Object.keys(shape).length > 0;
|
|
182
|
+
return isSupportedSelector(shape);
|
|
183
|
+
}
|
|
184
|
+
function isEmptyResolvedOutput(value) {
|
|
185
|
+
if (value === undefined || value === null)
|
|
186
|
+
return true;
|
|
187
|
+
if (Array.isArray(value))
|
|
188
|
+
return value.length === 0;
|
|
189
|
+
if (isPlainObject(value))
|
|
190
|
+
return Object.keys(value).length === 0;
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* When graph.response.shape is non-empty but resolution yields an empty object, surface FR-GE-006.
|
|
195
|
+
*/
|
|
196
|
+
export function assessGraphResponseCompleteness(response, finalOutput) {
|
|
197
|
+
if (!isNonEmptyResponseShape(response.shape)) {
|
|
198
|
+
return { incomplete: false };
|
|
199
|
+
}
|
|
200
|
+
if (!isEmptyResolvedOutput(finalOutput)) {
|
|
201
|
+
return { incomplete: false };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
incomplete: true,
|
|
205
|
+
code: GRAPH_RESPONSE_INCOMPLETE,
|
|
206
|
+
message: 'graph.response.shape is non-empty but finalOutput resolved empty — check executionMapping paths and selectors.',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -40,6 +40,8 @@ const FORBIDDEN_GRAPH_RESPONSE_KEYS = [
|
|
|
40
40
|
'notableExecutionPaths',
|
|
41
41
|
'mappingPreset',
|
|
42
42
|
];
|
|
43
|
+
const LEGACY_RESPONSE_SHAPE_KEYS = new Set(['subnetAnalysis', 'taskSections']);
|
|
44
|
+
const LEGACY_EXECUTION_MAPPING_PATH_RE = /^inference\.conceptSketch(\.|$)/;
|
|
43
45
|
/**
|
|
44
46
|
* Returns top-level keys on `graph` that are not part of the canonical executable document contract.
|
|
45
47
|
*/
|
|
@@ -431,6 +433,11 @@ function assertGraphResponseShapeNoLegacy(shape, path, context) {
|
|
|
431
433
|
}
|
|
432
434
|
if (!isPlainObject(shape))
|
|
433
435
|
return;
|
|
436
|
+
for (const key of Object.keys(shape)) {
|
|
437
|
+
if (LEGACY_RESPONSE_SHAPE_KEYS.has(key)) {
|
|
438
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `${path}.${key} is a removed legacy response shape key.`, { ...context, responseShapeKey: key });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
434
441
|
if (shape.type === 'nodeInputs') {
|
|
435
442
|
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, `${path}.type "nodeInputs" was removed; use nodeInputsConfig.`, context);
|
|
436
443
|
}
|
|
@@ -478,7 +485,21 @@ export function assertCanonicalTaskNode(node, context, options) {
|
|
|
478
485
|
if (!isTaskShape(node))
|
|
479
486
|
return;
|
|
480
487
|
if ('outputMapping' in node) {
|
|
481
|
-
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `Task node "${String(node.id)}": outputMapping belongs on the finalizer. Use executionMapping to write task results into executionMemory.`, { jobId: context?.jobId, graphId: context?.graphId, nodeId: String(node.id) });
|
|
488
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `Task node "${String(node.id)}": TASK_OUTPUT_MAPPING_DEPRECATED — outputMapping belongs on the finalizer. Use executionMapping to write task results into executionMemory.`, { jobId: context?.jobId, graphId: context?.graphId, nodeId: String(node.id) });
|
|
489
|
+
}
|
|
490
|
+
const executionMapping = node.executionMapping;
|
|
491
|
+
if (executionMapping?.path &&
|
|
492
|
+
typeof executionMapping.path === 'string' &&
|
|
493
|
+
LEGACY_EXECUTION_MAPPING_PATH_RE.test(executionMapping.path)) {
|
|
494
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `Task node "${String(node.id)}": EXECUTION_MAPPING_LEGACY_PATH — use answers.qN instead of inference.conceptSketch.stepN.`, { jobId: context?.jobId, graphId: context?.graphId, nodeId: String(node.id) });
|
|
495
|
+
}
|
|
496
|
+
const map = executionMapping?.map;
|
|
497
|
+
if (isPlainObject(map)) {
|
|
498
|
+
for (const targetKey of Object.keys(map)) {
|
|
499
|
+
if (targetKey.includes('.')) {
|
|
500
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_TASK_NODE, `Task node "${String(node.id)}": TASK_MAP_TARGET_INVALID — executionMapping.map target "${targetKey}" must be flat.`, { jobId: context?.jobId, graphId: context?.graphId, nodeId: String(node.id) });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
482
503
|
}
|
|
483
504
|
assertOptionalTaskNodeConditions(node.conditions, String(node.id), {
|
|
484
505
|
jobId: context?.jobId,
|
|
@@ -670,6 +691,10 @@ export function assertCanonicalGraphDocument(graph, context, options) {
|
|
|
670
691
|
jobId: context?.jobId,
|
|
671
692
|
graphId: resolvedGraphId,
|
|
672
693
|
});
|
|
694
|
+
const preset = metadata.responsePreset;
|
|
695
|
+
if (isPlainObject(preset) && preset.id === 'subnetAnalysis') {
|
|
696
|
+
throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_GRAPH_DOCUMENT, 'metadata.responsePreset.id "subnetAnalysis" was removed — migrate to graph.response.shape.', { jobId: context?.jobId, graphId: resolvedGraphId });
|
|
697
|
+
}
|
|
673
698
|
const graphEntry = metadata.graphEntry;
|
|
674
699
|
if (graphEntry != null && typeof graphEntry === 'object' && !Array.isArray(graphEntry)) {
|
|
675
700
|
const ge = graphEntry;
|
package/dist/src/types/refs.d.ts
CHANGED
|
@@ -133,30 +133,35 @@ export type FinalizerInputBinding = {
|
|
|
133
133
|
value: unknown;
|
|
134
134
|
};
|
|
135
135
|
export type GraphResponseMissingBehavior = 'omit' | 'null';
|
|
136
|
-
|
|
136
|
+
/** Post-resolution output coercion (9.x). Only `stringArray` is supported in 9.0. */
|
|
137
|
+
export type GraphResponseCoercion = 'stringArray';
|
|
138
|
+
type GraphResponseSelectorBase = {
|
|
139
|
+
coerce?: GraphResponseCoercion;
|
|
140
|
+
};
|
|
141
|
+
export type GraphResponseSelector = (GraphResponseSelectorBase & {
|
|
137
142
|
type: 'outputsMemoryPath';
|
|
138
143
|
path: string;
|
|
139
|
-
} | {
|
|
144
|
+
}) | (GraphResponseSelectorBase & {
|
|
140
145
|
type: 'executionMemoryPath';
|
|
141
146
|
path: string;
|
|
142
|
-
} | {
|
|
147
|
+
}) | (GraphResponseSelectorBase & {
|
|
143
148
|
type: 'executionPath';
|
|
144
149
|
path: string;
|
|
145
|
-
} | {
|
|
150
|
+
}) | (GraphResponseSelectorBase & {
|
|
146
151
|
type: 'nodeMetadata';
|
|
147
152
|
nodeId: string;
|
|
148
153
|
path: string;
|
|
149
|
-
} | {
|
|
154
|
+
}) | (GraphResponseSelectorBase & {
|
|
150
155
|
type: 'nodeInputsConfig';
|
|
151
156
|
nodeId: string;
|
|
152
157
|
path: string;
|
|
153
|
-
} | {
|
|
158
|
+
}) | (GraphResponseSelectorBase & {
|
|
154
159
|
type: 'literal';
|
|
155
160
|
value: unknown;
|
|
156
|
-
} | {
|
|
161
|
+
}) | (GraphResponseSelectorBase & {
|
|
157
162
|
type: 'firstPresent';
|
|
158
163
|
sources: GraphResponseSelector[];
|
|
159
|
-
};
|
|
164
|
+
});
|
|
160
165
|
export type GraphResponseShape = unknown;
|
|
161
166
|
export type GraphResponseDefinition = {
|
|
162
167
|
/** Default: "omit". When "null", missing selector values are materialized as null. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AuthoringGraphDocument } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { GraphResponseDefinition } from '../types/refs.js';
|
|
3
|
+
export declare const GRAPH_RESPONSE_LEGACY_SOURCE: "GRAPH_RESPONSE_LEGACY_SOURCE";
|
|
4
|
+
export declare const GRAPH_RESPONSE_DUAL_SOURCE: "GRAPH_RESPONSE_DUAL_SOURCE";
|
|
5
|
+
export type AuthoringResponseSourceIssue = {
|
|
6
|
+
code: typeof GRAPH_RESPONSE_LEGACY_SOURCE | typeof GRAPH_RESPONSE_DUAL_SOURCE;
|
|
7
|
+
message: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Import-only helper: copy metadata.graphResponse → graph.response when graph.response is absent.
|
|
12
|
+
* Does not run on the execute hot path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function migrateLegacyGraphResponseToAuthoring(doc: AuthoringGraphDocument): AuthoringGraphDocument;
|
|
15
|
+
/** Detect legacy-only or dual authoring response sources (FR-GE-001 / CR-GE-001). */
|
|
16
|
+
export declare function detectAuthoringResponseSourceIssues(doc: AuthoringGraphDocument): AuthoringResponseSourceIssue[];
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the single canonical authoring response definition.
|
|
19
|
+
* @throws Error with code GRAPH_RESPONSE_LEGACY_SOURCE when only legacy metadata carries the shape.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveAuthoringGraphResponse(doc: AuthoringGraphDocument): GraphResponseDefinition;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const GRAPH_RESPONSE_LEGACY_SOURCE = 'GRAPH_RESPONSE_LEGACY_SOURCE';
|
|
2
|
+
export const GRAPH_RESPONSE_DUAL_SOURCE = 'GRAPH_RESPONSE_DUAL_SOURCE';
|
|
3
|
+
/**
|
|
4
|
+
* Import-only helper: copy metadata.graphResponse → graph.response when graph.response is absent.
|
|
5
|
+
* Does not run on the execute hot path.
|
|
6
|
+
*/
|
|
7
|
+
export function migrateLegacyGraphResponseToAuthoring(doc) {
|
|
8
|
+
const next = structuredClone(doc);
|
|
9
|
+
const graph = next.graph;
|
|
10
|
+
const hasGraphResponse = isPlainRecord(graph.response) && 'shape' in graph.response;
|
|
11
|
+
const metaResponse = next.graph.metadata?.graphResponse;
|
|
12
|
+
if (!hasGraphResponse && isPlainRecord(metaResponse) && 'shape' in metaResponse) {
|
|
13
|
+
graph.response = {
|
|
14
|
+
...(metaResponse.missing !== undefined ? { missing: metaResponse.missing } : {}),
|
|
15
|
+
shape: structuredClone(metaResponse.shape),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return next;
|
|
19
|
+
}
|
|
20
|
+
function isPlainRecord(v) {
|
|
21
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
function hasExecutableResponseShape(response) {
|
|
24
|
+
if (!isPlainRecord(response))
|
|
25
|
+
return false;
|
|
26
|
+
const shape = response.shape;
|
|
27
|
+
if (shape === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (isPlainRecord(shape))
|
|
30
|
+
return Object.keys(shape).length > 0;
|
|
31
|
+
if (Array.isArray(shape))
|
|
32
|
+
return shape.length > 0;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
function readGraphLevelResponse(doc) {
|
|
36
|
+
const graph = doc.graph;
|
|
37
|
+
const response = graph.response;
|
|
38
|
+
if (!isPlainRecord(response) || !('shape' in response))
|
|
39
|
+
return undefined;
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
function readMetadataGraphResponse(doc) {
|
|
43
|
+
const metaResponse = doc.graph.metadata?.graphResponse;
|
|
44
|
+
if (!isPlainRecord(metaResponse) || !('shape' in metaResponse))
|
|
45
|
+
return undefined;
|
|
46
|
+
return metaResponse;
|
|
47
|
+
}
|
|
48
|
+
/** Detect legacy-only or dual authoring response sources (FR-GE-001 / CR-GE-001). */
|
|
49
|
+
export function detectAuthoringResponseSourceIssues(doc) {
|
|
50
|
+
const issues = [];
|
|
51
|
+
const graphResponse = readGraphLevelResponse(doc);
|
|
52
|
+
const metaResponse = readMetadataGraphResponse(doc);
|
|
53
|
+
const graphHasShape = graphResponse != null && hasExecutableResponseShape(graphResponse);
|
|
54
|
+
const metaHasShape = metaResponse != null && hasExecutableResponseShape(metaResponse);
|
|
55
|
+
if (!graphHasShape && metaHasShape) {
|
|
56
|
+
issues.push({
|
|
57
|
+
code: GRAPH_RESPONSE_LEGACY_SOURCE,
|
|
58
|
+
message: 'Executable response mapping exists only under metadata.graphResponse. Move shape to graph.response — studio metadata is UI-only and is not read at compile or execute.',
|
|
59
|
+
path: 'graph.metadata.graphResponse',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (graphHasShape && metaHasShape) {
|
|
63
|
+
issues.push({
|
|
64
|
+
code: GRAPH_RESPONSE_DUAL_SOURCE,
|
|
65
|
+
message: 'Response mapping is declared on both graph.response and metadata.graphResponse. graph.response is authoritative; remove executable shape from metadata.graphResponse.',
|
|
66
|
+
path: 'graph.metadata.graphResponse',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return issues;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolves the single canonical authoring response definition.
|
|
73
|
+
* @throws Error with code GRAPH_RESPONSE_LEGACY_SOURCE when only legacy metadata carries the shape.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveAuthoringGraphResponse(doc) {
|
|
76
|
+
const issues = detectAuthoringResponseSourceIssues(doc);
|
|
77
|
+
const blocking = issues.find((i) => i.code === GRAPH_RESPONSE_LEGACY_SOURCE);
|
|
78
|
+
if (blocking) {
|
|
79
|
+
const err = new Error(blocking.message);
|
|
80
|
+
err.code = GRAPH_RESPONSE_LEGACY_SOURCE;
|
|
81
|
+
err.path = blocking.path;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
const graphResponse = readGraphLevelResponse(doc);
|
|
85
|
+
if (graphResponse)
|
|
86
|
+
return graphResponse;
|
|
87
|
+
return { shape: {} };
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AuthoringGraphDocument, ExecutableGraphPlanV2 } from '@x12i/graphenix-executable-contracts';
|
|
2
|
+
import type { Graph } from '../types/refs.js';
|
|
3
|
+
export declare const RESPONSE_PATH_NO_WRITER: "RESPONSE_PATH_NO_WRITER";
|
|
4
|
+
export declare const TASK_MAP_TARGET_INVALID: "TASK_MAP_TARGET_INVALID";
|
|
5
|
+
export declare const TASK_MAP_SOURCE_INVALID: "TASK_MAP_SOURCE_INVALID";
|
|
6
|
+
export declare const RESPONSE_LEGACY_SHAPE_KEY: "RESPONSE_LEGACY_SHAPE_KEY";
|
|
7
|
+
export declare const EXECUTION_MAPPING_LEGACY_PATH: "EXECUTION_MAPPING_LEGACY_PATH";
|
|
8
|
+
export declare const RESPONSE_EMPTY_SHAPE: "RESPONSE_EMPTY_SHAPE";
|
|
9
|
+
export declare const RESPONSE_LEGACY_PRESET_ID: "RESPONSE_LEGACY_PRESET_ID";
|
|
10
|
+
export declare const RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR: "RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR";
|
|
11
|
+
export type GraphResponseWiringIssue = {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
nodeId?: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
};
|
|
17
|
+
export type GraphResponseWiringValidation = {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
errors: GraphResponseWiringIssue[];
|
|
20
|
+
warnings: GraphResponseWiringIssue[];
|
|
21
|
+
};
|
|
22
|
+
/** Validate response wiring for an authoring document, executable plan, or materialized graph. */
|
|
23
|
+
export declare function validateGraphResponseWiring(input: AuthoringGraphDocument | ExecutableGraphPlanV2 | Graph): GraphResponseWiringValidation;
|
|
24
|
+
export declare function assertGraphResponseWiringOk(validation: GraphResponseWiringValidation, context?: {
|
|
25
|
+
graphId?: string;
|
|
26
|
+
}): void;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { embeddedGraphToExellixGraph } from '../plan/embeddedGraphToExellixGraph.js';
|
|
2
|
+
import { detectAuthoringResponseSourceIssues, GRAPH_RESPONSE_DUAL_SOURCE, GRAPH_RESPONSE_LEGACY_SOURCE, } from './authoringGraphResponse.js';
|
|
3
|
+
export const RESPONSE_PATH_NO_WRITER = 'RESPONSE_PATH_NO_WRITER';
|
|
4
|
+
export const TASK_MAP_TARGET_INVALID = 'TASK_MAP_TARGET_INVALID';
|
|
5
|
+
export const TASK_MAP_SOURCE_INVALID = 'TASK_MAP_SOURCE_INVALID';
|
|
6
|
+
export const RESPONSE_LEGACY_SHAPE_KEY = 'RESPONSE_LEGACY_SHAPE_KEY';
|
|
7
|
+
export const EXECUTION_MAPPING_LEGACY_PATH = 'EXECUTION_MAPPING_LEGACY_PATH';
|
|
8
|
+
export const RESPONSE_EMPTY_SHAPE = 'RESPONSE_EMPTY_SHAPE';
|
|
9
|
+
export const RESPONSE_LEGACY_PRESET_ID = 'RESPONSE_LEGACY_PRESET_ID';
|
|
10
|
+
export const RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR = 'RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR';
|
|
11
|
+
const LEGACY_SHAPE_KEYS = new Set(['subnetAnalysis', 'taskSections']);
|
|
12
|
+
const LEGACY_EXECUTION_PATH_RE = /^inference\.conceptSketch(\.|$)/;
|
|
13
|
+
const KNOWN_MAP_SOURCE_PREFIXES = ['output.', 'parsed.', 'node.', 'variables.', 'jobMemory.'];
|
|
14
|
+
function isPlainRecord(v) {
|
|
15
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
16
|
+
}
|
|
17
|
+
function isSupportedSelector(v) {
|
|
18
|
+
return isPlainRecord(v) && typeof v.type === 'string';
|
|
19
|
+
}
|
|
20
|
+
function getGraphNodes(graph) {
|
|
21
|
+
return Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
22
|
+
}
|
|
23
|
+
function isTaskNode(node) {
|
|
24
|
+
return node.type !== 'finalizer';
|
|
25
|
+
}
|
|
26
|
+
function isFinalizerNode(node) {
|
|
27
|
+
return node.type === 'finalizer';
|
|
28
|
+
}
|
|
29
|
+
function isShapeEmpty(shape) {
|
|
30
|
+
if (shape == null)
|
|
31
|
+
return true;
|
|
32
|
+
if (Array.isArray(shape))
|
|
33
|
+
return shape.length === 0;
|
|
34
|
+
if (isPlainRecord(shape))
|
|
35
|
+
return Object.keys(shape).length === 0;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function hasTerminalFinalizer(graph) {
|
|
39
|
+
const nodes = getGraphNodes(graph);
|
|
40
|
+
const finalizers = nodes.filter(isFinalizerNode);
|
|
41
|
+
if (finalizers.length === 0)
|
|
42
|
+
return false;
|
|
43
|
+
const edges = graph.edges ?? [];
|
|
44
|
+
const targeted = new Set(edges.map((e) => e.to));
|
|
45
|
+
return finalizers.some((f) => targeted.has(f.id) || edges.some((e) => e.to === f.id));
|
|
46
|
+
}
|
|
47
|
+
function collectTaskWritePaths(graph) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const node of getGraphNodes(graph)) {
|
|
50
|
+
if (!isTaskNode(node))
|
|
51
|
+
continue;
|
|
52
|
+
const em = node.executionMapping;
|
|
53
|
+
if (em?.path && typeof em.path === 'string' && em.path.length > 0) {
|
|
54
|
+
out.push({ nodeId: String(node.id), path: em.path });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function pathHasWriter(selectorPath, writePaths) {
|
|
60
|
+
return writePaths.some(({ path }) => selectorPath === path || selectorPath.startsWith(`${path}.`));
|
|
61
|
+
}
|
|
62
|
+
function collectSelectors(shape, path, out, legacyKeys) {
|
|
63
|
+
if (isSupportedSelector(shape)) {
|
|
64
|
+
out.push({ selector: shape, path });
|
|
65
|
+
if (shape.type === 'firstPresent' && Array.isArray(shape.sources)) {
|
|
66
|
+
shape.sources.forEach((source, index) => collectSelectors(source, `${path}.sources[${index}]`, out, legacyKeys));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(shape)) {
|
|
71
|
+
shape.forEach((item, index) => collectSelectors(item, `${path}[${index}]`, out, legacyKeys));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (isPlainRecord(shape)) {
|
|
75
|
+
for (const [key, val] of Object.entries(shape)) {
|
|
76
|
+
if (LEGACY_SHAPE_KEYS.has(key)) {
|
|
77
|
+
legacyKeys.push({
|
|
78
|
+
code: RESPONSE_LEGACY_SHAPE_KEY,
|
|
79
|
+
message: `graph.response.shape must not use legacy key "${key}".`,
|
|
80
|
+
path: `${path}.${key}`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
collectSelectors(val, `${path}.${key}`, out, legacyKeys);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function isValidMapSource(source) {
|
|
88
|
+
if (source.startsWith('"') || source.startsWith('\\"'))
|
|
89
|
+
return true;
|
|
90
|
+
if (KNOWN_MAP_SOURCE_PREFIXES.some((prefix) => source.startsWith(prefix)))
|
|
91
|
+
return true;
|
|
92
|
+
// Shorthand leaf keys resolve against task output at runtime.
|
|
93
|
+
if (!source.includes('.'))
|
|
94
|
+
return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function validateTaskExecutionMapping(node, errors, warnings) {
|
|
98
|
+
const em = node.executionMapping;
|
|
99
|
+
if (!em?.path)
|
|
100
|
+
return;
|
|
101
|
+
const nodeId = String(node.id);
|
|
102
|
+
if (LEGACY_EXECUTION_PATH_RE.test(em.path)) {
|
|
103
|
+
errors.push({
|
|
104
|
+
code: EXECUTION_MAPPING_LEGACY_PATH,
|
|
105
|
+
message: `Task node "${nodeId}" uses legacy executionMapping.path "${em.path}". Use answers.qN (or another flat namespace) instead of inference.conceptSketch.stepN.`,
|
|
106
|
+
nodeId,
|
|
107
|
+
path: `nodes.${nodeId}.executionMapping.path`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const map = em.map;
|
|
111
|
+
if (!isPlainRecord(map))
|
|
112
|
+
return;
|
|
113
|
+
for (const [targetKey, sourcePath] of Object.entries(map)) {
|
|
114
|
+
if (targetKey.includes('.')) {
|
|
115
|
+
errors.push({
|
|
116
|
+
code: TASK_MAP_TARGET_INVALID,
|
|
117
|
+
message: `Task node "${nodeId}" executionMapping.map target "${targetKey}" must be flat — use graph.response.shape for nesting.`,
|
|
118
|
+
nodeId,
|
|
119
|
+
path: `nodes.${nodeId}.executionMapping.map.${targetKey}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (typeof sourcePath === 'string' && !isValidMapSource(sourcePath)) {
|
|
123
|
+
errors.push({
|
|
124
|
+
code: TASK_MAP_SOURCE_INVALID,
|
|
125
|
+
message: `Task node "${nodeId}" executionMapping.map source "${sourcePath}" is not under a known task output root (output., parsed., node., variables., jobMemory., or shorthand leaf).`,
|
|
126
|
+
nodeId,
|
|
127
|
+
path: `nodes.${nodeId}.executionMapping.map.${targetKey}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function validateResponseShape(graph, errors, warnings) {
|
|
133
|
+
const response = graph.response;
|
|
134
|
+
if (!isPlainRecord(response))
|
|
135
|
+
return;
|
|
136
|
+
const shape = response.shape;
|
|
137
|
+
if (isShapeEmpty(shape) && hasTerminalFinalizer(graph)) {
|
|
138
|
+
errors.push({
|
|
139
|
+
code: RESPONSE_EMPTY_SHAPE,
|
|
140
|
+
message: 'graph.response.shape is empty but the graph has a terminal finalizer — declare response selectors or remove the finalizer.',
|
|
141
|
+
path: 'graph.response.shape',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const writePaths = collectTaskWritePaths(graph);
|
|
145
|
+
const selectors = [];
|
|
146
|
+
const legacyShapeIssues = [];
|
|
147
|
+
collectSelectors(shape, 'graph.response.shape', selectors, legacyShapeIssues);
|
|
148
|
+
errors.push(...legacyShapeIssues);
|
|
149
|
+
for (const { selector, path } of selectors) {
|
|
150
|
+
if (selector.coerce === 'stringArray' && /shortAnswer/i.test(path)) {
|
|
151
|
+
warnings.push({
|
|
152
|
+
code: RESPONSE_COERCE_STRING_ARRAY_ON_SCALAR,
|
|
153
|
+
message: `coerce: "stringArray" on scalar field at ${path} is unusual — shortAnswer is string-typed by contract.`,
|
|
154
|
+
path,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const memoryPath = selector.type === 'executionMemoryPath' || selector.type === 'executionPath'
|
|
158
|
+
? selector.path
|
|
159
|
+
: undefined;
|
|
160
|
+
if (memoryPath && !pathHasWriter(memoryPath, writePaths)) {
|
|
161
|
+
errors.push({
|
|
162
|
+
code: RESPONSE_PATH_NO_WRITER,
|
|
163
|
+
message: `graph.response selector path "${memoryPath}" has no upstream task executionMapping.path prefix.`,
|
|
164
|
+
path,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function validateMetadataLegacyPreset(graph, errors) {
|
|
170
|
+
const metadata = graph.metadata;
|
|
171
|
+
if (!isPlainRecord(metadata))
|
|
172
|
+
return;
|
|
173
|
+
const preset = isPlainRecord(metadata.responsePreset) ? metadata.responsePreset : undefined;
|
|
174
|
+
if (preset?.id === 'subnetAnalysis') {
|
|
175
|
+
errors.push({
|
|
176
|
+
code: RESPONSE_LEGACY_PRESET_ID,
|
|
177
|
+
message: 'metadata.responsePreset.id "subnetAnalysis" was removed — migrate to graph.response.shape.',
|
|
178
|
+
path: 'metadata.responsePreset.id',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function validateGraphModel(graph) {
|
|
183
|
+
const errors = [];
|
|
184
|
+
const warnings = [];
|
|
185
|
+
validateMetadataLegacyPreset(graph, errors);
|
|
186
|
+
for (const node of getGraphNodes(graph)) {
|
|
187
|
+
if (isTaskNode(node))
|
|
188
|
+
validateTaskExecutionMapping(node, errors, warnings);
|
|
189
|
+
}
|
|
190
|
+
validateResponseShape(graph, errors, warnings);
|
|
191
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
192
|
+
}
|
|
193
|
+
function graphFromPlan(plan) {
|
|
194
|
+
return embeddedGraphToExellixGraph(plan);
|
|
195
|
+
}
|
|
196
|
+
/** Validate response wiring for an authoring document, executable plan, or materialized graph. */
|
|
197
|
+
export function validateGraphResponseWiring(input) {
|
|
198
|
+
const errors = [];
|
|
199
|
+
const warnings = [];
|
|
200
|
+
if ('formatVersion' in input && 'graph' in input) {
|
|
201
|
+
const doc = input;
|
|
202
|
+
for (const issue of detectAuthoringResponseSourceIssues(doc)) {
|
|
203
|
+
if (issue.code === GRAPH_RESPONSE_LEGACY_SOURCE) {
|
|
204
|
+
errors.push({ code: issue.code, message: issue.message, path: issue.path });
|
|
205
|
+
}
|
|
206
|
+
else if (issue.code === GRAPH_RESPONSE_DUAL_SOURCE) {
|
|
207
|
+
warnings.push({ code: issue.code, message: issue.message, path: issue.path });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const stubGraph = {
|
|
211
|
+
id: doc.id,
|
|
212
|
+
nodes: [],
|
|
213
|
+
edges: [],
|
|
214
|
+
response: doc.graph.response ?? { shape: {} },
|
|
215
|
+
metadata: doc.graph.metadata,
|
|
216
|
+
};
|
|
217
|
+
validateMetadataLegacyPreset(stubGraph, errors);
|
|
218
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
219
|
+
}
|
|
220
|
+
if ('normalizedGraph' in input && 'source' in input) {
|
|
221
|
+
return validateGraphModel(graphFromPlan(input));
|
|
222
|
+
}
|
|
223
|
+
return validateGraphModel(input);
|
|
224
|
+
}
|
|
225
|
+
export function assertGraphResponseWiringOk(validation, context) {
|
|
226
|
+
if (validation.ok)
|
|
227
|
+
return;
|
|
228
|
+
const summary = validation.errors.map((e) => `${e.code}: ${e.message}`).join('; ');
|
|
229
|
+
const err = new Error(`Graph response wiring validation failed${context?.graphId ? ` for "${context.graphId}"` : ''}: ${summary}`);
|
|
230
|
+
err.code = validation.errors[0]?.code ?? 'GRAPH_RESPONSE_WIRING_INVALID';
|
|
231
|
+
err.details = validation;
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
@@ -12,6 +12,7 @@ function authoringShell(graphId, nodes, edges, graphResponseShape, extra) {
|
|
|
12
12
|
graph: {
|
|
13
13
|
nodes,
|
|
14
14
|
edges,
|
|
15
|
+
response: authoringResponseBlock(graphResponseShape),
|
|
15
16
|
inputs: taskNodes.map((node) => ({
|
|
16
17
|
id: `graph-input:${node.id}`,
|
|
17
18
|
name: 'Input Record',
|
|
@@ -31,7 +32,6 @@ function authoringShell(graphId, nodes, edges, graphResponseShape, extra) {
|
|
|
31
32
|
]
|
|
32
33
|
: [],
|
|
33
34
|
metadata: {
|
|
34
|
-
graphResponse: { shape: graphResponseShape },
|
|
35
35
|
modelConfig: defaultExecutableModelConfig(),
|
|
36
36
|
...extra,
|
|
37
37
|
},
|
|
@@ -39,6 +39,9 @@ function authoringShell(graphId, nodes, edges, graphResponseShape, extra) {
|
|
|
39
39
|
types: [],
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
function authoringResponseBlock(graphResponseShape) {
|
|
43
|
+
return { shape: graphResponseShape };
|
|
44
|
+
}
|
|
42
45
|
/** Single ai-task + select finalizer — mirrors legacy flat `buildAiTaskGraph` tests. */
|
|
43
46
|
export function buildAiTaskAuthoringGraph(graphId) {
|
|
44
47
|
const taskId = 'ask-1';
|
|
@@ -6,7 +6,9 @@ function normalizeAuthoringDocument(input) {
|
|
|
6
6
|
/** Builds `{ plan, runtime }` for tests and hosts with a Graphenix 2.x {@link AuthoringGraphDocument}. */
|
|
7
7
|
export function buildExecuteGraphInput(doc, runtime) {
|
|
8
8
|
return {
|
|
9
|
-
plan: compileExellixExecutablePlan(normalizeAuthoringDocument(doc), runtime
|
|
9
|
+
plan: compileExellixExecutablePlan(normalizeAuthoringDocument(doc), runtime, {
|
|
10
|
+
strictResponseValidation: false,
|
|
11
|
+
}),
|
|
10
12
|
runtime,
|
|
11
13
|
};
|
|
12
14
|
}
|
|
@@ -228,11 +228,6 @@ export function flatTestGraphToAuthoringDocument(flat) {
|
|
|
228
228
|
metadata.jobKnowledge = normalizeKnowledgeRefs(flat.jobKnowledge);
|
|
229
229
|
if (Array.isArray(flat.taskKnowledge))
|
|
230
230
|
metadata.taskKnowledge = normalizeKnowledgeRefs(flat.taskKnowledge);
|
|
231
|
-
metadata.graphResponse = {
|
|
232
|
-
...(flatResponse.missing != null ? { missing: flatResponse.missing } : {}),
|
|
233
|
-
...(flatResponse.version != null ? { version: flatResponse.version } : {}),
|
|
234
|
-
shape: responseShape,
|
|
235
|
-
};
|
|
236
231
|
metadata.modelConfig = flatModelConfigToAuthoring(flat.modelConfig) ?? defaultExecutableModelConfig();
|
|
237
232
|
const taskNodes = nodes.filter((n) => n.kind === TASK_NODE_KIND);
|
|
238
233
|
const finalizer = nodes.find((n) => n.kind === FINALIZER_NODE_KIND);
|
|
@@ -244,6 +239,11 @@ export function flatTestGraphToAuthoringDocument(flat) {
|
|
|
244
239
|
graph: {
|
|
245
240
|
nodes: nodes,
|
|
246
241
|
edges,
|
|
242
|
+
response: {
|
|
243
|
+
...(flatResponse.missing != null ? { missing: flatResponse.missing } : {}),
|
|
244
|
+
...(flatResponse.version != null ? { version: flatResponse.version } : {}),
|
|
245
|
+
shape: responseShape,
|
|
246
|
+
},
|
|
247
247
|
inputs: taskNodes.map((node) => ({
|
|
248
248
|
id: `graph-input:${node.id}`,
|
|
249
249
|
name: 'Input Record',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exellix/graph-engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Graph executor SDK",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prebuild": "node scripts/clean-dist.mjs",
|
|
28
28
|
"build": "tsc",
|
|
29
29
|
"test": "npm run build && npm run check:no-legacy && tsx --test --test-force-exit --test-timeout=180000 --test-concurrency=2 tests/model-alias-canonical.test.ts tests/model-alias-execute.test.ts tests/model-alias-strategy.test.ts tests/model-alias-subnets.test.ts tests/ai-tasks-error-propagation.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",
|
|
30
|
-
"test:full": "npm run build && npm run check:no-legacy && tsx --test --test-force-exit --test-timeout=180000 --test-concurrency=2 tests/graph-engine.test.ts tests/graph-run-contract.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-canonical.test.ts tests/model-alias-execute.test.ts tests/model-alias-strategy.test.ts tests/model-alias-subnets.test.ts tests/compile-host-patches.test.ts tests/reference-fixtures-compile.test.ts tests/step-retry-llm-call.test.ts",
|
|
30
|
+
"test:full": "npm run build && npm run check:no-legacy && tsx --test --test-force-exit --test-timeout=180000 --test-concurrency=2 tests/graph-engine.test.ts tests/graph-run-contract.test.ts tests/graph-response-contract.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-canonical.test.ts tests/model-alias-execute.test.ts tests/model-alias-strategy.test.ts tests/model-alias-subnets.test.ts tests/compile-host-patches.test.ts tests/reference-fixtures-compile.test.ts tests/step-retry-llm-call.test.ts",
|
|
31
31
|
"test:live": "npm run run:pre-synthesis",
|
|
32
32
|
"test:subnets-graph-fixture": "npm run build && node --test tests/subnets-graph.fixture.test.mjs",
|
|
33
33
|
"test:subnets-graph-live": "npm run build && node --env-file=.env --test tests/subnets-graph.live.test.mjs",
|