@exellix/graph-engine 7.2.10 → 7.2.14

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.
@@ -24,6 +24,7 @@ import { buildAiTasksObservabilityRecord } from "./aiTasksObservability.js";
24
24
  import { buildRunTaskIdentityEnvelope, mergeDefinedLlmCallParts, shouldForwardRunTaskTraceMode, } from "./runTaskAugments.js";
25
25
  import { buildRunLog, extractLogxerCorrelationFromMetadata, extractTaskRunLogFromMetadata, resolveRunLogLimits, } from "./buildRunLog.js";
26
26
  import { setRuntimeObjectsLastJobId, summarizeRuntimeObjectsForPlayground } from "./runtimeObjects.js";
27
+ import { DebugLogAbstract, bindGraphEngineRunLogxer, clearGraphEngineRunLogxer, createGraphEngineLogxer, ensureGraphEngineLogxerOnRuntimeObjects, logGraphEngineErrorCode, patchGraphNodeLogContext, runGraphWithLogContext, traceExecutionMemory, } from "./graphEngineLogxer.js";
27
28
  import { assertHostJobId, newGraphRunTaskId } from "./graphRunIdentity.js";
28
29
  import { resolveTaskKey } from "./resolveTaskKey.js";
29
30
  import { buildPredicateEvalContextForNode, mirrorTaskVariablesOnExecution, readExecutionVariableBuckets, seedGraphVariableBucketsFromRuntime, } from "./variables.js";
@@ -611,9 +612,11 @@ export function createExellixGraphRuntime(opts) {
611
612
  ? toRunTaskModelConfigForPhase(effectiveModelConfig, 'main')
612
613
  : undefined;
613
614
  // DEBUG: Verify execution object before sending
614
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
615
- console.log(`[DEBUG] Node ${input.node?.id}: execution object =`, JSON.stringify(input.execution, null, 2));
616
- }
615
+ traceExecutionMemory('executeNode', 'Node execution object before runTask', {
616
+ nodeId: input.node?.id,
617
+ execution: input.execution,
618
+ });
619
+ patchGraphNodeLogContext(String(input.node?.id ?? ''));
617
620
  let execution = input.execution;
618
621
  if (execution == null || typeof execution !== "object")
619
622
  execution = {};
@@ -855,18 +858,18 @@ export function createExellixGraphRuntime(opts) {
855
858
  smartInput: input.node.smartInput,
856
859
  });
857
860
  // TRACE: Validate request object before sending
858
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
859
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: Node ${input.node?.id} - Request built`);
860
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req.executionMemory =`, JSON.stringify(req.executionMemory, null, 2));
861
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req.executionMemory type =`, typeof req.executionMemory);
862
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req.executionMemory is undefined?`, req.executionMemory === undefined);
863
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req.executionMemory is null?`, req.executionMemory === null);
864
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req has executionMemory property?`, 'executionMemory' in req);
865
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req keys =`, Object.keys(req));
866
- if (req.executionMemory && typeof req.executionMemory === 'object') {
867
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: req.executionMemory keys =`, Object.keys(req.executionMemory));
868
- }
869
- }
861
+ traceExecutionMemory('executeNode', 'runTask request built', {
862
+ nodeId: input.node?.id,
863
+ executionMemory: req.executionMemory,
864
+ executionMemoryType: typeof req.executionMemory,
865
+ executionMemoryIsUndefined: req.executionMemory === undefined,
866
+ executionMemoryIsNull: req.executionMemory === null,
867
+ hasExecutionMemoryProperty: 'executionMemory' in req,
868
+ requestKeys: Object.keys(req),
869
+ executionMemoryKeys: req.executionMemory && typeof req.executionMemory === 'object'
870
+ ? Object.keys(req.executionMemory)
871
+ : undefined,
872
+ });
870
873
  let res;
871
874
  const nodeTimeoutMs = input.nodeTimeoutMs;
872
875
  const stepRetryPolicyResolved = resolveStepRetryPolicy(input.stepRetryPolicy ?? opts.stepRetryPolicy, input.node);
@@ -1047,28 +1050,25 @@ export function createExellixGraphRuntime(opts) {
1047
1050
  updatedExecution = postExec;
1048
1051
  }
1049
1052
  // TRACE: Log execution object from response
1050
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true' || process.env.DEBUG_OUTPUT_MAPPING === 'true') {
1051
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: Node ${input.node?.id} - Processing response`);
1052
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: res.execution =`, JSON.stringify(res.execution, null, 2));
1053
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: input.execution =`, JSON.stringify(input.execution, null, 2));
1054
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: updatedExecution (before mapping) =`, JSON.stringify(updatedExecution, null, 2));
1055
- }
1053
+ traceExecutionMemory('executeNode', 'Processing runTask response', {
1054
+ nodeId: input.node?.id,
1055
+ responseExecution: res.execution,
1056
+ inputExecution: input.execution,
1057
+ updatedExecutionBeforeMapping: updatedExecution,
1058
+ });
1056
1059
  // Normalize task output for executionMapping.
1057
1060
  // Normalize: create output object that includes both response output and parsed
1058
1061
  // This allows path resolution to access both "output.parsed.shortAnswer" and "output.shortAnswer"
1059
1062
  // If response.parsed exists, wrap it in { parsed: ... } structure for path resolution
1060
1063
  const responseParsed = res.parsed;
1061
- // TRACE: Log response structure
1062
- if (process.env.DEBUG_OUTPUT_MAPPING === 'true' || process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1063
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: Node ${input.node?.id} - Response structure`);
1064
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: res.parsed =`, JSON.stringify(res.parsed, null, 2));
1065
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: res.parsed type =`, typeof res.parsed);
1066
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: res.parsed keys =`, res.parsed && typeof res.parsed === 'object' ? Object.keys(res.parsed) : 'N/A');
1067
- if (res.parsed && typeof res.parsed === 'object') {
1068
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: res.parsed.shortAnswer =`, res.parsed.shortAnswer);
1069
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: res.parsed.fullAnswer =`, res.parsed.fullAnswer);
1070
- }
1071
- }
1064
+ traceExecutionMemory('executeNode', 'runTask response structure', {
1065
+ nodeId: input.node?.id,
1066
+ parsed: res.parsed,
1067
+ parsedType: typeof res.parsed,
1068
+ parsedKeys: res.parsed && typeof res.parsed === 'object' ? Object.keys(res.parsed) : undefined,
1069
+ parsedShortAnswer: res.parsed && typeof res.parsed === 'object' ? res.parsed.shortAnswer : undefined,
1070
+ parsedFullAnswer: res.parsed && typeof res.parsed === 'object' ? res.parsed.fullAnswer : undefined,
1071
+ });
1072
1072
  // Create output object similar to executeNode.ts - use parsed as the base, or create structure
1073
1073
  const outputForMapping = res.parsed !== undefined
1074
1074
  ? (typeof res.parsed === 'object' && res.parsed !== null && !Array.isArray(res.parsed))
