@exellix/graph-engine 7.8.1 → 8.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +16 -13
  3. package/dist/src/adapters/compileExellixExecutablePlan.d.ts +8 -0
  4. package/dist/src/adapters/compileExellixExecutablePlan.js +18 -0
  5. package/dist/src/adapters/migrateExellixGraphModelToAuthoring.d.ts +6 -0
  6. package/dist/src/adapters/migrateExellixGraphModelToAuthoring.js +273 -0
  7. package/dist/src/adapters/patchFinalizerPlans.d.ts +7 -0
  8. package/dist/src/adapters/patchFinalizerPlans.js +63 -0
  9. package/dist/src/errors/exellixGraphErrorCodes.d.ts +5 -0
  10. package/dist/src/errors/exellixGraphErrorCodes.js +5 -0
  11. package/dist/src/index.d.ts +9 -2
  12. package/dist/src/index.js +6 -2
  13. package/dist/src/integrations/ActivixNodeActivityIntegration.js +7 -0
  14. package/dist/src/plan/aiModelSelectionWire.d.ts +11 -0
  15. package/dist/src/plan/aiModelSelectionWire.js +39 -0
  16. package/dist/src/plan/applyNodePlanInvoke.d.ts +10 -0
  17. package/dist/src/plan/applyNodePlanInvoke.js +67 -0
  18. package/dist/src/plan/embeddedGraphToExellixGraph.d.ts +5 -0
  19. package/dist/src/plan/embeddedGraphToExellixGraph.js +131 -0
  20. package/dist/src/plan/planDeferredGates.d.ts +16 -0
  21. package/dist/src/plan/planDeferredGates.js +118 -0
  22. package/dist/src/plan/planExecuteEntry.d.ts +12 -0
  23. package/dist/src/plan/planExecuteEntry.js +73 -0
  24. package/dist/src/plan/planExecutionPipeline.d.ts +11 -0
  25. package/dist/src/plan/planExecutionPipeline.js +54 -0
  26. package/dist/src/plan/planModelConfig.d.ts +10 -0
  27. package/dist/src/plan/planModelConfig.js +46 -0
  28. package/dist/src/runtime/ExellixGraphRuntime.d.ts +9 -6
  29. package/dist/src/runtime/ExellixGraphRuntime.js +138 -98
  30. package/dist/src/runtime/executionMatrixHost.js +2 -1
  31. package/dist/src/runtime/studioGraphExecuteRequest.d.ts +51 -0
  32. package/dist/src/runtime/studioGraphExecuteRequest.js +78 -0
  33. package/dist/testkit/buildExecuteGraphInput.d.ts +4 -0
  34. package/dist/testkit/buildExecuteGraphInput.js +8 -0
  35. package/dist/testkit/index.d.ts +1 -0
  36. package/dist/testkit/index.js +1 -0
  37. package/dist/testkit/testModelAliasRuntime.js +2 -2
  38. package/package.json +9 -3
@@ -1,3 +1,10 @@
1
+ import { createExecutionTrace, appendExecutionEvent, validateExecutionTrace } from "@x12i/graphenix-trace-format";
2
+ import { preparePlanExecuteEntry, computePlanAudit } from "../plan/planExecuteEntry.js";
3
+ import { embeddedGraphToExellixGraph } from "../plan/embeddedGraphToExellixGraph.js";
4
+ import { graphAiModelConfigFromFinalizerPlan, graphAiModelConfigFromNodePlan } from "../plan/planModelConfig.js";
5
+ import { executionPipelineFromNodePlan } from "../plan/planExecutionPipeline.js";
6
+ import { applyNodePlanInvoke, finalizerPlanForId, nodePlanForId } from "../plan/applyNodePlanInvoke.js";
7
+ import { evaluateDeferredNodeGate, evaluateDeferredEdgeGate, nodeDeferredGate, hasPlanConditionalEdges, evaluatePlanEntryGates, planNeedsRunx, } from "../plan/planDeferredGates.js";
1
8
  import { mergeExecutionObject, copyExecutionContextFields, } from "./memory.js";
2
9
  import { buildAiTasksRunTaskRequest, extractRunTaskStrategyOverrides, resolveJobTypeId, } from "./buildAiTasksRunTaskRequest.js";
3
10
  import { runTaskResponseSucceeded } from "./runTaskResponse.js";
@@ -16,11 +23,9 @@ import { resolveNarrixForTaskNode } from "./resolveNarrixForTaskNode.js";
16
23
  import { assertFinalizerRequiredReadsResolvable, validateGraphFinalizer, } from "./finalizers/validateFinalizer.js";
17
24
  import { executeDeterministicFinalizer, executeSynthesizeFinalizer } from "./finalizers/executeFinalizer.js";
18
25
  import { createFinalizerError } from "./finalizers/errors.js";
19
- import { assertCanonicalGraphDocument } from "./validateCanonicalGraphDocument.js";
20
26
  import { assertCanonicalGraphRuntimeObject } from "./validateCanonicalGraphRuntime.js";
21
27
  import { applyGraphResponseDefinition } from "./graphResponseMapping.js";
22
28
  import { migrateLegacyGraphResponse } from "./graphResponseMigration.js";
