@exellix/graph-engine
v9.8.1
Published
Graph executor SDK
Maintainers
Readme
@exellix/graph-engine — Clean Graph Executor SDK
A minimal, focused SDK for executing graphs in the exellix ecosystem.
What this package does and does not
In scope — what this package does: On each createExellixGraphRuntime(...).executeGraph({ plan, runtime }) invocation, it runs exactly one graph run (validate plan → schedule waves → nodes → finalizer) until that run completes or fails, then returns the result. There is no batching, queueing, or multi-job orchestration inside this package — each call is one logical run. You supply a compiled v2 plan: ExecutableGraphPlanV2 (from @x12i/graphenix-plan-compiler or {@link compileExellixExecutablePlan}), planner (GraphEngineFactory), tasks client, and dynamic runtime: GraphRuntimeObject with the mandatory host jobId, job envelope, active input, memory, variables, and per-run options. The engine generates a fresh taskId (UUID) per invocation and sends it on every runTask request together with jobId. That is the entire product role of graph-engine.
Out of scope — what this package does not do: It does not schedule work, own execution matrices, manage claims or rows, persist job lifecycle, implement retry/requeue policy, or track runs across tenants or sessions. Optional helpers and docs for matrix hosts only help build arguments for the same single-run API; they do not move orchestration into this package. Integrations (e.g. Activix graph-run events) emit data for that call when you wire an eventEmitter — they do not make graph-engine a workflow or matrix service.
Core Responsibilities
exellix-graph-engine does exactly this loop:
- Validate executable plan: The caller supplies
plan: ExecutableGraphPlanV2(compiled upstream). The effective correlationgraphIdon the result and in telemetry isplan.source.graphId. executeGraph({ plan, runtime })- For each runnable node:
- Map node →
skillKey(strict rules) - If
skillKeyis a local skill (scoped-data-reader,deterministic-rule,scoped-answer-writer,scoped-answer-assembler), run it in-process (noai-taskscall); otherwise callai-tasks.runTask(...) - Commit output into graphenix
- Map node →
- Repeat until done/fail
Nothing else beyond orchestration, memory mapping, finalizers, and the small local-skill surface above — and not matrix/worker/queue ownership (see above).
Specification & documentation
| Topic | Document |
|--------|----------|
| Object contracts (model × runtime + per-call resolution view) | Index: formats-documentations/README.md — graph-model, graph-runtime, task-node-model, task-node-runtime, ExellixRuntimeObject |
| Executable graph JSON (top-level shape, task/finalizer nodes, edges, variables, metadata.graphExecution, canonical root enforcement, execution memory, local skills, Narrix / web scope (forwarded narrix → runTask), graph JSON vs outbound runTask) | .docs/exellix-graph-engine-format.md — start here for authors and schema tooling |
| Task node bridge (shell, metadata, executionPipeline, aiTaskProfile → Narrix web merge, composer alignment) | .docs/task-node-exellix-graph-engine-and-graph-composer.md |
| Layer 01 / 08 graph entry & response contracts | .docs/graph-io-visibility.md |
| Graph entry dataFilters v1 / public evaluator | .docs/data-filters-evaluation.md |
| Task-node conditions + conditional modelConfig.cases (runx) | .docs/task-node-conditions-evaluation.md |
| Model profile aliases (7.x+: graph = aliases / profile/choice; ai-tasks resolves per phase) | BREAKING-CHANGES.md §7.6 / §7.7, .docs/ai-tasks-model-profile-aliases-7x.md, .docs/graph-engine-ai-tasks-boundary.md |
| Platform vs implementation (no domain operators in schema) | .docs/platform-generic-vs-implementation.md |
| Activix records, runContext, collection tracking (@x12i/activix 8.4+) | .docs/activix-records.md |
| Bundled graph examples & bundle README | graphs/README.md |
The runTask wire contract (identity, canonical input, optional extra ai-tasks PRE/POST utility calls vs executionPipeline) is summarized later under Run identity and runTask request contract, and expanded in the graph format doc’s Graph JSON vs outbound runTask section. Graph-engine integrates with @exellix/ai-tasks at the semver range declared in package.json (currently ^8.6.8); use your lockfile as the tested line. Variable buckets align with ai-tasks ≥ 7.6.2 (two-scope passthrough). There is no minimum graph-engine ↔ ai-tasks matrix published inside ai-tasks — follow dependency semver and upstream CHANGELOG.md. Graph-engine does not import @exellix/xynthesis directly; @x12i/funcx is used only for optional runx (conditional edges / modelConfig.cases), not for ai-tasks execution strategies (those run via xynthesis inside ai-tasks 8.5+).
Canonical executable graph document (strict boundary)
An executable graph model object may have only these top-level keys: id, version, modelConfig, jobKnowledge, nodes, edges, variables, response, metadata. The required root response is the single executable final response contract used to build ExecuteGraphResult.finalOutput. Document-model and authoring fields (name, description, exellixContractTarget, graphExecution, graphEntry, catalogRequests, and similar) belong under metadata only. Node-scoped taskKnowledge belongs under each task node, not at the model root. Runtime state (input, jobMemory, taskMemory, executionMemory, outputsMemory, per-run options) belongs under the execution request’s runtime object and is rejected on the model. runtime.modelConfig, runtime.aliasConfig, and runtime.nodes[id].modelConfig were removed in 7.7 — model profiles belong on the graph document only.
metadata.graphExecution can document graph execution defaults and labels, including mode: 'forward' | 'backward' | 'hybrid', optional goalNodeId, optional dimension, outputMode: 'mappedAggregation' | 'lastExitNode', coreObjective, optional nodesResponses, and metadata-only flowOutline: 'linearSequence' | 'convergingParallelFlow'. Planner mode comes from plan.schedulingPolicy.mode or runtime.mode; outputMode does not decide the returned finalOutput.
Enforcement: executeGraph, createExellixGraphRuntime().executeGraph, executeNode when a graph is passed in context, inspectGraph, inspectGraphContracts, validateCatalogPlanning, and Catalox graph validators call this check. Failures throw ExellixGraphError with code NON_CANONICAL_GRAPH_DOCUMENT. For CI or custom loaders, call assertCanonicalGraphDocument (exported from the package root) on parsed JSON before execution. loadGraph returns whatever the loader parsed; validation runs when you execute or inspect, not necessarily on load.
Installation
npm install @exellix/graph-engineUpstream tasks SDK: This package depends on @exellix/ai-tasks ^8.6.8 and @x12i/funcx 4.4.4 (see package.json). Graph-engine emits RunTaskRequest shapes that match the ai-tasks 8.x closed schema (mandatory executionStrategies, three-slot modelConfig, xynthesized, optional smartInput, no legacy root mirrors). Pin compatible versions in your app lockfile. Optional CI improvement: fail or warn when the resolved ai-tasks major/minor drifts outside an allowlist (not enforced in-repo today).
Execution matrix hosts (@x12i/exellix-runtime) — documentation only for the engine
Matrix claim, rows, and retry policy live outside this package. If your host wires matrix → graph run, see .docs/execution-matrix-handoff.md for how you should inject runtime.executeGraph, compile GraphModelObject → plan, resolve metadata.graphEntry per model, seed runtime.executionMemory, and pass runtime.jobId. Helpers such as buildMatrixJobForGraphRun align id and job.jobId on the job object so you can call runtime.executeGraph({ plan, runtime: { jobId: job.jobId, job, … } }). Those exports are optional helpers for callers; they do not expand graph-engine’s role beyond executing the single run when invoked.
Configuration (.env)
- Template:
.env.example— copy to.envat the project root for local tests and scripts that loaddotenv. - Semantics:
.docs/environment-and-xmemory-databases.md(must-have vs nice-to-have, Mongo / xmemory / Narrix / Activix). - Resolvable path after install: subpath export
@exellix/graph-engine/env.examplepoints at the same file (e.g.require.resolve('@exellix/graph-engine/env.example')in Node).
Package entrypoints
@exellix/graph-engine— Graph executor, types, loaders, integrations (platform).@exellix/graph-engine/testkit— Harness helpers (InMemoryGraphLoader,DepGraphEngineFactory,RealTasksClient) and sampleregisterNarrixGraphTasksfor repo graphs that still callnarrix/load-input,narrix/to-cni, etc. This is not re-exported from the root entry.
Catalox and graph planning catalog IDs
Skill templates and Catalox wiring for packaged runs live in @exellix/ai-tasks (and its upstream stack). This package only offers an optional planning-descriptor check when you already have a Catalox client:
validateGraphPlanningCatalogDescriptorsInCatalox(graph, catalox, ctx)— For everycatalogIdcollected frommetadata.catalogBinding/ planning metadata (seegetGraphCatalogs), assertscatalox.getCatalogDescriptor(ctx, catalogId)is non-null.
For Firebase Admin, createCataloxFromEnv, listAiSkillsCatalogItems, and related helpers, import from @exellix/ai-tasks or the graph-engine re-exports below instead of adding @exellix/ai-skills here.
taskConfiguration.aiTasksOutputValidation (shape: { schema, mode?, validateWhenMissing? }) is forwarded on runTask as outputValidation for server-side checks in @exellix/ai-tasks. The top-level node field outputValidation with rules remains a local post-check in executeNode only.
Task-node preflight (validation & analysis, no runTask)
Graph-engine exposes the @exellix/ai-tasks preflight surface so studios and matrix hosts can validate a node before execution without a second dependency on ai-tasks:
| Export | Purpose |
|--------|---------|
| buildTaskNodeRunTaskRequest | Build the same outbound RunTaskRequest as executeNode (skips local skills and finalizers). |
| validateTaskNodeRunTaskConfig | Static config checks (agentId, pipeline, smartInput, llmCall, …). |
| validateTaskNodeRunTaskInvoke | Config + payload path resolution + optional template/smart-input render checks. |
Lower-level helpers (validateRunTaskConfig, validateRunTaskInvoke, analyzeExpectedRunTaskInput, Rendrix listTokens / analyzeTemplateResolution, …) are re-exported from the package root when you already have a RunTaskRequest. Catalox-backed skill invoke packet analysis was removed from @exellix/ai-tasks 8.6 — use @exellix/graph-composer for studio / skills-manager invoke preflight (see @exellix/ai-tasks documenations/studio-skill-invoke-preflight.md).
Testing safety: Default npm test uses mocked Catalox in catalog validation tests and does not open Firestore. Do not point FIRESTORE_LIVE_TESTS / integration flags at a production Firebase project; Catalox’s own docs recommend a dedicated test project for live integration runs.
Quick Start
import { compileExellixExecutablePlan, createExellixGraphRuntime } from '@exellix/graph-engine';
const runtime = createExellixGraphRuntime({
graphLoader: myGraphLoader,
engineFactory: myEngineFactory, // GraphEngineFactory (e.g. DepGraphEngineFactory)
tasksClient: myTasksClient, // TasksClientLike — responses may use `ok` or `success` (both accepted)
});
// graphModel carries static modelConfig (profile aliases on the graph document — not on runtime).
const graphModel = {
id: 'my-graph',
modelConfig: {
cases: [{
modelConfig: {
preActionModel: 'cheap',
skillModel: 'balanced',
postActionModel: 'cheap',
},
}],
},
nodes: [/* … */],
// …
};
const graphRuntime = {
jobId: 'job-123',
job: { agentId: 'agent-1', input: {} },
input: { question: 'Analyze this record' },
};
const plan = compileExellixExecutablePlan(graphModel, graphRuntime);
// Host correlation id (required). Engine sets `job.id` / `job.jobId` from it and generates `result.taskId`.
const result = await runtime.executeGraph({
plan,
runtime: graphRuntime,
});
// Canonical business output + per-run ids:
console.log(result.finalOutput, result.jobId, result.taskId);Public API
runtime.executeGraph({ plan, runtime })
Execute a complete graph through the single canonical client API: createExellixGraphRuntime(...). The runtime owns local-skill interception, conditional edge filtering, optional eventEmitter, optional debugMode, and produces one ExecuteGraphResult shape.
Every call requires a compiled v2 plan: ExecutableGraphPlanV2 and a dynamic runtime: GraphRuntimeObject. Hosts compile authoring graphs with @x12i/graphenix-plan-compiler (or {@link compileExellixExecutablePlan} for legacy exellix GraphModelObject) before invoke. runtime.jobId is mandatory and non-empty. The engine also generates a taskId (UUID) per invocation. Together they form the identity forwarded to @exellix/ai-tasks (runTask({ jobId, taskId, … })), graph/node eventEmitter payloads, structured runLog, and Activix runContext / record metadata.
import { createExellixGraphRuntime } from '@exellix/graph-engine';
const runtime = createExellixGraphRuntime({
graphLoader,
engineFactory,
tasksClient,
eventEmitter, // optional graph/node lifecycle events
playgroundReporter, // optional
runxCreateOptions, // optional — lazy runx + funcx when graph needs conditional modelConfig / conditions
// …stepRetryPolicy, runLogMode, concurrency, runTaskDiagnostics, etc.
});
interface GraphExecutionRequest {
model: GraphModelObject; // static graph definition
runtime: GraphRuntimeObject; // dynamic run state
}
// 2x2 object split:
// Graph model: GraphModelObject
// Graph runtime: GraphRuntimeObject
// Task-node model: TaskNode
// Task-node runtime: TaskNodeRuntimeObject at runtime.nodes[nodeId]
// Task-node runtime: TaskNodeRuntimeObject at runtime.nodes[nodeId] — extensible bag;
// modelConfig / aliasConfig on runtime.nodes were removed in 7.7.
interface GraphRuntimeObject {
jobId: string; // required: host correlation id
job: any; // host envelope: agentId, input, jobType, …
input?: Record<string, any>; // active execution input
jobMemory?: any;
taskMemory?: any;
executionMemory?: any;
variables?: Record<string, any>;
/** Per-node runtime bag (must not carry modelConfig or aliasConfig since 7.7). */
nodes?: Record<string, Record<string, unknown>>;
mode?: 'forward' | 'backward' | 'hybrid';
goalNodeId?: string; // required when mode === 'backward'
debugMode?: boolean; // include per-node trace on result.debug
failFast?: boolean; // default: false
// …stepRetryPolicy, runLogMode, runtimeObjects, runTaskDiagnostics, etc.
}Return shape
interface ExecuteGraphResult {
jobId: string;
taskId: string;
graphId: string;
status: 'completed' | 'failed';
finalOutput?: unknown;
finalizerNodeId?: string;
finalizerType?: string;
outputsByNodeId: Record<string, unknown>;
stepsResponses: Record<string, unknown>[];
engineSnapshot: unknown;
errors?: Array<{ nodeId?: string; error: unknown }>;
execution?: unknown; // includes _trace.nodes
runLog?: RunLogEntry[];
runLogTruncated?: boolean;
runLogOmittedCount?: number;
logxerCorrelationId?: string;
debug?: { nodes: NodeTraceEntry[] }; // populated only when debugMode: true
planAudit?: {
planHash: string;
source: string;
};
trace?: unknown;
}Host HTTP handlers or workers should accept the same GraphExecutionRequest shape in the request body so payloads stay aligned with runtime.executeGraph.
Memory-only playground reporter
Use createPlaygroundReporter() when you want rich per-run debugging without filesystem output. The reporter is in-memory only: it does not accept an output directory, does not expose writeReport, and never writes request/response payloads to playground/, reports/, or any other path.
import { createExellixGraphRuntime, createPlaygroundReporter } from '@exellix/graph-engine';
const playgroundReporter = createPlaygroundReporter({ runId: 'local-debug-run' });
const runtime = createExellixGraphRuntime({
graphLoader,
engineFactory,
tasksClient,
playgroundReporter,
});
const plan = compileExellixExecutablePlan(graphModel, graphRuntime);
await runtime.executeGraph({
plan,
runtime: graphRuntime,
});
// Inspect full node request/response payloads in memory.
const artifacts = playgroundReporter.getArtifacts();
const snapshot = playgroundReporter.getDebugSnapshot();
const markdown = playgroundReporter.getMarkdown();getArtifacts() returns entries such as 01-<nodeId>-request and 01-<nodeId>-response with the full payload attached as payload. Use getDebugSnapshot() when a UI, debugger panel, or test wants steps, artifacts, and rendered markdown as one object.
Removed in 5.0: the legacy functional executeGraph from runtime/executeGraph, the ExellixGraphClient class, and their option/result types (ExecuteGraphOptions, ExecuteGraphResponse, ExecuteGraphFinalizedResponse, ExecuteGraphDebugResponse, GraphExecutionResult). Migrate to createExellixGraphRuntime(...).executeGraph(...) — the runtime now covers the same semantics and exposes the same diagnostics through debugMode: true. See BREAKING-CHANGES.md.
Run identity (host jobId and engine taskId)
jobId(mandatory): You must passruntime.jobId: stringon theexecuteGraphinput. It must be non-empty after trim. If it is missing or blank, the call fails withExellixGraphErrorCode.JOB_ID_REQUIRED. The runtime setsjob.idandjob.jobIdto this value for the duration of the run (so templates, events, and memory see a consistent id even ifjobomittedidon input).taskId(mandatory on the wire, generated here): At the start of eachexecuteGraph, the engine allocatestaskId = randomUUID(). The same value is attached to everyrunTaskrequest in that run (including synthesize-finalizer paths) astaskId, and is returned onExecuteGraphResult.taskId. This satisfies downstream expectations that each graph execution has a stable per-run task identity distinct from the hostjobId.Results:
ExecuteGraphResultincludes bothjobIdandtaskIdso callers and logs can correlate host scope vs engine run instance.Activix (
@x12i/activix8.4+): Graph-run and node integrations pass a top-levelrunContexton everystartRecord(sessionId=jobId, plustaskId,graphId, and for nodesnodeId/skillKey). Activix owns collection tracking state (track/offon legend rows) — graph-engine always calls lifecycle APIs; do not branch on persistence in app code. In-process maps correlate start → complete/fail byactivityId(jobId:graphId:taskIdfor graph runs;graphId:nodeId:taskIdfor nodes). Query rows viaactivixClient.getJobActivities(see Activix integration).Helpers:
assertHostJobIdandnewGraphRunTaskIdare exported from the package root for hosts/tests that build inputs outsideexecuteGraph.
Standalone node debugging: The runtime also exposes runtime.executeNode(...) for single-node test runs. Provide node, job, optional graph / execution, and (when continuing an existing run) graphRunTaskId from the parent ExecuteGraphResult.taskId so runTask and Activix stay aligned.
runTask request contract (@exellix/ai-tasks 8.x)
Graph-engine builds a canonical RunTaskRequest for every outbound task call it owns: MAIN task-node invokes, engine PRE/POST utility invokes, and synthesize finalizer invokes. MAIN request assembly lives in src/runtime/buildAiTasksRunTaskRequest.ts; all paths follow RUNTASK_REQUEST.md in @exellix/ai-tasks. Types align with RunTaskRequest / ExellixGraphRunTaskRequest exported from this package.
Identity and payload
- Required correlation:
agentId,jobTypeId,taskTypeId(in addition toskillKey,input). Defaults:jobTypeId←job.jobTypeorjob.jobTypeIdorexellix-graph-job;taskTypeId←node.taskConfiguration.taskTypeIdor the node’sskillKey. - Canonical task payload:
inputobject only (merged execution slice + materializednode.inputs). Root-levelquestion,raw,jobInput, duplicateinputs, and legacyexecutionTypeare not sent on the request object. - Graph telemetry:
graphId,nodeId,coreSkillId(node id),masterSkillId,masterSkillActivityId,jobId,taskId, optionalidentity.
To build RunTaskRequest without fallback defaults, the execution request must provide both sides of the contract: model.id, node.id, node.skillKey, explicit node.taskConfiguration.taskTypeId (even when it matches skillKey), node.taskConfiguration.executionStrategies (use [] for plain MAIN), runtime.jobId, runtime.job.agentId, runtime.job.jobTypeId or runtime.job.jobType, active input/memory, and any model/LLM/diagnostic options needed by the task.
Model profiles (7.x+): Graph and node modelConfig carry profile alias names only (cheap, balanced, deep, or profile/choice keys like cheap/default, cyber/deep_forensics) — never provider model ids in graph JSON. Since 7.7, model selection is graph-document only: merge node.taskConfiguration.modelConfig (partial override per slot) over model.modelConfig, then engine defaults (cheap / balanced / cheap). Graph-engine forwards the three-slot triplet { preActionModel, skillModel, postActionModel } on every outbound runTask; @exellix/ai-tasks resolves aliases via @x12i/ai-profiles. 7.7.8+ execute-mode validation accepts partial per-task overrides and profile/choice encoding without host-side pre-merge. See BREAKING-CHANGES.md §7.6 / §7.7 / §7.7.8.
Graph-engine still derives correlation fields such as graphId, nodeId, coreSkillId, masterSkillId, taskId, and masterSkillActivityId from those authored values.
Mandatory executionStrategies (breaking vs pre–v7 authoring)
- Every MAIN (and engine PRE/POST utility)
runTaskincludesexecutionStrategies: an array ofExecutionStrategyInvocationobjects (semantics defined by@exellix/ai-tasks/RUNTASK_REQUEST.md). - Plain MAIN (no wrappers): graph-engine sends
executionStrategies: []. - Optional task-node authoring:
taskConfiguration.executionStrategies— when present and non-empty, it overrides the default[]for that node’s MAIN call. - Optional catalog metadata:
taskConfiguration.executionStrategyCatalogItemsis forwarded toai-tasks; planner/optimizer rows useruntimeKind: "xynthesis-action"in ai-tasks 8.5+ (not funcx).ai-tasksstill validates the runtime invocation shape and consumes only safe catalog fields such as wrapper default sidekick actions. - Removed in 5.0:
metadata.executionStrategyKeytyping and code branches. Configure planners/optimizers throughexecutionStrategiesper ai-tasks (see upstream docs / Catalox task-strategy catalogs).
xynthesized and internal execution.xynthesized
- Outbound
runTask.xynthesized:{ job, task }snapshot from durable graph-engine memory —job=execution.xynthesized.job,task=execution.xynthesized.taskByNode[nodeId](never another node’s task bucket). - After a successful MAIN
runTask, graph-engine deep-mergesresponse.xynthesizedPatchintoexecution.xynthesized(patch.job→job,patch.task→taskByNode[nodeId]). - On graph start,
seedGraphRunExecutionStateensuresexecution.xynthesizedexists withjobandtaskByNodeobjects.
smartInput
- Optional task-node field
smartInput(paths: string[], optionalstrict) is forwarded onRunTaskRequest.smartInputwhen set. Paths are validated against graph-engine allowlists (see catalog planning /validateAiTasksNodeExtensions).
Optional taskConfiguration → runTask (strategy / Narrix)
Forwarded from taskConfiguration: narrixMode, inputStrategyKey, narrixInput, and executionStrategyCatalogItems. If both taskConfiguration.narrix and taskConfiguration.narrixInput are set, set taskConfiguration.narrixMode to preprocessor or handler (see format doc).
Input bindings before runTask
Values in node.inputsConfig (or deprecated node.inputs) shaped as { type: 'executionMemoryPath', path } (optional optional: true) or { $path: '…' } are resolved against the live execution object before building runTask.input, so chained graphs see concrete values (e.g. graphOutputs.*) instead of binding objects.
Graph-run execution seeding
When executeGraph / createExellixGraphRuntime().executeGraph starts, graph-engine seeds runtime.executionMemory.xynthesized and mirrors runtime.input to runtime.executionMemory.input (flat fields on the object; no input.raw wrapper).
Where hosts put the record: .docs/graph-execution-record-input.md — peer contract (runtime.input only, flat paths, matrix vs BFF).
Model knowledge references are resolved inside graph-engine without changing the ai-tasks wire contract. The merge happens when graph-engine builds the outbound RunTaskRequest: model.jobKnowledge is applied to the request copy at jobMemory.knowledge, and node.taskKnowledge is applied to that task node's request copy at taskMemory.knowledge. The shared runtime.jobMemory / runtime.taskMemory objects are not rewritten just to attach model knowledge.
Scope matters: jobKnowledge is graph-run scoped and is available to every task request through jobMemory.knowledge. taskKnowledge is node/task scoped and must be declared on the task node; root model.taskKnowledge is not part of the graph model contract.
PRE inputSynthesis (authoring → pipeline)
When taskConfiguration.aiTaskProfile.inputSynthesis is enabled, graph-engine may synthesize or merge a PRE synthesized-context step (still one outbound MAIN runTask). Conflicts with an explicit node.executionPipeline that already defines PRE synthesis are rejected at runtime / validation with INPUT_SYNTHESIS_PIPELINE_CONFLICT. Details: .docs/exellix-graph-engine-format.md.
Three layers (do not confuse)
- Engine PRE/POST strategy utilities (ai-tasks only):
taskConfiguration.aiTaskProfile.preStrategyKey/postStrategyKey→ extrarunTaskcalls via@exellix/ai-tasksbefore/after MAIN; outputs stored atexecution.xynthesis.pre/execution.xynthesis.post(historical slot names). executionPipelineinside ai-tasks: e.g. PREsynthesized-context+ MAIN direct — still one outbound MAINrunTaskhandled inside@exellix/ai-tasks.- Narrix web scope inside ai-tasks: when
taskConfiguration.aiTaskProfile.webScoping.enabledis true, graph-engine forwards anarrixpayload withenableWebScope/webScopeQuestions. The actual web fetch + skip rules run inside@exellix/ai-tasks(@x12i/narrix-web-scoper); graph-engine has no local web phase in 5.x.
synthesize finalizers are a fourth outbound task-call shape: the finalizer is still a graph model node, but its terminal utility runTask uses the same run identity, diagnostics, llmCall, and resolved model config as the rest of the graph run.
Finalizer nodes & Final Response
Executable graphs must include exactly one terminal finalizer node (type: "finalizer") with no outgoing edges.
Required finalizer reads must resolve from the selected memory lane. A non-optional executionMemoryPath read must either be seeded by runtime.executionMemory before the run starts or be written by a reachable upstream task node through executionMapping.path. A non-optional outputsMemoryPath read must either be seeded by runtime.outputsMemory or be written by a reachable upstream task node through outputMapping.path. If the value may be absent because a branch is conditional, mark that finalizer input, section, or item optional: true.
The finalizer is a terminal computation or fan-in barrier. It does not own the returned API response. The returned ExecuteGraphResult.finalOutput is always resolved from root model.response after node executionMapping/outputMapping writes and finalizer/barrier execution complete:
type GraphResponseDefinition = {
missing?: 'omit' | 'null';
shape: unknown;
};Supported selectors are outputsMemoryPath, executionMemoryPath, executionPath, nodeMetadata, nodeInputsConfig, literal, and firstPresent. Legacy nodeInputs is still accepted. Missing selector values are omitted by default; missing: "null" returns null for missing mapped fields.
Example:
{
"response": {
"missing": "omit",
"shape": {
"answer": {
"type": "executionMemoryPath",
"path": "answers.q1.shortAnswer"
}
}
}
}metadata.graphExecution.outputMode, finalizer output, and metadata.graphResponse.responseMapping are not final-output selectors. Graphs must declare root graph.response at persist time; graph-engine does not hoist or migrate legacy response sidecars at compile.
coreObjective is resolved once from graph/run context, not from each node response. Supported sourcePath roots are input.*, execution.*, variables.* / jobVariables.*, taskVariables.*, jobMemory.*, taskMemory.*, and job.*.
The selected canonical business output is exposed as:
ExecuteGraphResult.finalOutput(the only API in 5.x).
Deterministic finalizers (runtime-owned)
Implemented deterministic finalizer types:
aggregatebundleselect
aggregate — strategy: "object-map"
Build an object by mapping named inputs (from executionMemoryPath or literals) into output keys.
aggregate — strategy: "report-schema" (output object assembled from run memory)
Builds the finalizer output object: config.sections maps each output field key to a memory read, using the same { type, path } idiom as finalizer inputs and response.shape selectors:
"config": {
"strategy": "report-schema",
"sections": {
"q1": { "type": "executionMemoryPath", "path": "answers.q1", "optional": true },
"summary": { "type": "outputsMemoryPath", "path": "report.summary" }
},
"collectEpistemicTags": true
}- Each section:
type(executionMemoryPath|outputsMemoryPath),path, andoptional(whentrue, missing values becomenullinstead of throwing). collectEpistemicTags: whentrue, walks all section values and collects unique epistemic strings amongCONFIRMED,INFERRED,ASSUMED,UNKNOWNinto thecollectedTagsoutput field (array).
Breaking (model v8):
sections[].path: string→sections[].{ type, path };collect_tags→collectEpistemicTags; outputcollected_tags→collectedTags. Sectiontitleand the literal-mergemetablock were removed — authoring titles and studio hints belong in the studio document, and literal output fields belong ingraph.response.shape. The validator rejects the old keys with a migration message.
Bundled graphs under graphs/ (see graphs/README.md) use this strategy for multi-section reports, sometimes after scoped-answer-assembler and scoped-answer-writer persistence (typical store: x-scoped-data in deployments that use that collection). The graph file shape is defined in .docs/exellix-graph-engine-format.md.
aggregate — strategy: "question-driven" (generic Q→A formatting)
For “question-driven” graphs, question-driven formats the final output by pairing:
- question text from the referenced node definition (default path:
inputs.question) - answer from
executionMemoryat the referenced node’soutputMapping.path(or an explicitanswerPath)
Example:
{
"id": "finalize",
"type": "finalizer",
"finalizerType": "aggregate",
"inputs": {},
"config": {
"strategy": "question-driven",
"contractVersion": "1",
"items": {
"exploitability": { "nodeId": "q2-exploitability" },
"exposure": { "nodeId": "q5-exposure" },
"posture": { "nodeId": "q6-posture" }
}
}
}Output shape (per item key):
{
"exploitability": { "question": "...", "answer": { /* whatever the node mapped */ } }
}Activix integration (graph run record)
Graph-engine depends on @x12i/activix (see package.json, currently 8.4.x). Full wiring guide: .docs/activix-records.md. Activix README (collection tracking, getJobActivities, phased outer / inner I/O): node_modules/@x12i/activix/README.md.
Wiring (recommended)
import {
createActivixFromEnv,
createActivixExellixIntegration,
createExellixGraphRuntime,
resolveActivixExellixCollectionNamesFromEnv,
} from '@exellix/graph-engine';
import { buildExellixGraphRuntimeObjects } from '@exellix/graph-engine/testkit';
const ax = createActivixFromEnv({ strict: true });
await ax.init();
const { graphRuns, nodeActivity } = resolveActivixExellixCollectionNamesFromEnv();
const { eventEmitter, activixClient } = createActivixExellixIntegration(ax, {
activixGraphRun: { collection: graphRuns },
activixNodeActivity: {
collection: nodeActivity,
includeInputSnapshot: true,
extractNarrixOutcome: true,
},
});
const runtime = createExellixGraphRuntime({
graphLoader,
engineFactory,
tasksClient,
eventEmitter,
});
const result = await runtime.executeGraph({
model: graph,
runtime: {
jobId: 'job-123',
job: { agentId: 'agent-1', jobType: 'analysis' },
input: entryInput,
runtimeObjects: buildExellixGraphRuntimeObjects({ graphActivixClient: activixClient }),
},
});
// Playground / debug UI — no direct Mongo:
const activities = await activixClient.getJobActivities({
jobId: result.jobId,
graphId: result.graphId,
limit: 500,
});Exports: createActivixFromEnv, resolveActivixExellixCollectionNamesFromEnv, createActivixExellixIntegration, createActivixGraphRunIntegration, createActivixNodeActivityIntegration, createActivixQueryableClient, ensureActivixInitialized, ensureActivixClientOnRuntimeObjects, buildGraphRunActivixRunContext, buildNodeActivixRunContext.
Collection tracking state (Activix 8.4+)
Legend rows in activix-collections may set state: 'track' | 'off'.
| Value | Meaning |
|-------|---------|
| track (default) | Mongo persistence for that collection |
| off | Same APIs; Activix skips Mongo (ephemeral in-process rows only) |
Always call startRecord / completeRecord / failRecord from integrations — never skip Activix in graph-engine when off. Toggle at runtime: ax.setCollectionTrackingState(collectionName, 'off' | 'track'). After external DB edits: ax.refreshCollectionTrackingStates().
createActivixFromEnv() registers both exellix streams with runContext indexes and collectionRegistry.owner = @exellix/graph-engine. Collection names default to exellix-graph-runs and exellix-node-activity (override with ACTIVIX_GRAPH_RUNS_COLLECTION / ACTIVIX_NODE_ACTIVITY_COLLECTION).
Record shape (graph run)
When using createActivixGraphRunIntegration() / createActivixExellixIntegration():
- Top-level
runContextonstartRecord(not only nested inside custom fields). - Canonical response at
outer.output.response— only the graph’sfinalOutput. - Diagnostics under
outer.output.data(nodes, execution, jobMemory, errors, …). - Top-level
metadataon the row for orchestrator observability (e.g.graphNodeCount,inputVariableKeys);outer.metadatacarrieskind/type(exellix-graph-run,graph-run).
Graph start — bounded input summaries (Activix)
On graph:start, the graph-run integration does not persist raw data.input / data.request (no shallow copy of variables, full GraphExecutionRequest, or opaque host objects). Activix receives JSON-serializable summaries under outer.input:
inputSummary— bounded shape for the event’sinput(e.g.variables/ memory handles as key counts, shallow primitive previews, nested samples).requestSummary— same for the mergedrequestobject emitted with the start event.
Correlation fields (jobId, graphId, agentId, jobType, inputVariableKeys, runContext, etc.) are unchanged.
Recommended for job / GraphExecutionRequest / event data (Activix-safe): JSON-like primitives, plain objects/arrays, string ids, agentId, jobType, and small metadata you are willing to see reflected in summaries.
Not suitable to rely on for Activix persistence at graph start: functions, class instances, streams, Buffers, live clients (HTTP/DB), circular graphs, or very large nested payloads — they are summarized or typed (e.g. shape: "function", shape: "instance") and never passed through to startRecord as raw references. For large memory, use includeMemorySnapshots: true only when you explicitly accept storing outer.memory.start / end; that remains opt-in and separate from outer.input.
Logxer integration (runtime logs)
Graph-engine uses @x12i/logxer (^5.1.0) for structured logs and in-process getJobLogs (playground Logger, diagnostics).
- Per-run logger: Each
executeGraphcreates a run-scoped Logxer (createGraphEngineLogxer({ logging })) and binds it for the duration of the run. - Stack pass-through: Pass
logging?: StackLoggingOptionsoncreateExellixGraphRuntimeand/or perexecuteGraphto control downstream packages (GRAPH_ENGINE,AI_TASKS, …). The run is wrapped inrunWithAiTasksStackLoggingfrom@exellix/ai-tasks. - Service filter (Logger UI): Top-level
runtimeIdentity.serviceidentifies the component that emitted the line. Graph-engine native events usegraph-engine. Proxied downstream records keep the origin service (e.g.ai-skills,@exellix/ai-tasks) with wrapper attribution inproxyRuntimeIdentity— do not rely ondata.runtimeIdentity.servicefor filtering. - Env:
GRAPH_ENGINE_LOGS_LEVEL(canonical); bulk stack levels viaLOGXER_PACKAGE_LEVELS(see@x12i/logxerdocs).
Node-only Logxer exports: import from @exellix/graph-engine/runtime/logxer (not the root barrel — avoids pulling Logxer into browser bundles):
createGraphEngineLogxer, getGraphEngineLogxer, createGraphEngineLogxerClient, ensureGraphEngineLogxerOnRuntimeObjects, runGraphWithLogContext, GRAPH_ENGINE_RUNTIME_SERVICE, normalizeGraphEngineLogMeta.
Browser-safe subpaths: @exellix/graph-engine/inspection, @exellix/graph-engine/authoring (authoring gates such as isAuthoringGraphDocument — no Logxer).
Wire query surface on runtime.runtimeObjects.logxerClient (same pattern as Activix — see buildExellixGraphRuntimeObjects in testkit).
runtime.executeNode(input)
Execute a single node — useful for tests and for stepping through a node in the context of an existing graph run. Pass node, job, optional graph / execution, and (when continuing a parent run) graphRunTaskId from the parent ExecuteGraphResult.taskId so runTask correlation, Activix records, and runLog lines stay aligned.
loadGraph(graphId)
Load a graph through the injected loader and validate against the canonical document schema before returning.
Graph Entry
Graphs declare execution contract under metadata.graphEntry. Data source selection is an operator/work-factory concern (chosen in jobs-ui at queue time); graphs do not embed input-type or entity-id declarations.
{
"id": "network-vuln-triage.v1",
"metadata": {
"graphEntry": {
"summary": "Triage one vulnerability record with an optional caller query.",
"requiredExecutionPaths": ["input.subnetId"]
}
},
"nodes": []
}Use requiredExecutionPaths to declare dot-paths the first-wave nodes expect under runtime.input. Paths are relative to GraphRuntimeObject.executionMemory after runtime.input is mirrored (input.* root).
Node Inputs
Default (graph-engine ≥ 8.x): Omit inputsConfig on task nodes when the skill reads flat fields from host runtime.input. Templates such as {{input.cveId}} resolve against flat RunTaskRequest.input.
Optional inputsConfig aliases values from executionMemory into named skill input keys (e.g. upstream answers). Legacy opt-in: sole record → input nests the host record under runTask.input.record — use only when the skill catalog documents input.record.* templates.
{
"id": "q1-reachability",
"skillKey": "professional-answer",
"taskVariable": {
"question": "Is this asset reachable from outside the perimeter?"
}
}Execution-memory bindings (chaining): Values in node.inputsConfig (or deprecated node.inputs) shaped as { type: 'executionMemoryPath', path } (optional optional: true) or { $path: '…' } are resolved against the live execution object before building runTask.input, so chained graphs see concrete values (e.g. graphOutputs.*) instead of binding objects.
Optional smartInput: Top-level on the task node (sibling to inputsConfig): { "paths": ["input", "graphOutputs.prev"], "strict": true } — forwarded on RunTaskRequest.smartInput; paths must satisfy graph-engine allowlists.
Rules:
node.taskVariable.question— the question text sent to the skill template. Always set it undertaskVariable(notinputsConfig).- Per-node dynamic values from execution memory belong in
inputsConfigwhen aliasing is required; omitinputsConfigfor flat host input passthrough. - Path references in
executionMappinguse execution-memory paths written by upstream nodes. - Do not use deprecated sole
inputsConfig.record → inputwhenwebQueryTemplatesuse flat{{input.<field>}}tokens.
What does NOT belong in inputsConfig:
skillKey— top-level node field.taskConfiguration.narrix— NARRIX engine config (datasetId,questionId,layer, etc.). These are routing/filter fields consumed by the NARRIX pre-processor, not by the template.taskConfiguration.aiTaskProfile.webScoping— opt-in web context. Graph-engine forwards web intent on the outboundnarrixpayload; actual web fetch and skip rules run inside@exellix/ai-tasks. See .docs/exellix-graph-engine-format.md — Web scoping behavior.- Template variables — use
node.variables+ pathstaskVariables.*, ormodel.variables+jobVariables.*(see Variables (two buckets)). Usenode.taskVariablefor prompts, notinputs.
taskConfiguration.narrix.questionId(e.g."q1","q6") is a NARRIX internal routing key that tells the pre-processor which framework question slot this node fills. It has nothing to do withinputs.question(the human-readable question text). Do not confuse them.
First-class: question and questionId. For NARRIX nodes you can set inputs.question, or taskConfiguration.narrix.questionId, or both. Once NARRIX provides a question ↔ questionId resolver, the missing one can be filled automatically. See .docs/question-questionId-first-class-experience.md.
Skill Key Resolution
Canonical graph models must specify node.skillKey for remote task nodes. Legacy aliases node.data.skillKey and node.metadata.skillKey are rejected by canonical validation. Optional fallback to tasks/${node.id} exists only when allowFallbackToNodeId: true is explicitly enabled (default: disabled).
If skillKey is missing, exellix-graph-engine throws NODE_SKILLKEY_MISSING error before execution begins.
Configuration
skillKeyResolution: {
allowFallbackToNodeId?: boolean; // default: false (strict mode)
fallbackPrefix?: string; // default: "tasks/"
aliases?: Record<string, string>; // optional explicit remaps
}Variables (two buckets — job + task)
Template variables are not merged into one bag. Graph-engine mirrors two scopes on executionMemory:
| Scope | Runtime inputs | Execution mirror | Memory paths |
|-------|----------------|------------------|--------------|
| Job / graph (whole run) | model.variables, runtime.variables, runtime.jobVariables, legacy job.jobVariables | execution.jobVariables | jobVariables.* (legacy alias variables.*) |
| Task / node (current node) | node.variables, runtime.taskVariables | execution.taskVariables | taskVariables.* |
Outbound runTask sends variables (ai-tasks field name) = job bucket only; node scope stays on executionMemory.taskVariables. Requires graph-engine ≥ 5.13 and @exellix/ai-tasks ≥ 7.6.2 (passthrough, no flattening).
Important: graph-engine does not define skill templates; it forwards buckets as-is. See
formats-documentations/graph-runtime-object-format.md.
Upstream template rendering (ai-gateway / athenix-parser)
The stack that eventually renders skill prompts pulls in @athenices/ai-gateway (transitively via @exellix/ai-tasks / @exellix/ai-skills), which depends on @athenices/athenix-parser v4+. That parser implements the v4 template protocol: required {{path}} values throw TemplateResolutionError when missing; optional tokens use {{path |}} or {{path | fallback text}}; subPathSearch is opt-in for alternate root lookup. See the parser’s README under node_modules/@athenices/ai-gateway/node_modules/@athenices/athenix-parser/ when debugging template failures.
Web research in prompts: when this repo runs local question-driven web scope in executeNode, it still injects a flat webContextMarkdown string into jobContext so templates can use {{#if webContextMarkdown}} with a bounded markdown block. That remains the default pattern here even though deep execution.* paths are now supported upstream if you pass full context and configure the gateway/parser accordingly.
Web scoping now runs inside @exellix/ai-tasks, not as a local graph-engine web-scoper phase. Author web intent under taskConfiguration.aiTaskProfile.webScoping; downstream ai-tasks controls source snippets, markdown, skip rules, and any enrichment from execution.input.
Memory Wiring
Memory is passed through consistently:
jobMemory = runtime.jobMemory; model knowledge is added only to the outboundRunTaskRequest.jobMemory.knowledgecopy.taskMemory = runtime.taskMemory; node task knowledge is added only to that node's outboundRunTaskRequest.taskMemory.knowledgecopy.
Optional per-node memory overrides:
node.memory?.taskMemorynode.memory?.jobMemory(rare, but allowed)node.jobContextMapping(see below)
Input Grouping (inputs)
Nodes can group their dynamic inputs under an inputs object for better structure and to avoid conflicts with system fields.
{
"id": "node-1",
"type": "task",
"skillKey": "my-skill",
"inputs": {
"question": "What is the capital of France?",
"detailed": true
}
}Job Context Mapping (jobContextMapping)
jobContextMapping allows a node to pull specific data from jobMemory into its local execution context (jobContext).
"jobContextMapping": {
"map": {
"customer": "jobMemory.profiles.current",
"history": "jobMemory.activityLog"
}
}The runtime will resolve these paths from jobMemory and provide a jobContext object to the underlying task.
Path expressions in map support:
- Dot paths:
jobMemory.profiles.current,executionMemory.graphOutputs.vulnInstances.records - Array wildcard
[*]: e.g.executionMemory.graphOutputs.vulnInstances.records[*]— selects all array elements - Object values
{*}: e.g.recordsById{*}— selects all values of an object (deterministic key order)
Sources can be jobMemory.* or executionMemory.* (current execution state from previous nodes).
Execution Object & Trace
Execution Object
The execution object is a shared state container that persists across all nodes in a graph execution. It stores:
- Output mappings: Data written by nodes via
outputMapping xynthesizedmemory:execution.xynthesized.job(run-wide) andexecution.xynthesized.taskByNode[nodeId](per node). OutboundrunTask.xynthesizedexposes the current node’s slice;xynthesizedPatchon responses is merged after successful MAIN calls.- Execution trace: Per-node execution metadata (see below)
- Custom state: Any application-specific data
The execution object is passed to each node as executionMemory in the task request and updated through outputMapping configurations. Edge predicates may also read input.* and xynthesized.* roots via the same evaluation context (see src/runtime/predicates.ts).
Execution Trace (FR-6)
Each graph run records a structured trace per node in the execution object:
- Location:
execution._trace.nodes[nodeId] - Shape:
{ startedAt, endedAt, skillKey, ok, durationMs, activityId?, summary?, error? }
Trace fields:
startedAt: Unix timestamp (ms) when node execution startedendedAt: Unix timestamp (ms) when node execution endedskillKey: The resolved skill key for this nodeok:trueif successful,falseif faileddurationMs: Execution duration in millisecondsactivityId: Optional activity/task identifier from ai-taskssummary: Optional metadata from task response (on success)error: Error details withmessage, optionalcode, and optionalstack(on failure)
Trace is written by the runtime for every node (both executeGraph and createExellixGraphRuntime flows), regardless of success or failure.
Execution Object in Events (FR-3)
All node execution events include the execution object merged into jobMemory:
- Node Start Event:
input.jobMemory.executioncontains the current execution state - Node Complete Event:
memoryAfter.jobMemory.executioncontains the updated execution state (after outputMapping) - Node Fail Event:
memoryAfter.jobMemory.executioncontains the execution state at the time of failure
This ensures consistent tracking and allows activity tracking systems to access execution data for monitoring and debugging.
ai-tasks metadata for trace
For rich structured trace, local tasks (ai-tasks) should return metadata in the response so the graph can store it in execution._trace.nodes[nodeId].summary and use it for durationMs / activityId:
response.parsed.meta.durationMs— task duration in milliseconds (otherwise the runtime usesendedAt - startedAt)response.parsed.meta.localTaskIdorresponse.parsed.meta.activityId— optional activity/handler identifier
MAIN runTask traces may also include bounded summaries of smartInput.paths and keys touched by xynthesizedPatch (not full synthesized payloads).
Any other fields in response.parsed.meta are stored in summary.
Contextual Knowledge Scope (scope)
Nodes can define complex filtering rules to scope lists from memory into the node's context. This is useful for building localized knowledge for a task (e.g., "all assets in the same zone as the target").
{
"scope": {
"contextualKnowledge": [
{
"list": "zonesEnriched",
"select": ["zone", "semantics.trust_level"],
"filter": {
"where": [
{ "path": "virtual_router", "eq": "{{asset.virtual_router}}" },
{ "path": "containsTargetIp", "eq": true }
],
"limit": 10
}
}
]
}
}Features:
- Dynamic Filtering: Use placeholders like
{{asset.zone}}to reference variables or memory. - Operators:
eq,eqAnyonly (precompute domain-specific fields in the host when needed). - Selection: Pick specific fields using
selectto minimize context size. - Integration: The result is merged into
jobContextalongsidejobContextMapping.
Backward Planning
When mode === "backward":
goalNodeIdis requiredgoalNodeIdmust exist in the graph
If missing/invalid, exellix-graph-engine throws BACKWARD_GOAL_REQUIRED error before execution begins.
Error Handling
All errors are structured ExellixGraphError instances with:
code:ExellixGraphErrorCode(e.g.,NODE_SKILLKEY_MISSING,TASK_NOT_FOUND)message: Human-readable messagecontext: Additional context (jobId, graphId, nodeId, etc.)
Task Not Found
When ai-tasks.runTask() returns "not found":
- Node is marked as
failedwithreason: "TASK_NOT_FOUND" - Includes
skillKeyand ai-tasks diagnostics payload as-is - Commits failure to graphenix (so history contains it)
- Then:
- If
failFast=true→ abort graph - Else continue if graph allows it
- If
Understanding Task Content
To avoid confusion across the stack:
- Node
skillKeyidentifies the task (ai-tasks layer) - The task will reference or embed a skill (ai-skills layer)
- If content is missing, it's either:
- Missing
node.skillKey→exellix-graph-engineerror (NODE_SKILLKEY_MISSING) - Task missing →
ai-tasksnot found (TASK_NOT_FOUND) - Skill missing inside task →
ai-tasks/ai-skillserror
- Missing
exellix-graph-engine surfaces these errors; it doesn't decide what tasks/skills exist.
Types
import type {
Graph,
GraphModelObject,
GraphAiModelConfig,
GraphModelAliasConfig,
GraphNode,
TaskNode,
TaskNodeRuntimeObject,
TaskNodeTaskConfiguration,
Job,
ExecuteGraphInput,
GraphExecutionRequest,
GraphRuntimeObject,
ExecuteGraphResult,
ExellixGraphRunTaskRequest,
ExellixGraphRunTaskResponse,
BuildAiTasksRunTaskRequestArgs,
ExecutionStepOption,
SmartInputConfig,
ExecutionStrategyInvocation,
XynthesizedMemory,
} from '@exellix/graph-engine';RunTask wire: ExellixGraphRunTaskRequest / ExellixGraphRunTaskResponse are aliases of @exellix/ai-tasks RunTaskRequest / RunTaskResponse. SmartInputConfig, ExecutionStrategyInvocation, XynthesizedMemory, and related strategy / xynthesized types are re-exported from @exellix/ai-tasks via src/types/aiTasksDerivedTypes.ts so you can import stable names from @exellix/graph-engine without duplicating NonNullable<RunTaskRequest[…]> aliases.
Graph run persistency contract
Graph authoring documents declare writeback intent under graph.response.persistency. Hosts (@exellix/jobs, @exellix/jobs-api) read that contract before dispatch to resolve Memorix persistency targets and to validate assembled runtime input.
import {
resolveGraphRunContract,
parseGraphRunPersistency,
mergeGraphRunPersistency,
type GraphRunPersistency,
} from '@exellix/graph-engine';
const contract = resolveGraphRunContract(graphId, authoringDocument);
// contract.persistency?.targetEntityName — concrete entity or 'same-as-input'
// contract.persistency?.recordMode — 'always-new' | 'upsert-by-source'
// contract.persistency?.contentType — e.g. 'inferences'Exported helpers: resolveGraphRunContract, loadGraphRunContracts, parseGraphRunPersistency, mergeGraphRunPersistency, applyGraphRunContractOverride, readGraphDocumentVersion. Tests: tests/graph-run-contract.test.ts.
Error Codes
Structured errors are ExellixGraphError with ExellixGraphErrorCode:
import { ExellixGraphErrorCode } from '@exellix/graph-engine';
// Examples: JOB_ID_REQUIRED, NODE_SKILLKEY_MISSING, BACKWARD_GOAL_REQUIRED,
// GRAPH_LOAD_FAILED, GRAPH_EXECUTION_FAILED, NODE_EXECUTION_FAILED,
// TASK_NOT_FOUND, INVALID_GRAPH, INVALID_NODE, NON_CANONICAL_GRAPH_DOCUMENT,
// NON_CANONICAL_TASK_NODEValidation issues for smartInput / inputSynthesis during catalog planning use string codes such as SMART_INPUT_PATHS_INVALID, INPUT_SYNTHESIS_PIPELINE_CONFLICT, INPUT_SYNTHESIS_DESTINATION_INVALID (see src/inspection/validateAiTasksNodeExtensions.ts).
Repository Structure
src/
index.ts
runtime/ExellixGraphRuntime.ts
runtime/buildAiTasksRunTaskRequest.ts
runtime/aiTasksStrategyPhases.ts
runtime/graphRunExecutionSeed.ts
runtime/resolveExecutionPipelineForTaskNode.ts
runtime/localSkills/
runtime/variables.ts
runtime/memory.ts
runtime/events.ts
loaders/FileGraphLoader.ts
types/refs.ts
types/options.ts
types/aiTasksDerivedTypes.ts
types/results.ts
errors/ExellixGraphError.ts
errors/exellixGraphErrorCodes.tsTesting
npm test— Generic SDK checks only: deterministic finalizers, graph inspection, contract inspection (src/tests/run-tests.ts). Does not run bundled graph JSON fromgraphs/.npm run test:graphs— Question-breakdown sample graph (realai-tasks).npm run test:subnet—narrix-subnet-egress-triage.v1with sample data fromgraphs/tests/examples/.- Other graph runners (
dod,run:vuln-group:trace,test:web-scope-e2e, …) live undergraphs/tests/; see graphs/tests/README.md.
Fixtures and DOD outputs for those graphs are under graphs/tests/ (examples/, realdata/, outputs/). The top-level tests/ folder only documents the move (see tests/README.md).
Example graphs (graphs/)
Graph JSON shape: .docs/exellix-graph-engine-format.md.
Bundled graph definitions (JSON DAGs) live in graphs/. graphs/README.md is the bundle catalog (graph IDs, product-specific notes, execution call grid, appendix for bundled v2 graphs, and Platform contract: respected target for integration baseline and feature requests).
To execute a graph, supply a graphLoader that resolves graphId to JSON, build a job with the input shape your graph expects (often execution.input with raw and optional metadata), and call executeGraph. Further integration notes may live under .docs/.
taskConfiguration.aiTasksOutputValidation on task nodes is forwarded as outputValidation on the runTask request (@exellix/ai-tasks 8.x). Root outputConstraints is not part of the closed schema.
Runtime observability (runtimeObjects)
Hosts and playgrounds compose a single runtimeObjects tree instead of querying Mongo or Logxer stores directly:
| Client | Source | Query API |
|--------|--------|-----------|
| activixClient | Your Activix instance via createActivixQueryableClient / createActivixExellixIntegration | getJobActivities({ jobId, graphId?, nodeId?, limit? }) |
| logxerClient | Per-run graph-engine Logxer via ensureGraphEngineLogxerOnRuntimeObjects | getJobLogs({ jobId, graphId?, nodeId?, level?, … }) |
| packagesRuntimeObjects | Optional subtree from @exellix/ai-tasks | Package-owned clients when exported |
Use buildExellixGraphRuntimeObjects({ graphActivixClient, graphLogxerClient, aiTasksRuntimeObjects }) (testkit) and pass the result on runtime.runtimeObjects. setRuntimeObjectsLastJobId is applied automatically during executeGraph.
Integration with Other Packages
See .reports/ directory for request documents:
graphenix-request-node-contract.md- Graph node contractai-tasks-request-not-found-contract.md- Task not found handlingai-tasks-request-variable-channels.md- Variable flow documentationexellix-helpers-requests.md- Helper utilities
License
ISC