@@ -1076,15 +1076,13 @@ export function createExellixGraphRuntime(opts) {
1076
1076
  : { parsed: responseParsed } // Wrap parsed in object structure for path resolution
1077
1077
  : undefined;
1078
1078
  // TRACE: Log outputForMapping structure
1079
- if (process.env.DEBUG_OUTPUT_MAPPING === 'true' || process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1080
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: outputForMapping structure`);
1081
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: outputForMapping =`, JSON.stringify(outputForMapping, null, 2));
1082
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: outputForMapping.parsed =`, JSON.stringify(outputForMapping?.parsed, null, 2));
1083
- if (outputForMapping?.parsed) {
1084
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: outputForMapping.parsed.shortAnswer =`, outputForMapping.parsed.shortAnswer);
1085
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: outputForMapping.parsed.fullAnswer =`, outputForMapping.parsed.fullAnswer);
1086
- }
1087
- }
1079
+ traceExecutionMemory('executeNode', 'outputForMapping structure', {
1080
+ nodeId: input.node?.id,
1081
+ outputForMapping,
1082
+ outputForMappingParsed: outputForMapping?.parsed,
1083
+ outputForMappingParsedShortAnswer: outputForMapping?.parsed?.shortAnswer,
1084
+ outputForMappingParsedFullAnswer: outputForMapping?.parsed?.fullAnswer,
1085
+ });
1088
1086
  updatedExecution = applyTaskResultMapping({
1089
1087
  targetMemory: updatedExecution,
1090
1088
  mapping: input.node?.executionMapping,
@@ -1094,12 +1092,12 @@ export function createExellixGraphRuntime(opts) {
1094
1092
  });
1095
1093
  const updatedOutputsMemory = isPlainRecord(input.outputsMemory) ? input.outputsMemory : {};
1096
1094
  // TRACE: Log final execution state before returning
1097
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true' || process.env.DEBUG_OUTPUT_MAPPING === 'true') {
1098
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: Node ${input.node?.id} - Final execution state before returning`);
1099
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: updatedExecution =`, JSON.stringify(updatedExecution, null, 2));
1100
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: updatedExecution type =`, typeof updatedExecution);
1101
- console.log(`[TRACE] ExellixGraphRuntime.executeNode: updatedExecution is undefined?`, updatedExecution === undefined);
1102
- }
1095
+ traceExecutionMemory('executeNode', 'Final execution state before returning', {
1096
+ nodeId: input.node?.id,
1097
+ updatedExecution,
1098
+ updatedExecutionType: typeof updatedExecution,
1099
+ updatedExecutionIsUndefined: updatedExecution === undefined,
1100
+ });
1103
1101
  const resMetaOk = res.metadata ?? res.meta;
1104
1102
  const taskRunLogOk = [...retryOutcome.syntheticRunLog, ...extractTaskRunLogFromMetadata(resMetaOk)];
1105
1103
  const logxerCorrelationIdOk = extractLogxerCorrelationFromMetadata(resMetaOk);
@@ -1158,553 +1156,572 @@ export function createExellixGraphRuntime(opts) {
1158
1156
  throw new Error('GRAPH_ID_REQUIRED: execution request model must include a non-empty id');
1159
1157
  }
1160
1158
  const resolvedGraphId = String(resolvedGraphIdRaw);