23
- import { computeGraphDocumentContentSha256 } from "./graphDocumentFingerprint.js";
24
29
  import { buildAiTasksObservabilityRecord } from "./aiTasksObservability.js";
25
30
  import { buildMainLlmCallForRunTask, buildRunTaskIdentityEnvelope, shouldForwardRunTaskTraceMode, } from "./runTaskAugments.js";
26
31
  import { buildRunLog, extractLogxerCorrelationFromMetadata, extractTaskRunLogFromMetadata, resolveRunLogLimits, } from "./buildRunLog.js";
@@ -33,9 +38,6 @@ import { buildPredicateEvalContextForNode, mirrorTaskVariablesOnExecution, readE
33
38
  import { mergeExellixGraphRuntimeInvocation } from "./mergeExellixGraphRuntimeInvocation.js";
34
39
  import { resolveStepRetryPolicy, runRunTaskWithRetry } from "./stepRetry.js";
35
40
  import { isLocalSkillKey, runScopedDataReader, runDeterministicRule, runScopedAnswerWriter, runScopedAnswerAssembler, } from "./localSkills/index.js";
36
- import { evaluateGraphPredicate } from "./predicates.js";
37
- import { evaluateStructuredDataFilters } from "./dataFiltersEvaluation.js";
38
- import { evaluateTaskNodeConditions } from "./taskNodeConditionsEvaluation.js";
39
41
  import { resolveModelConfigForNode } from "./resolveModelConfigForNode.js";
40
42
  import { toRunTaskModelConfig } from "./graphAiModelConfig.js";
41
43
  import { isGraphAiModelConfig } from "./modelConfigSelection.js";
@@ -963,8 +965,10 @@ export function createExellixGraphRuntime(opts) {
963
965
  const resMeta = res.metadata ?? res.meta;
964
966
  const taskRunLog = [...retryOutcome.syntheticRunLog, ...extractTaskRunLogFromMetadata(resMeta)];
965
967
  const logxerCorrelationId = extractLogxerCorrelationFromMetadata(resMeta);
966
- const err = new Error(`TASK_RUN_FAILED: ${skillKey}`);
967
- err.code = res.error?.code ?? "TASK_RUN_FAILED";
968
+ const taskErrorMessage = res.error?.message ?? "Task run failed";
969
+ const taskErrorCode = res.error?.code ?? "TASK_RUN_FAILED";
970
+ const err = new Error(taskErrorMessage);
971
+ err.code = taskErrorCode;
968
972
  err.skillKey = skillKey;
969
973
  err.diagnostics = res.diagnostics;
970
974
  err.nodeId = input.node?.id;
@@ -1153,36 +1157,45 @@ export function createExellixGraphRuntime(opts) {
1153
1157
  };
1154
1158
  }
1155
1159
  async function executeGraph(input) {
1156
- const { model: suppliedGraph, runtime } = input;
1157
- const { graph } = migrateLegacyGraphResponse(suppliedGraph);
1160
+ const { plan: suppliedPlan, runtime } = input;
1161
+ const { plan } = preparePlanExecuteEntry(suppliedPlan, runtime);
1162
+ const materialized = embeddedGraphToExellixGraph(plan);
1163
+ const graph = migrateLegacyGraphResponse(materialized).graph;
1158
1164
  const merged = mergeExellixGraphRuntimeInvocation(runtime, opts);
1159
- const failFast = merged.failFast;
1165
+ const scheduling = plan.schedulingPolicy;
1166
+ const plannerMode = scheduling?.mode ?? runtime.mode;
1167
+ const plannerGoalNodeId = scheduling?.goalNodeId ?? runtime.goalNodeId;
1168
+ const failFast = scheduling?.failFast ?? merged.failFast;
1160
1169
  const debugMode = runtime.debugMode === true;
1161
1170
  const eventEmitter = runtime.eventEmitter ?? opts.eventEmitter;
1162
1171
  const jobId = assertHostJobId(runtime.jobId);
1163
1172
  const graphTaskId = newGraphRunTaskId();
1164
1173
  const job = { ...(runtime.job ?? {}), id: jobId, jobId };
1165
- if (runtime.mode === "backward" && !runtime.goalNodeId) {
1174
+ if (plannerMode === "backward" && !plannerGoalNodeId) {
1166
1175
  const err = new Error(`BACKWARD_GOAL_REQUIRED: mode=backward requires goalNodeId`);
1167
1176
  err.code = "BACKWARD_GOAL_REQUIRED";
1168
1177
  throw err;
1169
1178
  }
1170
- const resolvedGraphIdRaw = graph?.id;
1171
- if (resolvedGraphIdRaw == null || resolvedGraphIdRaw === '') {
1172
- throw new Error('GRAPH_ID_REQUIRED: execution request model must include a non-empty id');
1173
- }
1174
- const resolvedGraphId = String(resolvedGraphIdRaw);
1179
+ const resolvedGraphId = String(plan.source.graphId);
1175
1180
  const runLogxer = createGraphEngineLogxer({ logging: merged.logging });
1181
+ const executionTrace = createExecutionTrace(plan, runtime);
1182
+ appendExecutionEvent(executionTrace, {
1183
+ id: `evt:${graphTaskId}:graph.started`,
1184
+ ts: new Date().toISOString(),
1185
+ level: "info",
1186
+ type: "graph.started",
1187
+ message: "Graph execution started.",
1188
+ });
1189
+ const planAudit = computePlanAudit(plan);
1176
1190
  return runWithAiTasksStackLogging(merged.logging, () => runGraphWithLogContext({ jobId, taskId: graphTaskId, graphId: resolvedGraphId, runId: graphTaskId }, async () => {
1177
1191
  bindGraphEngineRunLogxer(runLogxer);
1178
1192
  try {
1179
- assertCanonicalGraphDocument(graph, { jobId, graphId: resolvedGraphId }, { mode: 'execute' });
1180
1193
  assertCanonicalGraphRuntimeObject(runtime, { jobId, graphId: resolvedGraphId });
1181
1194
  let runxClient = opts.runx;
1182
- if (graphNeedsRunxClient(graph)) {
1195
+ if (planNeedsRunx(plan)) {
1183
1196
  if (!runxClient) {
1184
1197
  if (!opts.runxCreateOptions) {
1185
- const err = new Error("RUNX_REQUIRED: graph uses jsConditionFunction, aiCondition, or conditional modelConfig cases that require runx. Pass runx or runxCreateOptions on createExellixGraphRuntime.");
1198
+ const err = new Error("RUNX_REQUIRED: executable plan deferred gates require runx. Pass runx or runxCreateOptions on createExellixGraphRuntime.");
1186
1199
  err.code = "RUNX_REQUIRED";
1187
1200
  throw err;
1188
1201
  }
@@ -1196,10 +1209,6 @@ export function createExellixGraphRuntime(opts) {
1196
1209
  await runxClient.reload();
1197
1210
  }
1198
1211
  }
1199
- const graphAudit = {
1200
- source: 'model',
1201
- contentSha256: computeGraphDocumentContentSha256(suppliedGraph),
1202
- };
1203
1212
  const graphDocumentModel = mergeGraphDocumentModel(graph);
1204
1213
  const graphExecution = graphDocumentModel.graphExecution;
1205
1214
  const nodeResponseKeys = resolveNodeResponseKeys(graphExecution);
@@ -1213,8 +1222,8 @@ export function createExellixGraphRuntime(opts) {
1213
1222
  const { finalizer } = validateGraphFinalizer(graph);
1214
1223
  const engine = opts.engineFactory.create({
1215
1224
  graph,
1216
- mode: runtime.mode,
1217
- goalNodeId: runtime.goalNodeId,
1225
+ mode: plannerMode,
1226
+ goalNodeId: plannerGoalNodeId,
1218
1227
  dimension: runtime.dimension,
1219
1228
  initialState: runtime.initialState,
1220
1229
  initialVariables: runtime.initialVariables,
@@ -1224,19 +1233,15 @@ export function createExellixGraphRuntime(opts) {
1224
1233
  const errors = [];
1225
1234
  const finalizerNodeId = String(finalizer.id);
1226
1235
  const jobCorrelation = job?.jobType != null ? { jobType: job.jobType } : undefined;
1227
- // Pre-compute incoming-edge map for conditional-edge filtering of plan.nextNodes.
1228
- const rawEdges = Array.isArray(graph.edges) ? graph.edges : [];
1229
- const hasConditionalEdges = rawEdges.some((e) => e && typeof e === "object" && e.when != null);
1236
+ const hasConditionalEdges = hasPlanConditionalEdges(plan);
1230
1237
  const incomingByTo = new Map();
1231
1238
  if (hasConditionalEdges) {
1232
- for (const e of rawEdges) {
1233
- if (!e || typeof e !== "object")
1234
- continue;
1235
- const to = e.to;
1239
+ for (const edgeRef of plan.topology.edges) {
1240
+ const to = edgeRef.to.nodeId;
1236
1241
  if (typeof to !== "string" || to.length === 0)
1237
1242
  continue;
1238
1243
  const list = incomingByTo.get(to) ?? [];
1239
- list.push(e);
1244
+ list.push(edgeRef);
1240
1245
  incomingByTo.set(to, list);
1241
1246
  }
1242
1247
  }
@@ -1395,50 +1400,67 @@ export function createExellixGraphRuntime(opts) {
1395
1400
  ? currentExecution.input
1396
1401
  : {};
1397
1402
  }
1398
- const graphEntryForGate = graphDocumentModel.graphEntry;
1399
- if (graphEntryForGate?.dataFilters !== undefined) {
1400
- const entryEv = evaluateStructuredDataFilters(graphEntryForGate.dataFilters, recordForStructuredDataFilters());
1401
- if (entryEv.status === "unsupported_shape" ||
1402
- (entryEv.status === "evaluated" && !entryEv.ok)) {
1403
- const err = new Error("GRAPH_ENTRY_DATA_FILTERS_REJECTED: metadata.graphEntry.dataFilters (structured) did not accept runtime.executionMemory.input");
1404
- err.code = ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED;
1405
- logGraphEngineErrorCode(ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED, err.message, {
1406
- graphId: resolvedGraphId,
1407
- jobId,
1408
- taskId: graphTaskId,
1409
- error: err,
1410
- });
1411
- const finalOutput = buildFinalOutputFromGraphResponse();
1412
- const result = {
1413
- jobId,
1414
- taskId: graphTaskId,
1415
- graphId: resolvedGraphId,
1416
- status: "failed",
1417
- outputsByNodeId,
1418
- stepsResponses,
1419
- engineSnapshot: engine.snapshot(),
1420
- ...(finalOutput !== undefined ? { finalOutput } : {}),
1421
- errors: [...errors, { error: err }],
1422
- graphAudit,
1423
- ...finalizeGraphPayload("failed"),
1424
- };
1425
- const debug = buildDebugTrace();
1426
- if (debug)
1427
- result.debug = debug;
1428
- if (eventEmitter) {
1429
- eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, err, {
1430
- finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1431
- ...(jobCorrelation ?? {}),
1432
- }));
1433
- }
1434
- return result;
1403
+ const entryGateEv = await evaluatePlanEntryGates(plan.deferredGates.entryGates, recordForStructuredDataFilters(), {
1404
+ executionMemory: currentExecution,
1405
+ jobMemory: currentJobMemory,
1406
+ taskMemory: currentTaskMemory,
1407
+ job,
1408
+ }, runxClient);
1409
+ if (!entryGateEv.ok) {
1410
+ const err = new Error("GRAPH_ENTRY_DATA_FILTERS_REJECTED: plan entry gates rejected runtime.executionMemory.input");
1411
+ err.code = ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED;
1412
+ logGraphEngineErrorCode(ExellixGraphErrorCode.GRAPH_ENTRY_DATA_FILTERS_REJECTED, err.message, {
1413
+ graphId: resolvedGraphId,
1414
+ jobId,
1415
+ taskId: graphTaskId,
1416
+ error: err,
1417
+ });
1418
+ const finalOutput = buildFinalOutputFromGraphResponse();
1419
+ appendExecutionEvent(executionTrace, {
1420
+ id: `evt:${graphTaskId}:graph.failed`,
1421
+ ts: new Date().toISOString(),
1422
+ level: "error",
1423
+ type: "graph.failed",
1424
+ message: err.message,
1425
+ });
1426
+ const result = {
1427
+ jobId,
1428
+ taskId: graphTaskId,
1429
+ graphId: resolvedGraphId,
1430
+ status: "failed",
1431
+ outputsByNodeId,
1432
+ stepsResponses,
1433
+ engineSnapshot: engine.snapshot(),
1434
+ ...(finalOutput !== undefined ? { finalOutput } : {}),
1435
+ errors: [...errors, { error: err }],
1436
+ planAudit,
1437
+ trace: executionTrace,
1438
+ ...finalizeGraphPayload("failed"),
1439
+ };
1440
+ const debug = buildDebugTrace();
1441
+ if (debug)
1442
+ result.debug = debug;
1443
+ if (eventEmitter) {
1444
+ eventEmitter.emit(createGraphFailEvent(jobId, resolvedGraphId, graphTaskId, err, {
1445
+ finalMemory: { jobMemory: currentJobMemory, taskMemory: currentTaskMemory, execution: currentExecution, outputsMemory: currentOutputsMemory },
1446
+ ...(jobCorrelation ?? {}),
1447
+ }));
1435
1448
  }
1449
+ return result;
1436
1450
  }
1437
1451
  while (true) {
1438
- const plan = engine.plan();
1439
- if (plan.status === "completed") {
1452
+ const wavePlan = engine.plan();
1453
+ if (wavePlan.status === "completed") {
1440
1454
  const graphStatus = errors.length ? "failed" : "completed";
1441
1455
  const finalOutput = buildFinalOutputFromGraphResponse();
1456
+ appendExecutionEvent(executionTrace, {
1457
+ id: `evt:${graphTaskId}:graph.${graphStatus}`,
1458
+ ts: new Date().toISOString(),
1459
+ level: graphStatus === "completed" ? "info" : "error",
1460
+ type: graphStatus === "completed" ? "graph.completed" : "graph.failed",
1461
+ message: `Graph execution ${graphStatus}.`,
1462
+ });
1463
+ validateExecutionTrace(executionTrace, plan);
1442
1464
  const result = {
1443
1465
  jobId,
1444
1466
  taskId: graphTaskId,
@@ -1450,7 +1472,8 @@ export function createExellixGraphRuntime(opts) {
1450
1472
  ...(finalOutput !== undefined ? { finalOutput } : {}),
1451
1473
  ...(errors.length === 0 ? { finalizerNodeId, finalizerType: finalizer.finalizerType } : {}),
1452
1474
  errors: errors.length ? errors : undefined,
1453
- graphAudit,
1475
+ planAudit,
1476
+ trace: executionTrace,
1454
1477
  ...finalizeGraphPayload(graphStatus),
1455
1478
  };
1456
1479
  const debug = buildDebugTrace();
@@ -1474,8 +1497,8 @@ export function createExellixGraphRuntime(opts) {
1474
1497
  }
1475
1498
  return result;
1476
1499
  }
1477
- if (plan.status !== "continue") {
1478
- const err = new Error(`GRAPH_BLOCKED: status=${plan.status}`);
1500
+ if (wavePlan.status !== "continue") {
1501
+ const err = new Error(`GRAPH_BLOCKED: status=${wavePlan.status}`);
1479
1502
  err.code = "GRAPH_BLOCKED";
1480
1503
  const finalOutput = buildFinalOutputFromGraphResponse();
1481
1504
  const result = {
@@ -1488,7 +1511,8 @@ export function createExellixGraphRuntime(opts) {
1488
1511
  engineSnapshot: engine.snapshot(),
1489
1512
  ...(finalOutput !== undefined ? { finalOutput } : {}),
1490
1513
  errors: [...errors, { error: err }],
1491
- graphAudit,
1514
+ planAudit,
1515
+ trace: executionTrace,
1492
1516
  ...finalizeGraphPayload("failed"),
1493
1517
  };
1494
1518
  const debug = buildDebugTrace();
@@ -1502,7 +1526,7 @@ export function createExellixGraphRuntime(opts) {
1502
1526
  }
1503
1527
  return result;
1504
1528
  }
1505
- let runnableNodes = plan.nextNodes ?? [];
1529
+ let runnableNodes = wavePlan.nextNodes ?? [];
1506
1530
  // Conditional edge filtering: a node with incoming edges that are *all* conditional must
1507
1531
  // have at least one incoming edge whose `when` evaluates true to be runnable this round.
1508
1532
  // Roots and nodes with at least one unconditional incoming edge always pass through.
@@ -1519,7 +1543,7 @@ export function createExellixGraphRuntime(opts) {
1519
1543
  const incoming = incomingByTo.get(id) ?? [];
1520
1544
  if (incoming.length === 0)
1521
1545
  return true;
1522
- const hasUnconditional = incoming.some((e) => e?.when == null);
1546
+ const hasUnconditional = incoming.some((e) => !e?.hasDeferredGate);
1523
1547
  if (hasUnconditional)
1524
1548
  return true;
1525
1549
  const planningContext = buildPredicateEvalContextForNode({
@@ -1529,7 +1553,10 @@ export function createExellixGraphRuntime(opts) {
1529
1553
  node: n,
1530
1554
  runtimeTaskVariables: runtime.taskVariables,
1531
1555
  });
1532
- return incoming.some((e) => evaluateGraphPredicate(e.when, planningContext));
1556
+ return incoming.some((edgeRef) => {
1557
+ const gate = plan.deferredGates.edgeGates[edgeRef.edgeId];
1558
+ return evaluateDeferredEdgeGate(gate, planningContext);
1559
+ });
1533
1560
  });
1534
1561
  }
1535
1562
  const dataFiltersRecord = recordForStructuredDataFilters();
@@ -1553,7 +1580,7 @@ export function createExellixGraphRuntime(opts) {
1553
1580
  node: n,
1554
1581
  runtimeTaskVariables: runtime.taskVariables,
1555
1582
  });
1556
- const condEv = await evaluateTaskNodeConditions(n.conditions, { ...conditionCtxBase, ...nodePredicateCtx }, dataFiltersRecord, { runx: runxClient });
1583
+ const condEv = await evaluateDeferredNodeGate(nodeDeferredGate(plan, String(n.id)), { ...conditionCtxBase, ...nodePredicateCtx }, dataFiltersRecord, runxClient);
1557
1584
  if (!condEv.ok) {
1558
1585
  skippedByConditions.push({
1559
1586
  node: n,
@@ -1603,27 +1630,32 @@ export function createExellixGraphRuntime(opts) {
1603
1630
  context: { model: graph, runtime, graphId: resolvedGraphId, jobId, taskId: graphTaskId, node: node },
1604
1631
  });
1605
1632
  const nodeTaskMemory = isPlainRecord(currentTaskMemory) ? { ...currentTaskMemory } : currentTaskMemory;
1606
- const effectiveModelConfig = await resolveModelConfigForNode({
1607
- nodeModelConfig: node.taskConfiguration?.modelConfig,
1608
- graphModelConfig: graph.modelConfig,
1609
- conditionCtx: buildPredicateEvalContextForNode({
1610
- executionMemory: currentExecution,
1611
- jobMemory: currentJobMemory,
1612
- taskMemory: currentTaskMemory,
1613
- node: node,
1614
- runtimeTaskVariables: runtime.taskVariables,
1615
- }),
1616
- executionInput: dataFiltersRecord,
1617
- runx: runxClient,
1618
- graphId: resolvedGraphId,
1619
- nodeId: typeof node.id === "string" ? node.id : String(node.id),
1620
- jobId,
1633
+ const nodeIdStr = String(node.id);
1634
+ appendExecutionEvent(executionTrace, {
1635
+ id: `evt:${graphTaskId}:node.started:${nodeIdStr}`,
1636
+ ts: new Date().toISOString(),
1637
+ level: "info",
1638
+ type: "node.started",
1639
+ nodeId: nodeIdStr,
1621
1640
  });
1641
+ const nodePlanEntryResolved = nodePlanForId(plan, nodeIdStr);
1642
+ const finalizerPlanEntryResolved = node.type === "finalizer"
1643
+ ? finalizerPlanForId(plan, nodeIdStr)
1644
+ : undefined;
1645
+ const taskNodeForExecute = nodePlanEntryResolved && node.type !== "finalizer"
1646
+ ? applyNodePlanInvoke(node, nodePlanEntryResolved)
1647
+ : node;
1648
+ const effectiveModelConfig = finalizerPlanEntryResolved != null
1649
+ ? graphAiModelConfigFromFinalizerPlan(finalizerPlanEntryResolved)
1650
+ : nodePlanEntryResolved && node.type !== "finalizer"
1651
+ ? graphAiModelConfigFromNodePlan(nodePlanEntryResolved)
1652
+ : undefined;
1653
+ const nodeExecutionPipeline = nodePlanEntryResolved != null ? executionPipelineFromNodePlan(nodePlanEntryResolved) : undefined;
1622
1654
  const r = await executeNode({
1623
1655
  graphId: resolvedGraphId,
1624
1656
  graph,
1625
1657
  graphRunTaskId: graphTaskId,
1626
- node,
1658
+ node: taskNodeForExecute,
1627
1659
  job,
1628
1660
  jobMemory: currentJobMemory,
1629
1661
  taskMemory: nodeTaskMemory,
@@ -1637,7 +1669,7 @@ export function createExellixGraphRuntime(opts) {
1637
1669
  runTaskIdentity: merged.runTaskIdentity,
1638
1670
  runTaskExecutionMode: merged.runTaskExecutionMode,
1639
1671
  runTaskDiagnostics: merged.runTaskDiagnostics,
1640
- graphExecutionPipeline: merged.executionPipeline,
1672
+ graphExecutionPipeline: nodeExecutionPipeline ?? merged.executionPipeline,
1641
1673
  skillKeyResolution: merged.skillKeyResolution,
1642
1674
  nodeTimeoutMs: merged.nodeTimeoutMs,
1643
1675
  clearSynthesizedContextPerNode: merged.clearSynthesizedContextPerNode,
@@ -1649,6 +1681,13 @@ export function createExellixGraphRuntime(opts) {
1649
1681
  outputsByNodeId[r.nodeId] = r.output;
1650
1682
  appendStepResponse(node, r.output);
1651
1683
  engine.commit({ nodeId: r.nodeId, output: r.output });
1684
+ appendExecutionEvent(executionTrace, {
1685
+ id: `evt:${graphTaskId}:node.completed:${nodeIdStr}`,
1686
+ ts: new Date().toISOString(),
1687
+ level: "info",
1688
+ type: "node.completed",
1689
+ nodeId: nodeIdStr,
1690
+ });
1652
1691
  const trl = r.taskRunLog;
1653
1692
  if (trl?.length)
1654
1693
  taskRunLogBuffer.push(...trl);
@@ -1720,7 +1759,8 @@ export function createExellixGraphRuntime(opts) {
1720
1759
  engineSnapshot: engine.snapshot(),
1721
1760
  ...(finalOutput !== undefined ? { finalOutput } : {}),
1722
1761
  errors,
1723
- graphAudit,
1762
+ planAudit,
1763
+ trace: executionTrace,
1724
1764
  ...finalizeGraphPayload("failed"),
1725
1765
  };
1726
1766
  const debug = buildDebugTrace();
@@ -5,6 +5,7 @@
5
5
  * a `{ model, runtime }` request with per-graph `GraphEntryContract` + seeded
6
6
  * `runtime.executionMemory` objects built here.
7
7
  */
8
+ import { compileExellixExecutablePlan } from '../adapters/compileExellixExecutablePlan.js';
8
9
  import { mergeGraphDocumentModel } from '../types/refs.js';
9
10
  import { createExellixGraphRuntime } from './ExellixGraphRuntime.js';
10
11
  import { selectByPath, writeByPath } from './pathExpr.js';
@@ -76,7 +77,7 @@ export function buildExecutionSeedFromGraphEntry(graphEntry, sources = {}) {
76
77
  /** Builds the canonical graph-engine execution request for matrix-style hosts. */
77
78
  export function buildGraphExecutionRequest(input) {
78
79
  return {
79
- model: input.model,
80
+ plan: compileExellixExecutablePlan(input.model, input.runtime),
80
81
  runtime: input.runtime,
81
82
  };
82
83
  }
@@ -0,0 +1,51 @@
1
+ import type { Graph } from '../types/refs.js';
2
+ import type { ExecuteGraphInput, GraphRuntimeObject } from './ExellixGraphRuntime.js';
3
+ /**
4
+ * Removed from the graphs-studio execute envelope (7.8.3+). No reader, no fallback.
5
+ * Default model selection belongs on {@link Graph}.modelConfig only.
6
+ */
7
+ export declare const STUDIO_GRAPH_EXECUTE_REMOVED_KEYS: readonly ["graphDefaultModel"];
8
+ export type StudioGraphExecuteRemovedKey = (typeof STUDIO_GRAPH_EXECUTE_REMOVED_KEYS)[number];
9
+ /**
10
+ * Graphs-studio / playground graph execute envelope before host adaptation to
11
+ * {@link ExecuteGraphInput}. Credential and simulate-only fields are host concerns;
12
+ * graph-engine validates shape and forbidden keys only.
13
+ */
14
+ export type StudioGraphExecuteRequest = {
15
+ mode: 'graph';
16
+ graph: Graph;
17
+ jobId: string;
18
+ runtime: Pick<GraphRuntimeObject, 'input' | 'inputs' | 'jobMemory' | 'taskMemory' | 'executionMemory' | 'outputsMemory' | 'variables' | 'jobVariables' | 'taskVariables' | 'nodes'>;
19
+ studioId?: string;
20
+ executionMode?: string;
21
+ runLogMode?: GraphRuntimeObject['runLogMode'];
22
+ maxRunLogEntries?: number;
23
+ maxRunLogDataJsonChars?: number;
24
+ graphExecution?: {
25
+ mode?: GraphRuntimeObject['mode'];
26
+ goalNodeId?: string;
27
+ dimension?: string;
28
+ };
29
+ runTaskDiagnostics?: GraphRuntimeObject['runTaskDiagnostics'];
30
+ exampleIndex?: number;
31
+ validateGraphEntrySeed?: boolean;
32
+ openrouterApiKey?: string;
33
+ };
34
+ export declare function getStudioGraphExecuteRemovedKeyViolations(request: unknown): StudioGraphExecuteRemovedKey[];
35
+ /**
36
+ * Rejects removed studio execute keys (notably `graphDefaultModel`).
37
+ * Hosts must persist model defaults on `graph.modelConfig` and stop sending request-level mirrors.
38
+ */
39
+ export declare function assertCanonicalStudioGraphExecuteRequest(request: unknown, context?: {
40
+ jobId?: string;
41
+ graphId?: string;
42
+ }): asserts request is StudioGraphExecuteRequest;
43
+ export type BuildGraphExecutionRequestFromStudioExecuteOptions = {
44
+ agentId?: string;
45
+ jobTypeId?: string;
46
+ };
47
+ /**
48
+ * Maps a validated studio execute envelope to {@link ExecuteGraphInput}.
49
+ * Does not read or merge removed request-level model defaults — graph.modelConfig is authoritative.
50
+ */
51
+ export declare function buildGraphExecutionRequestFromStudioExecute(request: StudioGraphExecuteRequest, options?: BuildGraphExecutionRequestFromStudioExecuteOptions): ExecuteGraphInput;
@@ -0,0 +1,78 @@
1
+ import { compileExellixExecutablePlan } from '../adapters/compileExellixExecutablePlan.js';
2
+ import { ExellixGraphError } from '../errors/ExellixGraphError.js';
3
+ import { ExellixGraphErrorCode } from '../errors/exellixGraphErrorCodes.js';
4
+ /**
5
+ * Removed from the graphs-studio execute envelope (7.8.3+). No reader, no fallback.
6
+ * Default model selection belongs on {@link Graph}.modelConfig only.
7
+ */
8
+ export const STUDIO_GRAPH_EXECUTE_REMOVED_KEYS = ['graphDefaultModel'];
9
+ function isPlainObject(v) {
10
+ return v != null && typeof v === 'object' && !Array.isArray(v);
11
+ }
12
+ export function getStudioGraphExecuteRemovedKeyViolations(request) {
13
+ if (!isPlainObject(request))
14
+ return [];
15
+ return STUDIO_GRAPH_EXECUTE_REMOVED_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(request, key));
16
+ }
17
+ /**
18
+ * Rejects removed studio execute keys (notably `graphDefaultModel`).
19
+ * Hosts must persist model defaults on `graph.modelConfig` and stop sending request-level mirrors.
20
+ */
21
+ export function assertCanonicalStudioGraphExecuteRequest(request, context) {
22
+ const removed = getStudioGraphExecuteRemovedKeyViolations(request);
23
+ if (removed.length > 0) {
24
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_STUDIO_GRAPH_EXECUTE_REQUEST, `Studio graph execute request must not include removed field(s): ${removed.join(', ')}. Model defaults belong on graph.modelConfig only; request-level graphDefaultModel was removed with no legacy fallback.`, { ...context, removedKeys: removed });
25
+ }
26
+ if (!isPlainObject(request)) {
27
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_STUDIO_GRAPH_EXECUTE_REQUEST, 'Studio graph execute request must be a plain object.', context);
28
+ }
29
+ if (request.mode !== 'graph') {
30
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_STUDIO_GRAPH_EXECUTE_REQUEST, 'Studio graph execute request mode must be "graph".', context);
31
+ }
32
+ const jobId = typeof request.jobId === 'string' ? request.jobId.trim() : '';
33
+ if (!jobId) {
34
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_STUDIO_GRAPH_EXECUTE_REQUEST, 'Studio graph execute request requires a non-empty jobId.', context);
35
+ }
36
+ if (!isPlainObject(request.graph)) {
37
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_STUDIO_GRAPH_EXECUTE_REQUEST, 'Studio graph execute request requires graph (GraphModelObject).', { ...context, jobId });
38
+ }
39
+ if (!isPlainObject(request.runtime)) {
40
+ throw new ExellixGraphError(ExellixGraphErrorCode.NON_CANONICAL_STUDIO_GRAPH_EXECUTE_REQUEST, 'Studio graph execute request requires runtime.', { ...context, jobId });
41
+ }
42
+ }
43
+ /**
44
+ * Maps a validated studio execute envelope to {@link ExecuteGraphInput}.
45
+ * Does not read or merge removed request-level model defaults — graph.modelConfig is authoritative.
46
+ */
47
+ export function buildGraphExecutionRequestFromStudioExecute(request, options) {
48
+ assertCanonicalStudioGraphExecuteRequest(request);
49
+ const jobId = request.jobId.trim();
50
+ const graphId = request.graph != null && typeof request.graph === 'object' && typeof request.graph.id === 'string'
51
+ ? request.graph.id
52
+ : undefined;
53
+ const runtime = {
54
+ jobId,
55
+ job: {
56
+ id: jobId,
57
+ jobId,
58
+ agentId: options?.agentId ?? 'standalone-agent',
59
+ jobTypeId: options?.jobTypeId ?? 'exellix-graph-job',
60
+ },
61
+ ...request.runtime,
62
+ ...(request.graphExecution?.mode != null ? { mode: request.graphExecution.mode } : {}),
63
+ ...(request.graphExecution?.goalNodeId != null
64
+ ? { goalNodeId: request.graphExecution.goalNodeId }
65
+ : {}),
66
+ ...(request.graphExecution?.dimension != null ? { dimension: request.graphExecution.dimension } : {}),
67
+ ...(request.runLogMode != null ? { runLogMode: request.runLogMode } : {}),
68
+ ...(request.maxRunLogEntries != null ? { maxRunLogEntries: request.maxRunLogEntries } : {}),
69
+ ...(request.maxRunLogDataJsonChars != null
70
+ ? { maxRunLogDataJsonChars: request.maxRunLogDataJsonChars }
71
+ : {}),
72
+ ...(request.runTaskDiagnostics != null ? { runTaskDiagnostics: request.runTaskDiagnostics } : {}),
73
+ };
74
+ return {
75
+ plan: compileExellixExecutablePlan(request.graph, runtime),
76
+ runtime,
77
+ };
78
+ }
@@ -0,0 +1,4 @@
1
+ import type { Graph, GraphModelObject } from '../src/types/refs.js';
2
+ import type { ExecuteGraphInput, GraphRuntimeObject } from '../src/runtime/ExellixGraphRuntime.js';
3
+ /** Builds `{ plan, runtime }` for tests and hosts that still author exellix GraphModelObject JSON. */
4
+ export declare function buildExecuteGraphInput(model: Graph | GraphModelObject, runtime: GraphRuntimeObject): ExecuteGraphInput;
@@ -0,0 +1,8 @@
1
+ import { compileExellixExecutablePlan } from '../src/adapters/compileExellixExecutablePlan.js';
2
+ /** Builds `{ plan, runtime }` for tests and hosts that still author exellix GraphModelObject JSON. */
3
+ export function buildExecuteGraphInput(model, runtime) {
4
+ return {
5
+ plan: compileExellixExecutablePlan(model, runtime),
6
+ runtime,
7
+ };
8
+ }
@@ -2,4 +2,5 @@ export { InMemoryGraphLoader } from './inMemoryGraphLoader.js';
2
2
  export { DepGraphEngineFactory } from './depGraphEngineFactory.js';
3
3
  export { RealTasksClient } from './RealTasksClient.js';
4
4
  export { createTestExellixGraphRuntime } from './testModelAliasRuntime.js';
5
+ export { buildExecuteGraphInput } from './buildExecuteGraphInput.js';
5
6
  export { tryLoadExellixAiTasksRuntimeSubtree, loadExellixGraphRuntimeObjects, } from './exellixRuntimeObjects.js';
@@ -2,4 +2,5 @@ export { InMemoryGraphLoader } from './inMemoryGraphLoader.js';
2
2
  export { DepGraphEngineFactory } from './depGraphEngineFactory.js';
3
3
  export { RealTasksClient } from './RealTasksClient.js';
4
4
  export { createTestExellixGraphRuntime } from './testModelAliasRuntime.js';
5
+ export { buildExecuteGraphInput } from './buildExecuteGraphInput.js';
5
6
  export { tryLoadExellixAiTasksRuntimeSubtree, loadExellixGraphRuntimeObjects, } from './exellixRuntimeObjects.js';
@@ -9,12 +9,12 @@ export function createTestExellixGraphRuntime(opts) {
9
9
  const base = input.runtime ?? {};
10
10
  const jobId = typeof base.jobId === 'string' && base.jobId.trim() !== ''
11
11
  ? base.jobId
12
- : `job-${input.model.id ?? 'graph'}`;
12
+ : `job-${input.plan.source.graphId}`;
13
13
  const job = base.job != null && typeof base.job === 'object'
14
14
  ? base.job
15
15
  : { agentId: 'test-agent', jobType: 'unit-test' };
16
16
  return executeGraph({
17
- ...input,
17
+ plan: input.plan,
18
18
  runtime: { ...base, jobId, job },
19
19
  });
20
20
  };