1161
- assertCanonicalGraphDocument(graph, { jobId, graphId: resolvedGraphId });
1162
- let runxClient = opts.runx;
1163
- if (graphNeedsRunxClient(graph, merged.modelConfig)) {
1164
- if (!runxClient) {
1165
- if (!opts.runxCreateOptions) {
1166
- const err = new Error("RUNX_REQUIRED: graph uses jsConditionFunction, aiCondition, or conditional modelConfig cases that require runx. Pass runx or runxCreateOptions on createExellixGraphRuntime.");
1167
- err.code = "RUNX_REQUIRED";
1168
- throw err;
1159
+ const runLogxer = createGraphEngineLogxer({ logging: merged.logging });
1160
+ return runGraphWithLogContext({ jobId, taskId: graphTaskId, graphId: resolvedGraphId, runId: graphTaskId }, async () => {
1161
+ bindGraphEngineRunLogxer(runLogxer);
1162
+ try {
1163
+ assertCanonicalGraphDocument(graph, { jobId, graphId: resolvedGraphId });
1164
+ let runxClient = opts.runx;
1165
+ if (graphNeedsRunxClient(graph, merged.modelConfig)) {
1166
+ if (!runxClient) {
1167
+ if (!opts.runxCreateOptions) {
1168
+ const err = new Error("RUNX_REQUIRED: graph uses jsConditionFunction, aiCondition, or conditional modelConfig cases that require runx. Pass runx or runxCreateOptions on createExellixGraphRuntime.");
1169
+ err.code = "RUNX_REQUIRED";
1170
+ throw err;
1171
+ }
1172
+ runxClient = await createRunx(opts.runxCreateOptions);
1173
+ await runxClient.bootstrap();
1174
+ await runxClient.reload();
1175
+ }
1169
1176
  }
1170
- runxClient = await createRunx(opts.runxCreateOptions);
1171
- await runxClient.bootstrap();
1172
- await runxClient.reload();
1173
- }
1174
- }
1175
- const graphAudit = {
1176
- source: 'model',
1177
- contentSha256: computeGraphDocumentContentSha256(suppliedGraph),
1178
- };
1179
- const graphDocumentModel = mergeGraphDocumentModel(graph);
1180
- const graphExecution = graphDocumentModel.graphExecution;
1181
- const nodeResponseKeys = resolveNodeResponseKeys(graphExecution);
1182
- const coreObjectiveConfig = resolveCoreObjectiveConfig(graphExecution);
1183
- setRuntimeObjectsLastJobId(merged.runtimeObjects, jobId);
1184
- const { finalizer } = validateGraphFinalizer(graph);
1185
- const engine = opts.engineFactory.create({
1186
- graph,
1187
- mode: runtime.mode,
1188
- goalNodeId: runtime.goalNodeId,
1189
- dimension: runtime.dimension,
1190
- initialState: runtime.initialState,
1191
- initialVariables: runtime.initialVariables,
1192
- });
1193
- const outputsByNodeId = {};
1194
- const stepsResponses = [];
1195
- const errors = [];
1196
- const finalizerNodeId = String(finalizer.id);
1197
- const jobCorrelation = job?.jobType != null ? { jobType: job.jobType } : undefined;
1198
- // Pre-compute incoming-edge map for conditional-edge filtering of plan.nextNodes.
1199
- const rawEdges = Array.isArray(graph.edges) ? graph.edges : [];
1200
- const hasConditionalEdges = rawEdges.some((e) => e && typeof e === "object" && e.when != null);
1201
- const incomingByTo = new Map();
1202
- if (hasConditionalEdges) {
1203
- for (const e of rawEdges) {
1204
- if (!e || typeof e !== "object")
1205
- continue;
1206
- const to = e.to;
1207
- if (typeof to !== "string" || to.length === 0)
1208
- continue;
1209
- const list = incomingByTo.get(to) ?? [];
1210
- list.push(e);
1211
- incomingByTo.set(to, list);
1212
- }
1213
- }
1214
- const graphRunStartedAt = Date.now();
1215
- const taskRunLogBuffer = [];
1216
- let logxerCorrelationIdLast;
1217
- function appendTaskRunLogFromError(e) {
1218
- if (Array.isArray(e?.taskRunLog))
1219
- taskRunLogBuffer.push(...e.taskRunLog);
1220
- if (typeof e?.logxerCorrelationId === "string" && e.logxerCorrelationId.length > 0) {
1221
- logxerCorrelationIdLast = e.logxerCorrelationId;
1222
- }
1223
- }
1224
- function finalizeGraphPayload(graphStatus) {
1225
- const lim = resolveRunLogLimits({
1226
- runLogMode: merged.runLogMode,
1227
- maxRunLogEntries: merged.maxRunLogEntries,
1228
- maxRunLogDataJsonChars: merged.maxRunLogDataJsonChars,
1229
- defaults: {},
1230
- });
1231
- const built = buildRunLog({
1232
- ...lim,
1233
- jobId,
1234
- taskId: graphTaskId,
1235
- graphId: resolvedGraphId,
1236
- graphRunStartedAt,
1237
- graphStatus,
1238
- execution: currentExecution,
1239
- taskAppendedEntries: taskRunLogBuffer,
1240
- logxerCorrelationId: logxerCorrelationIdLast,
1241
- });
1242
- return {
1243
- execution: currentExecution,
1244
- outputsMemory: currentOutputsMemory,
1245
- ...built,
1246
- };
1247
- }
1248
- // Track current memory and execution state (will be updated as nodes execute)
1249
- let currentJobMemory = runtime.jobMemory || {};
1250
- let currentTaskMemory = runtime.taskMemory;
1251
- let currentExecution = normalizeRuntimeExecutionMemory(runtime);
1252
- let currentOutputsMemory = normalizeRuntimeOutputsMemory(runtime);
1253
- if (!currentExecution._trace)
1254
- currentExecution._trace = { nodes: {} };
1255
- if (!currentExecution._trace.nodes)
1256
- currentExecution._trace.nodes = {};
1257
- const jobKnowledgePatch = await resolveKnowledgePatch({
1258
- refs: graph.jobKnowledge,
1259
- resolver: opts.resolveJobKnowledge,
1260
- context: { model: graph, runtime, graphId: resolvedGraphId, jobId, taskId: graphTaskId },
1261
- });
1262
- seedGraphRunExecutionState({
1263
- execution: currentExecution,
1264
- jobMemory: currentJobMemory,
1265
- job: runtime.job,
1266
- });
1267
- seedGraphVariableBucketsFromRuntime({
1268
- execution: currentExecution,
1269
- graph,
1270
- runtime,
1271
- job,
1272
- });
1273
- assertFinalizerRequiredReadsResolvable({
1274
- graph,
1275
- finalizer,
1276
- executionMemory: currentExecution,
1277
- outputsMemory: currentOutputsMemory,
1278
- });
1279
- const coreObjectiveValue = resolveCoreObjectiveValue({
1280
- sourcePath: coreObjectiveConfig.sourcePath,
1281
- execution: currentExecution,
1282
- jobMemory: currentJobMemory,
1283
- taskMemory: currentTaskMemory,
1284
- job,
1285
- });
1286
- if (eventEmitter) {
1287
- eventEmitter.emit(createGraphStartEvent(jobId, resolvedGraphId, graphTaskId, {
1288
- agentId: job?.agentId,
1289
- input: {
1290
- variables: runtime.variables,
1291
- jobMemory: currentJobMemory,
1292
- taskMemory: currentTaskMemory,
1293
- },
1294
- ...(jobCorrelation ?? {}),
1295
- }));
1296
- }
1297
- function buildDebugTrace() {
1298
- if (!debugMode)
1299
- return undefined;
1300
- const traceNodes = (currentExecution?._trace?.nodes ?? {});
1301
- const out = [];
1302
- for (const [nodeId, entry] of Object.entries(traceNodes)) {
1303
- if (entry == null || typeof entry !== "object")
1304
- continue;
1305
- out.push({ nodeId, ...entry });
1306
- }
1307
- // Stable order by startedAt, then by nodeId.
1308
- out.sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0) || a.nodeId.localeCompare(b.nodeId));
1309
- return { nodes: out };
1310
- }
1311
- function appendStepResponse(node, output) {
1312
- if (node?.type === "finalizer")
1313
- return;
1314
- const nodeResponse = unwrapNodeResponse(output);
1315
- stepsResponses.push({
1316
- [coreObjectiveConfig.propertyName]: coreObjectiveValue,
1317
- [nodeResponseKeys.singular]: nodeResponse,
1318
- });
1319
- }
1320
- function buildFinalOutputFromGraphResponse() {
1321
- const executionMemoryForResponse = isPlainRecord(currentExecution)
1322
- ? structuredClone(currentExecution)
1323
- : currentExecution;
1324
- return applyGraphResponseDefinition({
1325
- response: graph.response,
1326
- context: {
1327
- graph,
1328
- executionMemory: executionMemoryForResponse,
1329
- outputsMemory: isPlainRecord(currentOutputsMemory)
1330
- ? structuredClone(currentOutputsMemory)
1331
- : currentOutputsMemory,
1332
- outputsByNodeId,
1333
- stepsResponses,
1334
- },
1335
- });
1336
- }
1337
- const playgroundReporter = runtime.playgroundReporter ?? opts.playgroundReporter;
1338
- if (playgroundReporter) {
1339
- playgroundReporter.step("graph:start", {
1340
- jobId,
1341
- taskId: graphTaskId,
1342
- graphId: resolvedGraphId,
1343
- executionKeys: Object.keys(currentExecution),
1344
- variablesKeys: runtime.variables != null ? Object.keys(runtime.variables) : [],
1345
- executionExcerpt: typeof currentExecution === "object" && currentExecution !== null
1346
- ? JSON.stringify(currentExecution, null, 2).slice(0, 500)
1347
- : undefined,
1348
- ...(merged.playgroundMeta != null && typeof merged.playgroundMeta === "object"
1349
- ? merged.playgroundMeta
1350
- : {}),
1351
- });
1352
- if (merged.runtimeObjects) {
1353
- playgroundReporter.step("graph:observability", summarizeRuntimeObjectsForPlayground(merged.runtimeObjects));
1354
- }
1355
- }
1356
- // TRACE: Log initial execution state
1357
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1358
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: Initial execution state`);
1359
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: runtime.executionMemory =`, JSON.stringify(runtime.executionMemory, null, 2));
1360
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: currentExecution =`, JSON.stringify(currentExecution, null, 2));
1361
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: currentExecution type =`, typeof currentExecution);
1362
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: currentExecution is undefined?`, currentExecution === undefined);
1363
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: currentExecution is null?`, currentExecution === null);
1364
- }
1365
- function recordForStructuredDataFilters() {
1366
- return isPlainRecord(currentExecution?.input)
1367
- ? currentExecution.input
1368
- : {};
1369
- }
1370
- const graphEntryForGate = graphDocumentModel.graphEntry;
1371
- if (graphEntryForGate?.dataFilters !== undefined) {
1372
- const entryEv = evaluateStructuredDataFilters(graphEntryForGate.dataFilters, recordForStructuredDataFilters());
1373
- if (entryEv.status === "unsupported_shape" ||
1374
- (entryEv.status === "evaluated" && !entryEv.ok)) {
1375
- const err = new Error("GRAPH_ENTRY_DATA_FILTERS_REJECTED: metadata.graphEntry.dataFilters (structured) did not accept runtime.executionMemory.input");
1376
- err.code = ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED;
1377
- const finalOutput = buildFinalOutputFromGraphResponse();
1378
- const result = {
1379
- jobId,
1380
- taskId: graphTaskId,
1381
- graphId: resolvedGraphId,
1382
- status: "failed",
1383
- outputsByNodeId,
1384
- stepsResponses,
1385
- engineSnapshot: engine.snapshot(),
1386
- ...(finalOutput !== undefined ? { finalOutput } : {}),
1387
- errors: [...errors, { error: err }],
1388
- graphAudit,
1389
- ...finalizeGraphPayload("failed"),
1177
+ const graphAudit = {
1178
+ source: 'model',
1179
+ contentSha256: computeGraphDocumentContentSha256(suppliedGraph),
1390
1180
  };
1391
- const debug = buildDebugTrace();
1392
- if (debug)
1393
- result.debug = debug;
1394
- if (eventEmitter) {
1395
- eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, err, {
1396
- finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1397
- ...(jobCorrelation ?? {}),
1398
- }));
1399
- }
1400
- return result;
1401
- }
1402
- }
1403
- while (true) {
1404
- const plan = engine.plan();
1405
- if (plan.status === "completed") {
1406
- const graphStatus = errors.length ? "failed" : "completed";
1407
- const finalOutput = buildFinalOutputFromGraphResponse();
1408
- const result = {
1409
- jobId,
1410
- taskId: graphTaskId,
1181
+ const graphDocumentModel = mergeGraphDocumentModel(graph);
1182
+ const graphExecution = graphDocumentModel.graphExecution;
1183
+ const nodeResponseKeys = resolveNodeResponseKeys(graphExecution);
1184
+ const coreObjectiveConfig = resolveCoreObjectiveConfig(graphExecution);
1185
+ setRuntimeObjectsLastJobId(merged.runtimeObjects, jobId);
1186
+ ensureGraphEngineLogxerOnRuntimeObjects(merged.runtimeObjects, runLogxer);
1187
+ runLogxer.info("Graph run started", {
1411
1188
  graphId: resolvedGraphId,
1412
- status: graphStatus,
1413
- outputsByNodeId,
1414
- stepsResponses,
1415
- engineSnapshot: engine.snapshot(),
1416
- ...(finalOutput !== undefined ? { finalOutput } : {}),
1417
- ...(errors.length === 0 ? { finalizerNodeId, finalizerType: finalizer.finalizerType } : {}),
1418
- errors: errors.length ? errors : undefined,
1419
- graphAudit,
1420
- ...finalizeGraphPayload(graphStatus),
1421
- };
1422
- const debug = buildDebugTrace();
1423
- if (debug)
1424
- result.debug = debug;
1425
- if (eventEmitter) {
1426
- if (graphStatus === "completed") {
1427
- eventEmitter.emit(createGraphCompleteEvent(jobId, resolvedGraphId, graphTaskId, {
1428
- output: finalOutput,
1429
- nodesExecuted: Object.keys(outputsByNodeId).length,
1430
- finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1431
- ...(jobCorrelation ?? {}),
1432
- }));
1189
+ debugKind: DebugLogAbstract.EVENT,
1190
+ });
1191
+ const { finalizer } = validateGraphFinalizer(graph);
1192
+ const engine = opts.engineFactory.create({
1193
+ graph,
1194
+ mode: runtime.mode,
1195
+ goalNodeId: runtime.goalNodeId,
1196
+ dimension: runtime.dimension,
1197
+ initialState: runtime.initialState,
1198
+ initialVariables: runtime.initialVariables,
1199
+ });
1200
+ const outputsByNodeId = {};
1201
+ const stepsResponses = [];
1202
+ const errors = [];
1203
+ const finalizerNodeId = String(finalizer.id);
1204
+ const jobCorrelation = job?.jobType != null ? { jobType: job.jobType } : undefined;
1205
+ // Pre-compute incoming-edge map for conditional-edge filtering of plan.nextNodes.
1206
+ const rawEdges = Array.isArray(graph.edges) ? graph.edges : [];
1207
+ const hasConditionalEdges = rawEdges.some((e) => e && typeof e === "object" && e.when != null);
1208
+ const incomingByTo = new Map();
1209
+ if (hasConditionalEdges) {
1210
+ for (const e of rawEdges) {
1211
+ if (!e || typeof e !== "object")
1212
+ continue;
1213
+ const to = e.to;
1214
+ if (typeof to !== "string" || to.length === 0)
1215
+ continue;
1216
+ const list = incomingByTo.get(to) ?? [];
1217
+ list.push(e);
1218
+ incomingByTo.set(to, list);
1433
1219
  }
1434
- else {
1435
- eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, errors[0]?.error, {
1436
- finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1437
- ...(jobCorrelation ?? {}),
1438
- }));
1220
+ }
1221
+ const graphRunStartedAt = Date.now();
1222
+ const taskRunLogBuffer = [];
1223
+ let logxerCorrelationIdLast;
1224
+ function appendTaskRunLogFromError(e) {
1225
+ if (Array.isArray(e?.taskRunLog))
1226
+ taskRunLogBuffer.push(...e.taskRunLog);
1227
+ if (typeof e?.logxerCorrelationId === "string" && e.logxerCorrelationId.length > 0) {
1228
+ logxerCorrelationIdLast = e.logxerCorrelationId;
1439
1229
  }
1440
1230
  }
1441
- return result;
1442
- }
1443
- if (plan.status !== "continue") {
1444
- const err = new Error(`GRAPH_BLOCKED: status=${plan.status}`);
1445
- err.code = "GRAPH_BLOCKED";
1446
- const finalOutput = buildFinalOutputFromGraphResponse();
1447
- const result = {
1448
- jobId,
1449
- taskId: graphTaskId,
1450
- graphId: resolvedGraphId,
1451
- status: "failed",
1452
- outputsByNodeId,
1453
- stepsResponses,
1454
- engineSnapshot: engine.snapshot(),
1455
- ...(finalOutput !== undefined ? { finalOutput } : {}),
1456
- errors: [...errors, { error: err }],
1457
- graphAudit,
1458
- ...finalizeGraphPayload("failed"),
1459
- };
1460
- const debug = buildDebugTrace();
1461
- if (debug)
1462
- result.debug = debug;
1463
- if (eventEmitter) {
1464
- eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, err, {
1465
- finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1466
- ...(jobCorrelation ?? {}),
1467
- }));
1231
+ function finalizeGraphPayload(graphStatus) {
1232
+ const lim = resolveRunLogLimits({
1233
+ runLogMode: merged.runLogMode,
1234
+ maxRunLogEntries: merged.maxRunLogEntries,
1235
+ maxRunLogDataJsonChars: merged.maxRunLogDataJsonChars,
1236
+ defaults: {},
1237
+ });
1238
+ const built = buildRunLog({
1239
+ ...lim,
1240
+ jobId,
1241
+ taskId: graphTaskId,
1242
+ graphId: resolvedGraphId,
1243
+ graphRunStartedAt,
1244
+ graphStatus,
1245
+ execution: currentExecution,
1246
+ taskAppendedEntries: taskRunLogBuffer,
1247
+ logxerCorrelationId: logxerCorrelationIdLast,
1248
+ });
1249
+ return {
1250
+ execution: currentExecution,
1251
+ outputsMemory: currentOutputsMemory,
1252
+ ...built,
1253
+ };
1468
1254
  }
1469
- return result;
1470
- }
1471
- let runnableNodes = plan.nextNodes ?? [];
1472
- // Conditional edge filtering: a node with incoming edges that are *all* conditional must
1473
- // have at least one incoming edge whose `when` evaluates true to be runnable this round.
1474
- // Roots and nodes with at least one unconditional incoming edge always pass through.
1475
- if (hasConditionalEdges && runnableNodes.length > 0) {
1476
- const planningContextBase = {
1477
- executionMemory: currentExecution,
1255
+ // Track current memory and execution state (will be updated as nodes execute)
1256
+ let currentJobMemory = runtime.jobMemory || {};
1257
+ let currentTaskMemory = runtime.taskMemory;
1258
+ let currentExecution = normalizeRuntimeExecutionMemory(runtime);
1259
+ let currentOutputsMemory = normalizeRuntimeOutputsMemory(runtime);
1260
+ if (!currentExecution._trace)
1261
+ currentExecution._trace = { nodes: {} };
1262
+ if (!currentExecution._trace.nodes)
1263
+ currentExecution._trace.nodes = {};
1264
+ const jobKnowledgePatch = await resolveKnowledgePatch({
1265
+ refs: graph.jobKnowledge,
1266
+ resolver: opts.resolveJobKnowledge,
1267
+ context: { model: graph, runtime, graphId: resolvedGraphId, jobId, taskId: graphTaskId },
1268
+ });
1269
+ seedGraphRunExecutionState({
1270
+ execution: currentExecution,
1478
1271
  jobMemory: currentJobMemory,
1479
- taskMemory: currentTaskMemory,
1480
- };
1481
- runnableNodes = runnableNodes.filter((n) => {
1482
- const id = n?.id;
1483
- if (typeof id !== "string" || id.length === 0)
1484
- return true;
1485
- const incoming = incomingByTo.get(id) ?? [];
1486
- if (incoming.length === 0)
1487
- return true;
1488
- const hasUnconditional = incoming.some((e) => e?.when == null);
1489
- if (hasUnconditional)
1490
- return true;
1491
- const planningContext = buildPredicateEvalContextForNode({
1492
- executionMemory: currentExecution,
1493
- jobMemory: currentJobMemory,
1494
- taskMemory: currentTaskMemory,
1495
- node: n,
1496
- runtimeTaskVariables: runtime.taskVariables,
1497
- });
1498
- return incoming.some((e) => evaluateGraphPredicate(e.when, planningContext));
1272
+ job: runtime.job,
1499
1273
  });
1500
- }
1501
- const dataFiltersRecord = recordForStructuredDataFilters();
1502
- const conditionCtxBase = {
1503
- executionMemory: currentExecution,
1504
- jobMemory: currentJobMemory,
1505
- taskMemory: currentTaskMemory,
1506
- job,
1507
- };
1508
- const skippedByConditions = [];
1509
- const gatedRunnable = [];
1510
- for (const n of runnableNodes) {
1511
- if (n?.type === "finalizer") {
1512
- gatedRunnable.push(n);
1513
- continue;
1514
- }
1515
- const nodePredicateCtx = buildPredicateEvalContextForNode({
1274
+ seedGraphVariableBucketsFromRuntime({
1275
+ execution: currentExecution,
1276
+ graph,
1277
+ runtime,
1278
+ job,
1279
+ });
1280
+ assertFinalizerRequiredReadsResolvable({
1281
+ graph,
1282
+ finalizer,
1516
1283
  executionMemory: currentExecution,
1284
+ outputsMemory: currentOutputsMemory,
1285
+ });
1286
+ const coreObjectiveValue = resolveCoreObjectiveValue({
1287
+ sourcePath: coreObjectiveConfig.sourcePath,
1288
+ execution: currentExecution,
1517
1289
  jobMemory: currentJobMemory,
1518
1290
  taskMemory: currentTaskMemory,
1519
- node: n,
1520
- runtimeTaskVariables: runtime.taskVariables,
1291
+ job,
1521
1292
  });
1522
- const condEv = await evaluateTaskNodeConditions(n.conditions, { ...conditionCtxBase, ...nodePredicateCtx }, dataFiltersRecord, { runx: runxClient });
1523
- if (!condEv.ok) {
1524
- skippedByConditions.push({
1525
- node: n,
1526
- skipReason: condEv.skipReason ?? "condition_eval_error",
1527
- });
1528
- continue;
1293
+ if (eventEmitter) {
1294
+ eventEmitter.emit(createGraphStartEvent(jobId, resolvedGraphId, graphTaskId, {
1295
+ agentId: job?.agentId,
1296
+ input: {
1297
+ variables: runtime.variables,
1298
+ jobMemory: currentJobMemory,
1299
+ taskMemory: currentTaskMemory,
1300
+ },
1301
+ ...(jobCorrelation ?? {}),
1302
+ }));
1529
1303
  }
1530
- gatedRunnable.push(n);
1531
- }
1532
- for (const { node, skipReason } of skippedByConditions) {
1533
- engine.commit({
1534
- nodeId: String(node.id),
1535
- output: { ...skippedByConditionsOutput(skipReason) },
1536
- });
1537
- }
1538
- runnableNodes = gatedRunnable;
1539
- const concurrency = resolveConcurrencyLimit({ graph, runnableNodes });
1540
- // Execute the current runnable batch in parallel (bounded)
1541
- // Important: we only call plan() again AFTER all commits from this batch land.
1542
- let batchHadFailFastError = false;
1543
- await runPool(runnableNodes, concurrency, async (node) => {
1544
- // If failFast was triggered, don't schedule more work (runPool also stops scheduling)
1545
- if (batchHadFailFastError)
1546
- return;
1547
- const executionBaseForNode = cloneJsonLike(currentExecution);
1548
- const executionForNode = cloneJsonLike(executionBaseForNode);
1549
- const outputsBaseForNode = cloneJsonLike(currentOutputsMemory);
1550
- const outputsForNode = cloneJsonLike(outputsBaseForNode);
1551
- // TRACE: Log execution before passing to node
1552
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1553
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: About to execute node ${node.id}`);
1554
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: executionForNode =`, JSON.stringify(executionForNode, null, 2));
1555
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: executionForNode type =`, typeof executionForNode);
1556
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: executionForNode is undefined?`, executionForNode === undefined);
1557
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: executionForNode is null?`, executionForNode === null);
1558
- if (executionForNode && typeof executionForNode === 'object') {
1559
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: executionForNode keys =`, Object.keys(executionForNode));
1304
+ function buildDebugTrace() {
1305
+ if (!debugMode)
1306
+ return undefined;
1307
+ const traceNodes = (currentExecution?._trace?.nodes ?? {});
1308
+ const out = [];
1309
+ for (const [nodeId, entry] of Object.entries(traceNodes)) {
1310
+ if (entry == null || typeof entry !== "object")
1311
+ continue;
1312
+ out.push({ nodeId, ...entry });
1560
1313
  }
1314
+ // Stable order by startedAt, then by nodeId.
1315
+ out.sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0) || a.nodeId.localeCompare(b.nodeId));
1316
+ return { nodes: out };
1561
1317
  }
1562
- try {
1563
- const isTaskNodeForKnowledge = node?.type !== "finalizer";
1564
- const taskKnowledgePatch = await resolveKnowledgePatch({
1565
- refs: isTaskNodeForKnowledge
1566
- ? (Array.isArray(node.taskKnowledge) ? node.taskKnowledge : [])
1567
- : undefined,
1568
- resolver: opts.resolveTaskKnowledge,
1569
- context: { model: graph, runtime, graphId: resolvedGraphId, jobId, taskId: graphTaskId, node: node },
1318
+ function appendStepResponse(node, output) {
1319
+ if (node?.type === "finalizer")
1320
+ return;
1321
+ const nodeResponse = unwrapNodeResponse(output);
1322
+ stepsResponses.push({
1323
+ [coreObjectiveConfig.propertyName]: coreObjectiveValue,
1324
+ [nodeResponseKeys.singular]: nodeResponse,
1570
1325
  });
1571
- const nodeTaskMemory = isPlainRecord(currentTaskMemory) ? { ...currentTaskMemory } : currentTaskMemory;
1572
- const runtimeNodeConfig = readRuntimeNodeConfig(merged.nodes, typeof node.id === "string" ? node.id : undefined);
1573
- const effectiveModelConfig = await resolveModelConfigForNode({
1574
- runtimeNodeConfig,
1575
- nodeModelConfig: node.taskConfiguration?.modelConfig,
1576
- runtimeModelConfig: merged.modelConfig,
1577
- graphModelConfig: graph.modelConfig,
1578
- conditionCtx: buildPredicateEvalContextForNode({
1579
- executionMemory: currentExecution,
1580
- jobMemory: currentJobMemory,
1581
- taskMemory: currentTaskMemory,
1582
- node: node,
1583
- runtimeTaskVariables: runtime.taskVariables,
1584
- }),
1585
- executionInput: dataFiltersRecord,
1586
- runx: runxClient,
1587
- graphId: resolvedGraphId,
1588
- nodeId: typeof node.id === "string" ? node.id : String(node.id),
1589
- jobId,
1326
+ }
1327
+ function buildFinalOutputFromGraphResponse() {
1328
+ const executionMemoryForResponse = isPlainRecord(currentExecution)
1329
+ ? structuredClone(currentExecution)
1330
+ : currentExecution;
1331
+ return applyGraphResponseDefinition({
1332
+ response: graph.response,
1333
+ context: {
1334
+ graph,
1335
+ executionMemory: executionMemoryForResponse,
1336
+ outputsMemory: isPlainRecord(currentOutputsMemory)
1337
+ ? structuredClone(currentOutputsMemory)
1338
+ : currentOutputsMemory,
1339
+ outputsByNodeId,
1340
+ stepsResponses,
1341
+ },
1590
1342
  });
1591
- const r = await executeNode({
1343
+ }
1344
+ const playgroundReporter = runtime.playgroundReporter ?? opts.playgroundReporter;
1345
+ if (playgroundReporter) {
1346
+ playgroundReporter.step("graph:start", {
1347
+ jobId,
1348
+ taskId: graphTaskId,
1592
1349
  graphId: resolvedGraphId,
1593
- graph,
1594
- graphRunTaskId: graphTaskId,
1595
- node,
1596
- job,
1597
- jobMemory: currentJobMemory,
1598
- taskMemory: nodeTaskMemory,
1599
- jobKnowledgePatch,
1600
- taskKnowledgePatch,
1601
- execution: executionForNode,
1602
- outputsMemory: outputsForNode,
1603
- variables: runtime.variables,
1604
- taskVariables: runtime.taskVariables,
1605
- modelConfig: effectiveModelConfig,
1606
- llmCall: merged.llmCall,
1607
- runTaskIdentity: merged.runTaskIdentity,
1608
- runTaskExecutionMode: merged.runTaskExecutionMode,
1609
- runTaskDiagnostics: merged.runTaskDiagnostics,
1610
- graphExecutionPipeline: merged.executionPipeline,
1611
- skillKeyResolution: merged.skillKeyResolution,
1612
- nodeTimeoutMs: merged.nodeTimeoutMs,
1613
- clearSynthesizedContextPerNode: merged.clearSynthesizedContextPerNode,
1614
- stepRetryPolicy: merged.stepRetryPolicy,
1615
- mainReadinessPolicy: merged.mainReadinessPolicy,
1616
- eventEmitter,
1617
- jobCorrelation,
1350
+ executionKeys: Object.keys(currentExecution),
1351
+ variablesKeys: runtime.variables != null ? Object.keys(runtime.variables) : [],
1352
+ executionExcerpt: typeof currentExecution === "object" && currentExecution !== null
1353
+ ? JSON.stringify(currentExecution, null, 2).slice(0, 500)
1354
+ : undefined,
1355
+ ...(merged.playgroundMeta != null && typeof merged.playgroundMeta === "object"
1356
+ ? merged.playgroundMeta
1357
+ : {}),
1618
1358
  });
1619
- outputsByNodeId[r.nodeId] = r.output;
1620
- appendStepResponse(node, r.output);
1621
- engine.commit({ nodeId: r.nodeId, output: r.output });
1622
- const trl = r.taskRunLog;
1623
- if (trl?.length)
1624
- taskRunLogBuffer.push(...trl);
1625
- const lcid = r.logxerCorrelationId;
1626
- if (typeof lcid === "string" && lcid.length > 0)
1627
- logxerCorrelationIdLast = lcid;
1628
- // Update execution object from node result if available.
1629
- // Task nodes write execution state through executionMapping.
1630
- if (r.execution !== undefined) {
1631
- // TRACE: Log execution object update
1632
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1633
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: Node ${node.id} returned execution object`);
1634
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: r.execution =`, JSON.stringify(r.execution, null, 2));
1635
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: r.execution type =`, typeof r.execution);
1636
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: r.execution keys =`, r.execution && typeof r.execution === 'object' ? Object.keys(r.execution) : 'N/A');
1359
+ if (merged.runtimeObjects) {
1360
+ playgroundReporter.step("graph:observability", summarizeRuntimeObjectsForPlayground(merged.runtimeObjects));
1361
+ }
1362
+ }
1363
+ // TRACE: Log initial execution state
1364
+ traceExecutionMemory('executeGraph', 'Initial execution state', {
1365
+ runtimeExecutionMemory: runtime.executionMemory,
1366
+ currentExecution,
1367
+ currentExecutionType: typeof currentExecution,
1368
+ currentExecutionIsUndefined: currentExecution === undefined,
1369
+ currentExecutionIsNull: currentExecution === null,
1370
+ });
1371
+ function recordForStructuredDataFilters() {
1372
+ return isPlainRecord(currentExecution?.input)
1373
+ ? currentExecution.input
1374
+ : {};
1375
+ }
1376
+ const graphEntryForGate = graphDocumentModel.graphEntry;
1377
+ if (graphEntryForGate?.dataFilters !== undefined) {
1378
+ const entryEv = evaluateStructuredDataFilters(graphEntryForGate.dataFilters, recordForStructuredDataFilters());
1379
+ if (entryEv.status === "unsupported_shape" ||
1380
+ (entryEv.status === "evaluated" && !entryEv.ok)) {
1381
+ const err = new Error("GRAPH_ENTRY_DATA_FILTERS_REJECTED: metadata.graphEntry.dataFilters (structured) did not accept runtime.executionMemory.input");
1382
+ err.code = ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED;
1383
+ logGraphEngineErrorCode(ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED, err.message, {
1384
+ graphId: resolvedGraphId,
1385
+ jobId,
1386
+ taskId: graphTaskId,
1387
+ error: err,
1388
+ });
1389
+ const finalOutput = buildFinalOutputFromGraphResponse();
1390
+ const result = {
1391
+ jobId,
1392
+ taskId: graphTaskId,
1393
+ graphId: resolvedGraphId,
1394
+ status: "failed",
1395
+ outputsByNodeId,
1396
+ stepsResponses,
1397
+ engineSnapshot: engine.snapshot(),
1398
+ ...(finalOutput !== undefined ? { finalOutput } : {}),
1399
+ errors: [...errors, { error: err }],
1400
+ graphAudit,
1401
+ ...finalizeGraphPayload("failed"),
1402
+ };
1403
+ const debug = buildDebugTrace();
1404
+ if (debug)
1405
+ result.debug = debug;
1406
+ if (eventEmitter) {
1407
+ eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, err, {
1408
+ finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1409
+ ...(jobCorrelation ?? {}),
1410
+ }));
1637
1411
  }
1638
- currentExecution = mergeExecutionUpdate(currentExecution, r.execution, executionBaseForNode);
1639
- // TRACE: Log updated currentExecution
1640
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1641
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: Updated currentExecution =`, JSON.stringify(currentExecution, null, 2));
1412
+ return result;
1413
+ }
1414
+ }
1415
+ while (true) {
1416
+ const plan = engine.plan();
1417
+ if (plan.status === "completed") {
1418
+ const graphStatus = errors.length ? "failed" : "completed";
1419
+ const finalOutput = buildFinalOutputFromGraphResponse();
1420
+ const result = {
1421
+ jobId,
1422
+ taskId: graphTaskId,
1423
+ graphId: resolvedGraphId,
1424
+ status: graphStatus,
1425
+ outputsByNodeId,
1426
+ stepsResponses,
1427
+ engineSnapshot: engine.snapshot(),
1428
+ ...(finalOutput !== undefined ? { finalOutput } : {}),
1429
+ ...(errors.length === 0 ? { finalizerNodeId, finalizerType: finalizer.finalizerType } : {}),
1430
+ errors: errors.length ? errors : undefined,
1431
+ graphAudit,
1432
+ ...finalizeGraphPayload(graphStatus),
1433
+ };
1434
+ const debug = buildDebugTrace();
1435
+ if (debug)
1436
+ result.debug = debug;
1437
+ if (eventEmitter) {
1438
+ if (graphStatus === "completed") {
1439
+ eventEmitter.emit(createGraphCompleteEvent(jobId, resolvedGraphId, graphTaskId, {
1440
+ output: finalOutput,
1441
+ nodesExecuted: Object.keys(outputsByNodeId).length,
1442
+ finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1443
+ ...(jobCorrelation ?? {}),
1444
+ }));
1445
+ }
1446
+ else {
1447
+ eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, errors[0]?.error, {
1448
+ finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1449
+ ...(jobCorrelation ?? {}),
1450
+ }));
1451
+ }
1642
1452
  }
1453
+ return result;
1643
1454
  }
1644
- else {
1645
- // TRACE: Log when execution is not returned
1646
- if (process.env.DEBUG_EXECUTION_MEMORY === 'true') {
1647
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: Node ${node.id} did NOT return execution object`);
1648
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: r.execution =`, r.execution);
1649
- console.log(`[TRACE] ExellixGraphRuntime.executeGraph: currentExecution remains =`, JSON.stringify(currentExecution, null, 2));
1455
+ if (plan.status !== "continue") {
1456
+ const err = new Error(`GRAPH_BLOCKED: status=${plan.status}`);
1457
+ err.code = "GRAPH_BLOCKED";
1458
+ const finalOutput = buildFinalOutputFromGraphResponse();
1459
+ const result = {
1460
+ jobId,
1461
+ taskId: graphTaskId,
1462
+ graphId: resolvedGraphId,
1463
+ status: "failed",
1464
+ outputsByNodeId,
1465
+ stepsResponses,
1466
+ engineSnapshot: engine.snapshot(),
1467
+ ...(finalOutput !== undefined ? { finalOutput } : {}),
1468
+ errors: [...errors, { error: err }],
1469
+ graphAudit,
1470
+ ...finalizeGraphPayload("failed"),
1471
+ };
1472
+ const debug = buildDebugTrace();
1473
+ if (debug)
1474
+ result.debug = debug;
1475
+ if (eventEmitter) {
1476
+ eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, err, {
1477
+ finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1478
+ ...(jobCorrelation ?? {}),
1479
+ }));
1650
1480
  }
1481
+ return result;
1651
1482
  }
1652
- if (r.outputsMemory !== undefined) {
1653
- currentOutputsMemory = mergeExecutionUpdate(currentOutputsMemory, r.outputsMemory, outputsBaseForNode);
1483
+ let runnableNodes = plan.nextNodes ?? [];
1484
+ // Conditional edge filtering: a node with incoming edges that are *all* conditional must
1485
+ // have at least one incoming edge whose `when` evaluates true to be runnable this round.
1486
+ // Roots and nodes with at least one unconditional incoming edge always pass through.
1487
+ if (hasConditionalEdges && runnableNodes.length > 0) {
1488
+ const planningContextBase = {
1489
+ executionMemory: currentExecution,
1490
+ jobMemory: currentJobMemory,
1491
+ taskMemory: currentTaskMemory,
1492
+ };
1493
+ runnableNodes = runnableNodes.filter((n) => {
1494
+ const id = n?.id;
1495
+ if (typeof id !== "string" || id.length === 0)
1496
+ return true;
1497
+ const incoming = incomingByTo.get(id) ?? [];
1498
+ if (incoming.length === 0)
1499
+ return true;
1500
+ const hasUnconditional = incoming.some((e) => e?.when == null);
1501
+ if (hasUnconditional)
1502
+ return true;
1503
+ const planningContext = buildPredicateEvalContextForNode({
1504
+ executionMemory: currentExecution,
1505
+ jobMemory: currentJobMemory,
1506
+ taskMemory: currentTaskMemory,
1507
+ node: n,
1508
+ runtimeTaskVariables: runtime.taskVariables,
1509
+ });
1510
+ return incoming.some((e) => evaluateGraphPredicate(e.when, planningContext));
1511
+ });
1654
1512
  }
1655
- // Note: We don't modify jobMemory or taskMemory here
1656
- // They are set before the job starts and we just pass them through
1657
- }
1658
- catch (e) {
1659
- appendTaskRunLogFromError(e);
1660
- errors.push({ nodeId: node?.id, error: e });
1661
- engine.commit({ nodeId: node?.id, error: e });
1662
- if (eventEmitter && !wasNodeLifecycleEmitted(e)) {
1663
- eventEmitter.emit(createNodeFailEvent(jobId, resolvedGraphId, graphTaskId, node, String(e?.skillKey ?? ""), {
1664
- error: { code: e?.code, message: e?.message, stack: e?.stack },
1665
- memoryAfter: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1666
- response: undefined,
1667
- }, jobCorrelation));
1513
+ const dataFiltersRecord = recordForStructuredDataFilters();
1514
+ const conditionCtxBase = {
1515
+ executionMemory: currentExecution,
1516
+ jobMemory: currentJobMemory,
1517
+ taskMemory: currentTaskMemory,
1518
+ job,
1519
+ };
1520
+ const skippedByConditions = [];
1521
+ const gatedRunnable = [];
1522
+ for (const n of runnableNodes) {
1523
+ if (n?.type === "finalizer") {
1524
+ gatedRunnable.push(n);
1525
+ continue;
1526
+ }
1527
+ const nodePredicateCtx = buildPredicateEvalContextForNode({
1528
+ executionMemory: currentExecution,
1529
+ jobMemory: currentJobMemory,
1530
+ taskMemory: currentTaskMemory,
1531
+ node: n,
1532
+ runtimeTaskVariables: runtime.taskVariables,
1533
+ });
1534
+ const condEv = await evaluateTaskNodeConditions(n.conditions, { ...conditionCtxBase, ...nodePredicateCtx }, dataFiltersRecord, { runx: runxClient });
1535
+ if (!condEv.ok) {
1536
+ skippedByConditions.push({
1537
+ node: n,
1538
+ skipReason: condEv.skipReason ?? "condition_eval_error",
1539
+ });
1540
+ continue;
1541
+ }
1542
+ gatedRunnable.push(n);
1668
1543
  }
1669
- if (failFast) {
1670
- batchHadFailFastError = true;
1671
- // Throw to stop scheduling new nodes in runPool
1672
- throw e;
1544
+ for (const { node, skipReason } of skippedByConditions) {
1545
+ engine.commit({
1546
+ nodeId: String(node.id),
1547
+ output: { ...skippedByConditionsOutput(skipReason) },
1548
+ });
1549
+ }
1550
+ runnableNodes = gatedRunnable;
1551
+ const concurrency = resolveConcurrencyLimit({ graph, runnableNodes });
1552
+ // Execute the current runnable batch in parallel (bounded)
1553
+ // Important: we only call plan() again AFTER all commits from this batch land.
1554
+ let batchHadFailFastError = false;
1555
+ await runPool(runnableNodes, concurrency, async (node) => {
1556
+ // If failFast was triggered, don't schedule more work (runPool also stops scheduling)
1557
+ if (batchHadFailFastError)
1558
+ return;
1559
+ const executionBaseForNode = cloneJsonLike(currentExecution);
1560
+ const executionForNode = cloneJsonLike(executionBaseForNode);
1561
+ const outputsBaseForNode = cloneJsonLike(currentOutputsMemory);
1562
+ const outputsForNode = cloneJsonLike(outputsBaseForNode);
1563
+ // TRACE: Log execution before passing to node
1564
+ traceExecutionMemory('executeGraph', 'About to execute node', {
1565
+ nodeId: node.id,
1566
+ executionForNode,
1567
+ executionForNodeType: typeof executionForNode,
1568
+ executionForNodeIsUndefined: executionForNode === undefined,
1569
+ executionForNodeIsNull: executionForNode === null,
1570
+ executionForNodeKeys: executionForNode && typeof executionForNode === 'object'
1571
+ ? Object.keys(executionForNode)
1572
+ : undefined,
1573
+ });
1574
+ try {
1575
+ const isTaskNodeForKnowledge = node?.type !== "finalizer";
1576
+ const taskKnowledgePatch = await resolveKnowledgePatch({
1577
+ refs: isTaskNodeForKnowledge
1578
+ ? (Array.isArray(node.taskKnowledge) ? node.taskKnowledge : [])
1579
+ : undefined,
1580
+ resolver: opts.resolveTaskKnowledge,
1581
+ context: { model: graph, runtime, graphId: resolvedGraphId, jobId, taskId: graphTaskId, node: node },
1582
+ });
1583
+ const nodeTaskMemory = isPlainRecord(currentTaskMemory) ? { ...currentTaskMemory } : currentTaskMemory;
1584
+ const runtimeNodeConfig = readRuntimeNodeConfig(merged.nodes, typeof node.id === "string" ? node.id : undefined);
1585
+ const effectiveModelConfig = await resolveModelConfigForNode({
1586
+ runtimeNodeConfig,
1587
+ nodeModelConfig: node.taskConfiguration?.modelConfig,
1588
+ runtimeModelConfig: merged.modelConfig,
1589
+ graphModelConfig: graph.modelConfig,
1590
+ conditionCtx: buildPredicateEvalContextForNode({
1591
+ executionMemory: currentExecution,
1592
+ jobMemory: currentJobMemory,
1593
+ taskMemory: currentTaskMemory,
1594
+ node: node,
1595
+ runtimeTaskVariables: runtime.taskVariables,
1596
+ }),
1597
+ executionInput: dataFiltersRecord,
1598
+ runx: runxClient,
1599
+ graphId: resolvedGraphId,
1600
+ nodeId: typeof node.id === "string" ? node.id : String(node.id),
1601
+ jobId,
1602
+ });
1603
+ const r = await executeNode({
1604
+ graphId: resolvedGraphId,
1605
+ graph,
1606
+ graphRunTaskId: graphTaskId,
1607
+ node,
1608
+ job,
1609
+ jobMemory: currentJobMemory,
1610
+ taskMemory: nodeTaskMemory,
1611
+ jobKnowledgePatch,
1612
+ taskKnowledgePatch,
1613
+ execution: executionForNode,
1614
+ outputsMemory: outputsForNode,
1615
+ variables: runtime.variables,
1616
+ taskVariables: runtime.taskVariables,
1617
+ modelConfig: effectiveModelConfig,
1618
+ llmCall: merged.llmCall,
1619
+ runTaskIdentity: merged.runTaskIdentity,
1620
+ runTaskExecutionMode: merged.runTaskExecutionMode,
1621
+ runTaskDiagnostics: merged.runTaskDiagnostics,
1622
+ graphExecutionPipeline: merged.executionPipeline,
1623
+ skillKeyResolution: merged.skillKeyResolution,
1624
+ nodeTimeoutMs: merged.nodeTimeoutMs,
1625
+ clearSynthesizedContextPerNode: merged.clearSynthesizedContextPerNode,
1626
+ stepRetryPolicy: merged.stepRetryPolicy,
1627
+ mainReadinessPolicy: merged.mainReadinessPolicy,
1628
+ eventEmitter,
1629
+ jobCorrelation,
1630
+ });
1631
+ outputsByNodeId[r.nodeId] = r.output;
1632
+ appendStepResponse(node, r.output);
1633
+ engine.commit({ nodeId: r.nodeId, output: r.output });
1634
+ const trl = r.taskRunLog;
1635
+ if (trl?.length)
1636
+ taskRunLogBuffer.push(...trl);
1637
+ const lcid = r.logxerCorrelationId;
1638
+ if (typeof lcid === "string" && lcid.length > 0)
1639
+ logxerCorrelationIdLast = lcid;
1640
+ // Update execution object from node result if available.
1641
+ // Task nodes write execution state through executionMapping.
1642
+ if (r.execution !== undefined) {
1643
+ traceExecutionMemory('executeGraph', 'Node returned execution object', {
1644
+ nodeId: node.id,
1645
+ execution: r.execution,
1646
+ executionType: typeof r.execution,
1647
+ executionKeys: r.execution && typeof r.execution === 'object'
1648
+ ? Object.keys(r.execution)
1649
+ : undefined,
1650
+ });
1651
+ currentExecution = mergeExecutionUpdate(currentExecution, r.execution, executionBaseForNode);
1652
+ traceExecutionMemory('executeGraph', 'Updated currentExecution after node', {
1653
+ nodeId: node.id,
1654
+ currentExecution,
1655
+ });
1656
+ }
1657
+ else {
1658
+ traceExecutionMemory('executeGraph', 'Node did not return execution object', {
1659
+ nodeId: node.id,
1660
+ execution: r.execution,
1661
+ currentExecution,
1662
+ });
1663
+ }
1664
+ if (r.outputsMemory !== undefined) {
1665
+ currentOutputsMemory = mergeExecutionUpdate(currentOutputsMemory, r.outputsMemory, outputsBaseForNode);
1666
+ }
1667
+ // Note: We don't modify jobMemory or taskMemory here
1668
+ // They are set before the job starts and we just pass them through
1669
+ }
1670
+ catch (e) {
1671
+ appendTaskRunLogFromError(e);
1672
+ errors.push({ nodeId: node?.id, error: e });
1673
+ engine.commit({ nodeId: node?.id, error: e });
1674
+ if (eventEmitter && !wasNodeLifecycleEmitted(e)) {
1675
+ eventEmitter.emit(createNodeFailEvent(jobId, resolvedGraphId, graphTaskId, node, String(e?.skillKey ?? ""), {
1676
+ error: { code: e?.code, message: e?.message, stack: e?.stack },
1677
+ memoryAfter: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1678
+ response: undefined,
1679
+ }, jobCorrelation));
1680
+ }
1681
+ if (failFast) {
1682
+ batchHadFailFastError = true;
1683
+ // Throw to stop scheduling new nodes in runPool
1684
+ throw e;
1685
+ }
1686
+ }
1687
+ }, { failFast }).catch((e) => {
1688
+ // failFast path: we intentionally end early after stopping scheduling
1689
+ // (in-flight nodes may still finish; JS won't cancel them).
1690
+ if (!failFast)
1691
+ throw e;
1692
+ });
1693
+ if (failFast && errors.length) {
1694
+ const finalOutput = buildFinalOutputFromGraphResponse();
1695
+ const result = {
1696
+ jobId,
1697
+ taskId: graphTaskId,
1698
+ graphId: resolvedGraphId,
1699
+ status: "failed",
1700
+ outputsByNodeId,
1701
+ stepsResponses,
1702
+ engineSnapshot: engine.snapshot(),
1703
+ ...(finalOutput !== undefined ? { finalOutput } : {}),
1704
+ errors,
1705
+ graphAudit,
1706
+ ...finalizeGraphPayload("failed"),
1707
+ };
1708
+ const debug = buildDebugTrace();
1709
+ if (debug)
1710
+ result.debug = debug;
1711
+ if (eventEmitter) {
1712
+ eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, errors[0]?.error, {
1713
+ finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1714
+ ...(jobCorrelation ?? {}),
1715
+ }));
1716
+ }
1717
+ return result;
1673
1718
  }
1674
1719
  }
1675
- }, { failFast }).catch((e) => {
1676
- // failFast path: we intentionally end early after stopping scheduling
1677
- // (in-flight nodes may still finish; JS won't cancel them).
1678
- if (!failFast)
1679
- throw e;
1680
- });
1681
- if (failFast && errors.length) {
1682
- const finalOutput = buildFinalOutputFromGraphResponse();
1683
- const result = {
1684
- jobId,
1685
- taskId: graphTaskId,
1686
- graphId: resolvedGraphId,
1687
- status: "failed",
1688
- outputsByNodeId,
1689
- stepsResponses,
1690
- engineSnapshot: engine.snapshot(),
1691
- ...(finalOutput !== undefined ? { finalOutput } : {}),
1692
- errors,
1693
- graphAudit,
1694
- ...finalizeGraphPayload("failed"),
1695
- };
1696
- const debug = buildDebugTrace();
1697
- if (debug)
1698
- result.debug = debug;
1699
- if (eventEmitter) {
1700
- eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, errors[0]?.error, {
1701
- finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1702
- ...(jobCorrelation ?? {}),
1703
- }));
1704
- }
1705
- return result;
1706
1720
  }
1707
- }
1721
+ finally {
1722
+ clearGraphEngineRunLogxer();
1723
+ }
1724
+ });
1708
1725
  }
1709
1726
  return {
1710
1727
  executeGraph,