@exellix/ai-tasks
v10.17.1
Published
Task orchestration for the Exellix stack: runTask() with local handlers or LLM-backed execution, task-scoped memory/context enrichment, and executor dispatch via @exellix/ai-skills. ERC-compliant.
Maintainers
Readme
@exellix/ai-tasks
Private Git/npm package for executing tasks using the Exellix execution stack (@exellix/ai-tasks orchestrates @exellix/ai-skills MAIN + @exellix/xynthesis PRE/POST).
Catalox 6 train (in progress):
@exellix/xynthesisin this repo already uses Catalox 6 / Mongo locally (4.9.13 prepared, not published).@exellix/ai-tasksand@exellix/catalox-ai-skillsstill use Firebase until@x12i/[email protected]ships and catalox-ai-skills migrates. Phase 2 then alignsai-skills-manager-uiandai-tasks-playground-ui. Do not publish the stack until then. Seedocumenations/catalox-6-rollout.md.
Breaking — executionStrategies (required): Every runTask request must include executionStrategies: an array of MAIN strategy invocations or [] for plain gateway MAIN. The old executionStrategyKey field is removed. See BREAKING-CHANGES.md and RUNTASK_REQUEST.md. Supported MAIN execution is exactly: direct via executionStrategies: [], planner before MAIN, optimizer after MAIN, or planner + optimizer together. 8.5+: planner and optimizer run via @exellix/xynthesis sidekick actions (execution-plan, execution-evaluate-result) — not direct FuncX run() from ai-tasks. Catalog rows use runtimeKind: "xynthesis-action" and defaultSidekickAction.
Graphenix node execution (10.x — graph-engine / Studio)
runTask() requires a frozen nodePlan (planVersion: graphenix.node-plan/v2.1). Graph-engine compiles authoring JSON once; ai-tasks does not accept taskConfiguration or executionPipeline on the wire.
| Layer | Units | Memory contract |
|-------|--------|-----------------|
| Layer 1 (pre-SKILL) | webScope, narrativePreprocessor, xynthesis pre-actions | Enrich executionMemory (webContext, _narrix, …) |
| Layer 2 (in-SKILL) | synthesis PRE → plan/split → mainSkill → merge/pick-best | Synthesis uses invokeContract.reads + optional unitParams.memoryPaths — not authoring preInputStrategy |
Authoring: aiTaskStrategies.pre: "synthesis" enables the context step only. Paths come from inputsConfig / smartInput.paths at compile time.
Compiled plan (graphenix 2.1.0+): synthesis unit must include non-empty invokeContract.reads and optional unitParams.memoryPaths. unitParams.synthesisInputStrategy is forbidden on the plan wire — validation error LEGACY_SYNTHESIS_INPUT_STRATEGY_ON_PLAN. The Catalox template key comes from supportedCapabilities.synthesisContext on the skill row only (@exellix/ai-skills ^6.11.0).
Dependencies (10.13+): @x12i/graphenix-executable-contracts ^2.15.0, @exellix/web-scope ^1.2.1, @exellix/ai-skills ^7.0.2, @exellix/xynthesis ^4.9.3, @x12i/funcx ^4.9.20, @x12i/ai-gateway ^11.0.0, @x12i/rendrix ^4.3.0, @exellix/catalox-ai-skills ^1.0.2. Requires Graphenix 2.15+ compiled nodePlan for synthesis/webScope IO.
Synthesis memory (10.8+): three template paths — executionMemory.synthesizedContext.contextMarkdown (job context), executionMemory.synthesizedInput.sourceMaterial (task input), executionMemory.synthesizedWebContext.contextMarkdown (web digest). webScope PRE runs forced synthesis to synthesizedWebContext. See BREAKING-CHANGES.md.
Gateway server tools on MAIN (llmRuntime) — 10.5+
When a mainSkill unit includes Graphenix llmRuntime, ai-tasks forwards serverTools, openrouter, and serverToolInstructionMode to @exellix/ai-skills (never to @x12i/ai-gateway directly). Tool metadata returns on RunTaskResponse.metadata and optional top-level mirrors; optional toolMetadataPatch.execution.aiTooling when llmRuntime.output.toolMetadata is configured.
| Path | Source | Destination |
|------|--------|-------------|
| webScope (PRE) | externalPreUtility / strategyKey: webScope | executionMemory.webContext, executionMemory.synthesizedWebContext (forced synthesis) |
| serverTools (MAIN) | mainSkill.llmRuntime.serverTools | response.metadata.*, optional toolMetadataPatch |
Root RunTaskRequest.serverTools / openrouter are forbidden when nodePlan is set — use per-unit llmRuntime only. See documenations/gateway-server-tools.md.
Downstream handoff: graph-model-change-2026-06-13.md, graphenix-format-alignment-fr.md, graphenix-format-fact-guard-fr.md, ai-engine-runtime-runtask-integration.md. Wire fixture: test/fixtures/run-task/graph-engine-pre-synthesis.json.
Breaking — 8.4+ LLM invoke contract (no legacy): Every runTask requires modelConfig.preActionModel, modelConfig.skillModel, and modelConfig.postActionModel. Do not send xynthesisModel, root modelConfig.model, maxTokens, maxTokensCap, or graph-era tier names (weak / strong). Token budgets are owned by Optimixer in @exellix/ai-skills (MAIN) and @exellix/xynthesis (PRE/POST). Send outputExpectation (xynthesis) and optional reasoningEffort, temperature, topP. See How completion budgets work and Model profile aliases.
Stack alignment (8.4+)
| Package | Role in runTask | Minimum (this repo) |
|---------|-------------------|---------------------|
| @x12i/ai-profiles | Profile/choice → wire model (catalogLane required on resolve) | 3.0.0 |
| @exellix/xynthesis | PRE synthesis, POST audit/polish, scoping, utilities, execution-strategy sidekick actions (executeXynthesisAction via @x12i/funcx) | ≥ 4.9.2 |
| @exellix/fact-guard-* | Optional sub-action atom integrity (PRE/POST only; disabled by default) | ^1.0.0 (monorepo: file:; see ../fact-guard/RELEASE.md) |
| @exellix/ai-skills | MAIN runSkill (Optimixer caps, gateway server-tools pass-through, provider failure taxonomy) | ≥ 7.0.1 |
| @x12i/funcx | PRE/POST sidekick built-ins, orchestration run(), logging (direct in ai-tasks; default invoker in xynthesis) | ≥ 4.9.20 |
| @x12i/ai-gateway | LLM wire for MAIN skill (transitive via ai-skills) | ≥ 11.0.0 |
Gateway 11.x (via @exellix/ai-skills ≥ 7.0.2): The gateway owns billing — read response.metadata.costUsd, costStatus, and token usage only; do not post-price with @x12i/ai-tools. Every invoke requires explicit model and maxTokens (Optimixer supplies maxTokens on MAIN before gateway.invoke()). Removed upstream: default model, flex-md token auto-fill, packaged instruction blocks, and the 500 ms between-call rate limit. On Activix rows, prefer root cost / costUsd and outer.cost over outer.metadata for billing. Failed invokes expose error.metadata (GatewayInvokeRejectionMetadata with providerSubCode, operatorHint, providerAttempts when the fallback chain exhausts — gateway ≥ 10.4.4). Full migration rules: UPGRADING_AI_GATEWAY_10.md in @exellix/ai-skills.
Stored modelConfig slots must use explicit ai-profiles profile/choice keys (cheap/default, pro/default, …). Bare profile names (cheap, pro), profile@choice, registry shortcuts, and legacy tier aliases (weak / strong) are rejected at validation — not expanded at invoke.
Monorepo dev: npm test syncs sibling xynthesis/dist and ai-skills/dist into node_modules before compile (see scripts/ensure-xynthesis-dist.mjs and scripts/ensure-ai-skills-dist.mjs).
Execution model (10.x): Every runTask() requires a frozen nodePlan (graphenix.node-plan/v2.1). See RUNTASK_REQUEST.md for the authoritative wire contract and documenations/node-execution-graphenix.md for runtime behavior.
This package implements the canonical runTask() flow (every request carries executionStrategies — use [] for plain MAIN):
- Validate
nodePlanand reject forbidden legacy wire fields (executionPipeline,taskConfiguration, rootmodelConfig) - if a local task handler is registered for
skillKey→ run it and return - Loop
nodePlan.executionUnitsin order (PRE utilities, MAIN skill, POST utilities) viaexecuteNodeFromPlan - Return
RunTaskResponsewith optionalexecutionTraceevents
Legacy root executionPipeline, executionType, and compileTaskConfigurationOnRunTaskRequest are not supported at runtime in 10.x.
Template core and core-aware synthesis (additive)
runTask supports template-core-aware structured synthesis in PRE synthesized-context:
- Template content declares one or more core directives in Athenix
{{core:...}}format (for example{{core:analysis}}); closed list:question,action,plan,objective,decision,comparison,classification,evaluation,analysis,summary,generation,extraction. RunTaskRequest.taskCoreis removed from structured synthesis semantics; runtime derives cores from template declarations.- Synthesis mode selection is additive:
- preferred:
SynthesisConfig.synthesisMode: "markdown" | "structured" - legacy-compatible:
SynthesisConfig.synthesisOutputFormatstill works.
- preferred:
- Structured mode uses detected
templateCores+ resolved question, then builds clean MAIN context from validated synthesized payload. - Core discovery reads raw templates (instructions/prompt) via
ExellixSkillsClient.resolveRawTemplate(templates loaded from the Cataloxai-skillscatalog in@exellix/ai-skills4.1+) before normal rendering; if no core directives are declared, structured synthesis is rejected as a template-definition error. - Raw/enriched materials (
_narrix, memory bundle,webContext) remain in memory; synthesized output is also stored atexecutionMemory.synthesizedContextfor MAIN traceability unless a PRE step opts out viaSynthesisConfig.xynthesizedOutput.alsoWriteLegacySynthesizedContext: false(see Xynthesized memory and smart input). - Optional
RunTaskRequest.xynthesizedholds job-scoped, task-scoped, and execution-scoped synthesized material for graph execution (distinct from raw memories). OptionalsmartInputmatchesSmartInputConfigfrom@exellix/ai-skills/ Rendrix (pathsas{ title, path, required? }[]). Callers may still send legacypaths: string[];runTaskvalidates and normalizes each string to{ title: path, path }beforerunSkill. Paths underxynthesized.*must use scopejob,task, orexecution; full path resolution for{{smartInput}}happens in the gateway/Rendrix stack. OptionalsmartInputRenderOptionsis passed through like otherRunSkillRequestfields. - Backward compatibility is preserved by default: existing flows continue unchanged unless structured mode is explicitly selected.
Task responses may optionally include intermediateSteps when a task (or skill) runs multiple logical steps in one call (e.g. to-cni + enrich + triage); see Intermediate steps (multi-step tasks).
@exellix/ai-tasks depends on @exellix/ai-skills (MAIN) and @exellix/xynthesis (PRE/POST) and re-exports their catalog, invoke, and observability surfaces for single-package consumers.
Gateway template rendering (v4)
Task execution goes through ExellixSkillsClient.runSkill (or the skills client’s executor), which uses gateway.invoke() — not invokeChat() — so instruction, prompt, and context templates are built via buildMessages, nx-content resolution, and @x12i/rendrix render. The gateway rejects a top-level input field on invoke requests; runSkill / runAudit map caller input (and related fields) into workingMemory.input (merged with variables, memories, object context, knowledge) so .prompt templates can use {{input}}.
On the runTask MAIN path, variables is the job/graph bucket forwarded as-is (align with executionMemory.jobVariables). executionMemory.taskVariables holds node scope. input, xynthesized, and smartInput are separate top-level fields — ai-tasks does not fold them into variables. Templates and Rendrix resolve jobVariables.*, taskVariables.*, xynthesized.*, and smartInput against the forwarded payload. Optional host helper mergeSkillTemplateVariables merges maps outside default MAIN.
In this package:
runTask({ ... })accepts the same optional fields asRunSkillRequest:templateRenderOptions,templateTokens,smartInputRenderOptions(withsmartInput). They are passed through unchanged torunSkillon the DIRECT / pipeline MAIN path.- Default parser options for all skill runs: set
templateRenderingonExellixSkillsClientOptionswhen constructingExellixSkillsClient(or configuretemplateRenderingon a gateway instance you pass asoptions.gateway). Per-call overrides usetemplateRenderOptionson the request. - Synthesis, audit, polish, and AI scoping are executed via
@exellix/xynthesis(and therefore inherit xynthesis behavior for retries/repair and diagnostics when enabled).
| Mechanism | Where | Effect |
|-----------|--------|--------|
| ExellixSkillsClientOptions.templateRendering | Client constructor | Sets gateway defaults when this package (or your app) constructs AIGateway. If you inject options.gateway, set templateRendering on that instance. |
| RunTaskRequest.templateRenderOptions | Per runTask | Deep-merged on gateway defaults for that invoke only. |
| RunTaskRequest.templateTokens | Per runTask | Passed through as gateway templateTokens (highest overlay priority during render). |
Types TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions, SmartInputRenderResult, RunTaskSmartInput, and GatewayTemplateTokens are re-exported from @exellix/ai-tasks (skill/gateway/Rendrix definitions). mergeSkillTemplateVariables is exported for callers that reproduce the same merge outside runTask. TaskRequestBuilder supports .withTemplateRenderOptions(...) and .withTemplateTokens(...), plus .withXynthesized, .withSmartInput, .withSmartInputPaths, .withSmartInputRenderOptions(...), .withXynthesizedJob, .withXynthesizedTask, .withXynthesizedExecution (see Xynthesized memory and smart input).
Errors: TemplateResolutionError (or codes like TEMPLATE_RESOLUTION_ERROR / TEMPLATE_VARIABLE_MISSING) means a required template path was undefined after the gateway merged memory. Fix variables / memories / input, use optional fragments ({{path \|}}) in templates, or see the v4 guide for legacy silentMissingMustTokens. Template resolution failures are not treated as missing registry content (they do not go through RegistryManager.diagnose the same way as “content not found”).
Full protocol (MUST vs optional tokens, subPathSearch, errors): GATEWAY_TEMPLATE_PROTOCOL_V4.md in @exellix/ai-skills. Nx-content layout: @x12i/ai-gateway Content Resolver — Upstream Guide.
Install
Packaged skills (recommended): depend only on @exellix/ai-tasks; it bundles the execution stack and ships .metadata templates.
npm install @exellix/ai-tasksCustom gateway / advanced injection: add @exellix/ai-skills and construct WorexClientTasks with your own client + executor.
npm install @exellix/ai-tasks @exellix/ai-skillsBundled x12i stack (direct dependencies of this package): @x12i/catalox (catalog reads / publish script), @x12i/env (env conventions aligned with the Exellix stack), and @x12i/logxer 4.6.0+ — createLogxer, warnCode/errorCode with shipped diagnostic catalog (.metadata/log-diagnostics.json), DebugLogAbstract, fieldEvidence, runWithLogContext (wired in runTask() via runAiTasksWithLogContext), and in-process getAiTasksJobLogs for debug tooling. Per-package thresholds: AI_TASKS_LOGS_LEVEL, ACTIVIX_LOGS_LEVEL. Older names logs-gateway and nx-config2 are not direct dependencies here.
Narrix (v5+)
From v5.0.0, the local skills/skill.local:narrixRun path and narrixInput for narrix-then-direct require a unified shape: always set medium (record | text | docs | chat), datasetId, and the payload fields for that medium. Routing uses @exellix/narrix-runner v2 (runByQuestion / runByNarrative) and @exellix/narrix-catalox for catalog hints—there is no narrix-packs-library or legacy “record job without medium” compatibility.
Migration: see BREAKING-CHANGES-NARRIX-V5.md. Live Narrix tests: npm run test:narrix:live (requires .archive/packages/… seed; see that doc).
Catalox catalogs (upstream from this package)
@exellix/ai-tasks publishes task-strategy metadata to Catalox under appId: ai-tasks, and re-exports read helpers so apps can list catalogs, descriptors, and items without adding @x12i/catalox as a direct dependency (types such as Catalox, CataloxContext, AppCatalogBootstrap are re-exported from the task-strategies surface).
AI Tasks catalogs
| Catalog ID | Role |
|-------------|------|
| ai-task-synthesis-input-strategies (alias ai-task-strategies-pre) | Synthesis template keys for supportTemplates.synthesisContext on the PRE step (memory scope is invokeContract.reads on nodePlan, not per-node authoring enums). |
| ai-task-pre-orchestration-strategies | PRE fan-out: plan, split. |
| ai-task-post-orchestration-strategies | POST fan-in: merge, pick-best, audit-loop. |
| ai-task-strategies-post | Post–core audit selectionStrategy (best / synthesis). |
| ai-task-input-strategies | Optional authoring hints: inputStrategyKey on RunTaskRequest (does not drive Narrix invocation). |
| execution-strategy | Preferred catalog for supported MAIN execution metadata: direct, planner, optimizer. |
| ai-task-execution-strategies | Compatibility MAIN wrappers metadata catalog: same seeded rows as execution-strategy. |
| ai-task-main-execution-wrappers | Compatibility MAIN execution-strategy catalog: same seeded rows as execution-strategy. |
| ai-task-narrix-modes | Narrix invocation: narrixMode (off | preprocessor | handler). |
Runtime does not require Catalox to be configured for these keys. Catalogs seed console metadata, and execution-strategy rows may be passed to runTask as executionStrategyCatalogItems for guarded metadata consumption. The runtime still code-validates supported strategy keys, phases, and retry rules; malformed or conflicting catalog rows are ignored. Consumption order: optional Narrix → optional web (preprocessor path) → optional Xynthesis PRE steps → MAIN.
Supported MAIN Execution Strategies
The runtime supports three MAIN execution strategy records:
| Strategy | Request shape | Runtime behavior |
|----------|---------------|------------------|
| direct | executionStrategies: [] | Plain MAIN. Calls runSkill once. direct is catalog metadata only and is not valid as a row inside executionStrategies[]. |
| planner | { strategyKey: "planner", phase: "before", priority } | Runs xynthesis execution-plan before MAIN and can merge instructions, prompt, variables, and template tokens into the request. |
| optimizer | { strategyKey: "optimizer", phase: "after", priority, maxIterations? } | Runs xynthesis execution-evaluate-result after MAIN. If not satisfied, retries MAIN with feedback until satisfied or maxIterations is reached. |
planner must use phase: "before" and optimizer must use phase: "after". optimizer.maxIterations controls MAIN attempts; when omitted it falls back to AI_TASKS_OPTIMIZER_MAX_ITERATIONS or the package default. Per-invocation args.functionId always wins over catalog metadata and code defaults.
execution-strategy catalog items are structured metadata, not an open behavior plug-in. Safe runtime consumption is currently limited to fields explicitly marked in safeRuntimeFields, such as defaultSidekickAction / runtimeKind: "xynthesis-action", and only when the catalog row matches the already-validated strategy key, phase, and request shape.
Legacy monolith id ai-task-strategies is no longer written by this repo; use the catalogs above.
Publishing Firestore metadata
Note: Task-strategy catalog publishing still uses Firebase / Firestore (
@x12i/catalox/firebase) until the Catalox 6 rollout completes for@exellix/catalox-ai-skills.
From a clone of this repo, with Firebase env in .env as required by createCataloxFromEnv() (see @exellix/ai-skills): FIREBASE_PROJECT_ID (required), GOOGLE_SERVICE_ACCOUNT_BASE64 (required: service account JSON, base64-encoded), and optional FIRESTORE_DATABASE_ID:
npm run publish:task-strategiesThat provisions apps/ai-tasks, all six native catalogs, bindings, descriptors, and canonical items.
Reading catalogs and items (from @exellix/ai-tasks)
Construct a Catalox instance (for example createCataloxFromEnv() from @exellix/ai-tasks, which re-exports it from @exellix/ai-skills), then use any of:
| Export | Purpose |
|--------|---------|
| defaultAiTasksCataloxContext() | Build CataloxContext with appId: "ai-tasks". |
| listAiTasksAppCatalogs(catalox, context?) | Discovery list: catalog id, label, access, source mode. |
| getAiTasksAppCatalogBootstrap(catalox, context?) | All descriptors visible to the app (identity, query fields, capabilities). |
| getAiTasksTaskStrategiesCatalogDescriptor(catalox, catalogId, context?) | One catalog’s descriptor (any of the six catalog ids above). |
| listPreCoreTaskStrategies / listPostCoreTaskStrategies | Normalized rows for pre / post synthesis–audit catalogs. |
| listInputStrategies / listExecutionStrategyCatalogItems / listExecutionStrategies / listMainExecutionWrappers / listNarrixModes | Normalized rows for input, preferred execution metadata, compatibility execution catalogs, MAIN wrappers, and Narrix mode catalogs. |
| getAiTasksTaskStrategiesCatalogSnapshot(catalox, context?, query?) | All seven task-strategy catalogs in one round-trip (pre, post, input, executionStrategy, execution, mainExecutionWrappers, narrixModes). |
Raw rows (full UnifiedCatalogItem) if you need them:
import {
createCataloxFromEnv,
defaultAiTasksCataloxContext,
AI_TASK_PRE_STRATEGIES_CATALOG_ID,
} from "@exellix/ai-tasks";
const catalox = createCataloxFromEnv();
const ctx = defaultAiTasksCataloxContext();
const { listOutcome, items } = await catalox.listCatalogItems(
ctx,
AI_TASK_PRE_STRATEGIES_CATALOG_ID,
{ limit: 100 }
);Skill templates (ai-skills app)
Skill definitions and template bodies for execution live under Catalox appId: ai-skills (owned by @exellix/ai-skills). This package re-exports the Catalox skill surface from @exellix/ai-skills via the same entrypoint (e.g. listAiSkillsCatalogItems, fetchSkillTemplatesFromCatalox, createCataloxFromEnv, initFirebaseAdminFromEnv, …) so you can view and edit skill catalog items while depending only on @exellix/ai-tasks.
Usage
Packaged skills (default client)
import {
createDefaultWorexClientTasks,
ExecutionType,
} from "@exellix/ai-tasks";
const client = createDefaultWorexClientTasks();
// Optional before first runTask if nx-content needs time to connect:
await client.whenReady({ timeoutMs: 60_000 });
const result = await client.runTask({
skillKey: "tasks/your-skill",
agentId: "my-agent",
jobTypeId: "my-job-type",
taskTypeId: "my-task-type",
executionStrategies: [],
executionType: ExecutionType.DIRECT,
input: { question: "..." },
jobContext: { graphRunId: "..." },
});
await client.dispose();Utility tasks (non-question tasks)
ai-tasks supports utility tasks: tasks that run from structured inputs and memory/context to produce structured, machine-consumable output, without requiring a user-facing question.
- Contract:
RunTaskRequest.inputcan be any JSON-compatible object (or string).input.questionis optional. - Correlation (required): every
runTaskrequest must include non-emptyagentId,jobTypeId, andtaskTypeId(same as@exellix/ai-skills). SeeRUNTASK_REQUEST.md(including Graph task node → RunTaskRequest for authoring vs invoke). - When question matters: Only skills/templates that explicitly reference
{{question}}(or rely on question-driven synthesis) need a question. Utility tasks should not. - Output: Utility tasks should typically return a structured
parsedpayload (for downstream graph nodes), not only prose inrawText.
Example (utility task with no question):
import { runTask } from "@exellix/ai-tasks";
const res = await runTask({
agentId: "my-agent",
jobTypeId: "prepare-context-job",
taskTypeId: "prepare-context-task",
skillKey: "skills/graph.prepareContext",
input: {
record: { /* ... */ },
policy: { /* ... */ },
},
executionMemory: {},
jobMemory: {},
graphId: "graph-123",
nodeId: "prepare-context",
});
console.log(res.parsed);Example (utility task as a local deterministic handler):
import { runTask } from "@exellix/ai-tasks";
const res = await runTask({
skillKey: "skills/skill.local:validateInput",
input: {
recordsPath: { $path: "executionMemory.inputs.records" },
rules: { requirePaths: ["id"] },
},
executionMemory: { inputs: { records: [{ id: "a" }] } },
});
console.log(res.parsed); // { ok: true, meta: ... } (example shape)Structured decision output (response.parsed) + optional schema enforcement
For decision/utility nodes, prefer returning a stable object payload in response.parsed so graph outputMapping can reference fields deterministically (no markdown parsing).
Example decision task (built-in local handler):
import { runTask } from "@exellix/ai-tasks";
const res = await runTask({
skillKey: "skills/graph.decide-web-scope",
input: { localEvidenceSufficient: true },
taskKind: "decision",
outputValidation: {
schema: {
type: "object",
required: ["contractVersion", "shouldUseWeb", "reasonCodes", "missingSignals"],
properties: {
contractVersion: { type: "string" },
shouldUseWeb: { type: "boolean" },
reasonCodes: { type: "array", items: { type: "string" } },
missingSignals: { type: "array", items: { type: "string" } },
},
additionalProperties: false,
},
mode: "fail",
},
});
// Stable structured artifact for deterministic graph mapping:
// - res.parsed.shouldUseWeb
// - res.parsed.reasonCodes
// - res.parsed.missingSignalsgetPackagedAiTasksMetadataDir() returns the absolute path to the shipped .metadata folder (override with contentRegistryLocalPath in client options). forPackagedSkills is an alias of createDefaultWorexClientTasks.
Class-Based API (custom ai-skills client)
import { ExellixSkillsClient, WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
// `WorexClientSkills` is a type alias for `ExellixSkillsClient`
// skills client is the shared execution + helper layer
const skills = new ExellixSkillsClient({
// ... your existing ai-skills config (gateway/provider/router)
});
// Get executor from skills client (no-enrichment execute primitive)
const executor = skills.getExecutor();
// tasks client orchestrates task-level execution
const tasks = new WorexClientTasks(skills, executor);
const res = await tasks.runTask({
skillKey: "tasks/security-risk-summary",
// executionType is optional, defaults to ExecutionType.DIRECT
input: { assetId: "a-123", windowDays: 30 },
// optional
variables: { orgName: "Acme" },
modelConfig: {
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "openai/gpt-5",
temperature: 0.7,
topP: 0.9,
},
jobId: "job-1",
agentId: "agent-1",
});
console.log(res.rawContent);
console.log(res.parsed); // if your pipeline supports parsed outputFunction-Based API (Convenience)
import { runTask, ExecutionType } from "@exellix/ai-tasks";
// Automatically initializes SDK (ERC mode)
// executionType is optional, defaults to ExecutionType.DIRECT
const res = await runTask({
skillKey: "tasks/security-risk-summary",
agentId: "agent-1",
jobTypeId: "security-job-type",
taskTypeId: "security-task-type",
executionStrategies: [],
input: { assetId: "a-123" },
jobId: "job-1",
});RunTask Algorithm (Canonical)
Given a request, runTask() performs:
- Validate
nodePlan(graphenix.node-plan/v2.1) and reject forbidden wire fields. See RUNTASK_REQUEST.md. - Local task dispatch: If
getLocalTask(skillKey)returns a handler, run it and return (no LLM MAIN). - Node execution:
executeNodeFromPlanloops orderedexecutionUnits(PRE utilities, MAIN skill, POST utilities), appendingexecutionTraceevents. - Finalize response: Attach observability, xynthesized patches, and trace metadata as configured.
Legacy steps involving root executionPipeline, executionType, or runtime taskConfiguration compile are not executed in 10.x.
Trace mode (authoritative ordered debug trace)
Set executionMode: "trace" to opt in to an authoritative, ordered per-step execution trace. When enabled, the response includes:
debugTrace.tasks: DebugTraceTask[]
Each entry represents a real unit of work that executed (pre steps, main skills/gateway call, post steps) and includes:
taskType:"pre-execution" | "ai-task" | "post-execution"details: short human stringmodelUsed: string for LLM-backed steps;nullfor deterministic stepsmetadata.timing:{ startedAt, endedAt, durationMs }metadata.step:{ phase, type, stepId }(when applicable)
When upstream providers expose them (via @exellix/xynthesis / @exellix/ai-skills trace surfaces), trace tasks may also include usage/routing/cost under stable metadata keys.
Graph / smart-input hints (ai-tasks–specific): Trace entries for MAIN direct execution include metadata.smartInput: { paths } (echo of RunTaskRequest.smartInput.paths after normalization—{ title, path }[], or empty array). PRE synthesized-context entries may include metadata.xynthesized (inputPathsUsed, outputDestination, outputKey, patchKeys) when SynthesisConfig.xynthesizedOutput is set—keys only, not full synthesized payloads.
Xynthesized memory and smart input
Graph runtimes need structured execution context that is not the same as raw jobMemory / taskMemory / executionMemory: material synthesized once at job scope, scoped to a single node, or held in a run-wide execution bucket. RunTaskRequest supports:
| Field | Purpose |
|-------|---------|
| xynthesized?: { job?, task?, execution? } | job — synthesized context reusable across nodes in the same graph run (graph-engine merges patches from each runTask). task — scoped to this task/node invocation only. execution — run-wide execution bucket (distinct from job/task). Values are Record<string, unknown> buckets keyed by your outputKey (and any caller-supplied keys). |
| smartInput?: RunTaskSmartInput | Declares smart-input paths for Rendrix {{smartInput}} (via @exellix/ai-skills / gateway). Use Rendrix-native paths: { title, path, required? }[], or legacy paths: string[] (normalized before runSkill). Paths under xynthesized.* must use job, task, or execution (e.g. xynthesized.execution.priorAnalysis). The smartInput object allows only the paths property at the root. Optional smartInputRenderOptions on RunTaskRequest controls markdown folding for the macro (same merge rules as RunSkillRequest). Full dot-path resolution against live memory happens downstream (Rendrix / gateway). |
Validation (smartInput)
If smartInput is present, it must be a plain object with paths (array of non-empty strings or { title, path, required? }) and optional strict (boolean). Invalid: smartInput: {}, bad paths, extra root keys, or xynthesized.<scope>.* with <scope> not job, task, or execution. Prefer path prefixes jobVariables.* and taskVariables.* (legacy variables.* ≡ job scope when the engine resolves paths). { paths: [] } is valid.
At runTask() entry, invalid shape throws SmartInputValidationError (error.code, error.details, error.skillKey) — the task does not run (no NARRIX, no PRE xynthesis, no MAIN). Omitted smartInput is allowed.
Pre-flight validation (no LLM invoke)
Use these helpers before runTask() to inspect config and payload without calling xynthesis or the gateway:
| Function | Purpose |
|----------|---------|
| validateRunTaskConfig(request) | Static checks: agentId / jobTypeId / taskTypeId, executionStrategies, smartInput shape, modelConfig / llmCall numeric fields, executionPipeline (one MAIN, synthesis PRE context flag). Returns { ok, issues, errors, warnings }. |
| validateRunTaskInvoke({ request, templateResolver?, ... }) | Config checks plus whether expected paths resolve on the request (input, memories, xynthesized). Uses Rendrix renderSmartInput for required smart-input paths and optional analyzeTemplateResolution on raw templates. |
| analyzeExpectedRunTaskInput({ skillKey, smartInput?, instructions?, prompt?, templateResolver? }) | Returns expected dot-paths from smartInput.paths and path tokens in skill templates (Rendrix listTokens). |
import {
validateRunTaskConfig,
validateRunTaskInvoke,
analyzeExpectedRunTaskInput,
SmartInputValidationError,
isSmartInputValidationError,
} from "@exellix/ai-tasks";
// Config only (fast)
const configCheck = validateRunTaskConfig(runTaskRequest);
if (!configCheck.ok) {
for (const issue of configCheck.errors) {
console.error(issue.code, issue.path, issue.message);
}
}
// Config + payload + templates
const invokeCheck = await validateRunTaskInvoke({
request: runTaskRequest,
templateResolver: {
async resolveRawTemplate(skillKey, section) {
const r = await skillsClient.resolveRawTemplate(skillKey, section);
return r?.found ? r.content : undefined;
},
},
checkTemplateResolution: true,
});
if (!invokeCheck.ok) {
console.error(invokeCheck.errors);
}
// What paths does this node need?
const expected = await analyzeExpectedRunTaskInput({
skillKey: runTaskRequest.skillKey,
smartInput: runTaskRequest.smartInput,
prompt: "...",
});Each issue has code, severity (error | warning), message, and optional path (e.g. smartInput.paths[0].path, modelConfig.preActionModel). See .docs/flow-io/ for flow-level wiring.
Skill catalog helpers (runtime re-exports)
@exellix/ai-tasks re-exports Catalox-backed catalog and execution helpers from @exellix/ai-skills so orchestrators can depend on one package for runTask plus template maintenance:
| Export | Purpose |
|--------|---------|
| extractTokenNamesFromStrings, getSkillTokens, getSkillContent, modifySkillContent, createSubSkill, deleteSubSkill, SKILL_TEMPLATE_ROLES_ORDER | Template catalog introspection and edits (Catalox). |
| runSkillRequestFromFlat, resolveAiEngineIdForRunSkill, SkillExecutionTraceError, trace helpers | Flat skill execution and trace utilities. |
Re-consider direct functions (not runTask)
Revision skills are direct skill wrappers from @exellix/ai-skills, re-exported on @exellix/ai-tasks for orchestrators that already depend on the task SDK. Do not invoke these via runTask — call them on ExellixSkillsClient or bind the standalone functions with a runSkill implementation.
| Export | Skill key | Result |
|--------|-----------|--------|
| fillReConsiderationGap | RECONSIDER_FILL_GAP_SKILL_KEY (re-consider.fill-gap) | { gapOutput } |
| reviewReConsiderationResponse | RECONSIDER_REVIEW_SKILL_KEY | { reviewStatus, reviewOutput } |
| rewriteReConsiderationScope | RECONSIDER_REWRITE_SCOPE_SKILL_KEY | { correctedScope } |
| rewriteReConsiderationFull | RECONSIDER_REWRITE_FULL_SKILL_KEY | { correctedAnswer } |
| calibrateReConsiderationDecision | RECONSIDER_CALIBRATE_DECISION_SKILL_KEY | { calibratedOutput } |
Also exported: stringifyForPrompt, the five build*ReConsideration*RunSkillRequest builders, and types such as ReConsiderationRuntime, ReviewStatus, and ReConsiderationRunSkill. Shared runtime fields: jobId, taskId, agentId, jobTypeId, taskTypeId, modelConfig. Callers own merge/apply of returned fragments. See @exellix/ai-skills README for token roots and examples.
import {
ExellixSkillsClient,
fillReConsiderationGap,
RECONSIDER_FILL_GAP_SKILL_KEY,
} from "@exellix/ai-tasks";
const client = new ExellixSkillsClient(/* … */);
const result = await client.fillReConsiderationGap(
{
input: "What are the risks?",
originalInstructions: "Answer with evidence.",
originalResponse: priorAnswer,
gapExplained: "Missing evidence for Section B.",
},
{
jobId: "job-1",
taskId: "task-1",
agentId: "agent-1",
jobTypeId: "job-type",
taskTypeId: "task-type",
modelConfig: { model: "gpt-4o-mini" },
}
);
// Or: await fillReConsiderationGap.call({ runSkill: client.runSkill.bind(client) }, input, runtime);Studio preflight (invoke packet review, markdown analysis reports, FuncX ai-skills/analyze-gateway-invoke-request) is not part of the runtime stack — use your studio / skills-manager layer. See documenations/studio-skill-invoke-preflight.md for a suggested design (requirements, former behavior, finding catalog, optional FuncX step). Also see @exellix/ai-skills SKILLS_MANAGER_CLIENT_GUIDE.md.
Rendrix helpers used by validation are also re-exported: listTokens, analyzeTemplateResolution, renderSmartInput, SmartInputInvalidError, TemplateResolutionError, and related types.
Runtime surfaces
- Narrix preprocessor / Narrix handler
ctx: Samexynthesized/smartInputas on therunTaskrequest after normalization (pipeline PRE runs later and can mutatexynthesizedbefore MAIN). - PRE
synthesized-context: The bundle passed into template/rendering includesxynthesizedandsmartInputso synthesis prompts can reference them consistently with MAIN. - MAIN executor payload: Top-level
xynthesized,smartInput(normalized), andsmartInputRenderOptions(when set) are forwarded on the object passed torunSkill(alongside memories andcontext). - Template variables:
variables(job bucket) andexecutionMemory.jobVariables/executionMemory.taskVariablesare forwarded separately; use{{xynthesized…}},{{smartInput}},jobVariables.*, andtaskVariables.*in templates (not a single flattened bag). - Response: When downstream returns a
SmartInputRenderResult,runTaskfinalizeexposesresponse.smartInputRenderResultand mirrorsmetadata.smartInputRenderResult(also readsmetadata.smartInputwhen it matches that shape).
PRE synthesis destination (SynthesisConfig.xynthesizedOutput)
Optional on the synthesized-context step config:
destination:"job"|"task"|"execution"— writes underrequest.xynthesized.job[outputKey],.task[outputKey], or.execution[outputKey](no redirect to other scopes).outputKey: string key for the synthesized value.mode:"replace"(default) or"merge"— for merge, if the existing value atoutputKeyand the new value are both plain objects, ai-tasks deep-merges them; otherwise the new value replaces.alsoWriteLegacySynthesizedContext: Whentrueor omitted, keep today’s behavior: persist structured/markdown artifacts onexecutionMemory.synthesizedContext(andsynthesizedInputwhen applicable) for MAIN. Whenfalse, skip that legacy persistence for this PRE step; MAIN still receives synthesized context markdown from the PRE step when produced.
Stored value: Prefer the full synthesis artifact when present (structured / question-driven / markdown artifact shape). If there is no artifact, ai-tasks stores { contextMarkdown } using the PRE step’s markdown string.
Response: RunTaskResponse.xynthesizedPatch has the same { job?, task?, execution? } shape so the graph-engine can merge into durable graph memory. Omitted when nothing was written.
Execution-scope example (PRE + smart-input):
executionPipeline: [
{
phase: "pre",
type: "synthesized-context",
config: {
xynthesizedOutput: { destination: "execution", outputKey: "priorAnalysis" },
},
},
{ phase: "main", type: "direct" },
],
smartInput: { paths: ["xynthesized.execution.priorAnalysis"] },
// response.xynthesizedPatch.execution.priorAnalysis → graph-engine merges into executionMemory.xynthesized.executionBuilder
TaskRequestBuilder: withXynthesized, withSmartInput, withSmartInputPaths (builds native { title, path } entries), withSmartInputRenderOptions, withXynthesizedJob, withXynthesizedTask, withXynthesizedExecution.
What ai-tasks does not do
Does not choose graph mappings, apply outputMapping, run the full graph, or own long-lived graph state—that belongs to the graph runtime. It allowlists xynthesized.* smart-input scopes (job, task, execution) but does not render {{smartInput}} or resolve paths against live memory (Rendrix / gateway).
Local Tasks
Local tasks are deterministic handlers (no LLM) that run inside runTask() before any memory enrichment or executor call. They are identified by skillKey. If a handler is registered for that skillKey, it runs with the task input and context; the return value is wrapped into a standard RunTaskResponse (e.g. parsed, metadata.durationMs, metadata.localSkillKey).
Built-in local tasks
| Skill key | Purpose |
|--------|--------|
| skills/node.callExport | Dynamically import a module, resolve an export (default or named), and call it. Supports $path-based argument resolution (e.g. input.payload, jobMemory.foo), optional job/global caching for instances. |
| skills/node.callExportBatch | Same module/export/cache semantics; creates or reuses a cached instance, then calls a batch method (e.g. enrichJson) with payload/options. Returns { ok, value, meta: { processed, errors, durationMs } }. |
| skills/skill.local:validateInput | Validate records at a $path (e.g. executionMemory.inputs.vulnInstances.records). Rules: requirePaths, graphTypeEquals. No mutation; returns { ok, meta: { total, valid, invalid } } or { ok: false, errors }. |
| skills/skill.local:normalizeNarrixResult | Normalize records to a stable “Narrix attachment” shape: ensure _narrix.meta.entity.entityKey, _narrix.scoping / _narrix.discovery and their arrays, joinCandidates. Returns { ok: true, value: { records } }. |
| skills/skill.local:narrixRun | Run one input through the Narrix pipeline (ingest → runner) for record, text, docs, or chat. Unified input: medium ("record" | "text" | "docs" | "chat") + datasetId + medium-specific payload. Returns { ok: true, entity, signals, stories, passes?, meta } or { ok: false, error, message?, meta? }. Legacy record shape (no medium) still supported. |
| skills/graph.collectEvidence | Opt-in reference local handler: collect evidence from structured requests (queries and/or URLs) and return a reusable EvidenceBundle for downstream graph nodes. No question required. |
Generic evidence collection (skills/graph.collectEvidence)
ai-tasks standardizes a generic, domain-agnostic evidence collection contract for staged graph execution. This is independent of NARRIX question-driven web scoping: callers provide structured requests; the result is a reusable EvidenceBundle suitable for storing in executionMemory and reusing downstream.
- Must-have: a stable input/output contract graphs can depend on cleanly.
- Reference handler: ai-tasks ships an opt-in local task (
skills/graph.collectEvidence) that implements the contract to reduce integration friction. Retrieval is not implicit; graphs opt in by selecting thisskillKey. - Backends: the reference handler may reuse existing internal search/fetch components, but the public contract does not expose NARRIX semantics (
narrix,webContext, web-scope templates, etc.). - Types: see
EvidenceCollectionInput/EvidenceBundleexported from@exellix/ai-tasks(implemented insrc/types/evidence-types.ts). - Current reference implementation notes:
- Supports query-driven discovery and direct URL fetch.
- Enforces policy limits like
maxRequests,maxSourcesPerRequest,maxTotalSources, domain allow/block lists, and snippet size caps. - Returns
value.sources[]+ per-requestvalue.requests[]. Ifextraction.returnFacts/extraction.returnSummaryare set, the reference handler currently returns emptyfacts: []/summary: ""(placeholders for future variants).
Recommended graph pattern:
const evidenceRes = await runTask({
skillKey: "skills/graph.collectEvidence",
input: {
requests: [
{ kind: "vendor_guidance", query: "ExampleProduct security advisory" },
{ kind: "exploitation_status", url: "https://example.com/advisory" },
],
policy: { maxTotalSources: 10, includeSnippets: true },
},
executionMemory,
jobMemory,
graphId,
nodeId,
});
// Store once, reuse later:
executionMemory.evidence = evidenceRes.parsed; // EvidenceBundlenode.callExport (input contract)
module: string (module name or path for dynamicimport())export: string (default"default") — export namecall:{ method: string, args?: any[] }—methodcan be"function"(call export as function),"create"(call and optionally cache), or any other string (call as method on cached/created instance)cache: optional{ scope: "global" | "job", key: string }- Args may use
{ $path: "input.x" }(orjobMemory.*,taskMemory.*,executionMemory.*,variables.*); resolved via dot-path before calling.
node.callExportBatch (input contract)
module,export,cache: same as abovecall.create:{ method, args? }— used to create/cache the instancecall.batch:{ method, args? }— method and args for the batch call (args can use$path)payload,options: typically passed via$pathincall.batch.args(e.g.input.payload,input.options)
skills/skill.local:validateInput (input contract)
recordsPath:{ $path: "..." }— path to records array (e.g.executionMemory.inputs.vulnInstances.records)rules.requirePaths: string[] — dot paths that must exist on each recordrules.graphTypeEquals: optional string — requiregraphized.meta.graphTypeto equal this valuedatasetId: optional (for validation rules)
skills/skill.local:normalizeNarrixResult (input contract)
recordsPath:{ $path: "..." }— path to records arrayattachRoot: string (default"_narrix") — key under which Narrix attachment livesdefaults.scopingPath,defaults.discoveryPath: optional paths under attachRoot (default"scoping","discovery")
skills/skill.local:narrixRun (input contract)
One handler supports four media: record, text, docs, chat. Send a unified input with medium and datasetId, plus the payload for that medium. For full object shapes and use cases, see NARRIX-FOUR-OBJECTS-AND-USE-CASES.md.
Unified input (recommended):
- Record:
{ medium: "record", datasetId: string, record: Record<string, unknown> } - Text:
{ medium: "text", datasetId: string, text: string, meta?: object } - Docs:
{ medium: "docs", datasetId: string, document: { pages: Array<{ text, pageId?, pageNumber?, ... }>, docId?, title? } } - Chat:
{ medium: "chat", datasetId: string, thread: { messages: Array<{ role, text, ... }>, threadId?, title?, participants? } }
Success response: { ok: true, entity, signals, stories, passes?, meta } — meta includes datasetId, packId, entityKind (and run metadata).
Failure response: { ok: false, error: string, message?: string, meta?: object } — e.g. NARRIX_DISABLED, NARRIX_INVALID_INPUT, RUNNER_PACK_NOT_FOUND, or ingest/runner error codes.
Feature flag: Set USE_NARRIX_INGEST=1 to enable the Narrix pipeline (use npm run test:with-narrix-ingest on Windows instead of prefixing the command). When unset, the handler returns { ok: false, error: "NARRIX_DISABLED" } without calling ingest or runner.
Debug logging: Set NARRIX_DEBUG=1 to log datasetId, medium, packId, entityKind, signal codes, and narrativeTypeIds on success (default: quiet).
See NARRIX-STATUS-REPORT.md for implementation details and test layout.
Custom local tasks
You can register your own handlers so they run when skillKey matches the registered key:
import {
registerLocalTask,
getLocalTask,
type LocalTaskContext,
type LocalTaskHandler,
} from "@exellix/ai-tasks";
const myHandler: LocalTaskHandler = async ({ input, ctx }) => {
// ctx: skillKey, jobMemory, taskMemory, executionMemory, variables, jobId, agentId, graphId, nodeId, ...
return { ok: true, value: input.someField };
};
registerLocalTask("skills/my.custom.task", myHandler);
// Later: runTask({ skillKey: "skills/my.custom.task", input: { someField: 1 } }) will run myHandler and skip enrichment/executorregisterLocalTask(skillKey: string, handler: LocalTaskHandler): void— register a handler forskillKeygetLocalTask(skillKey: string): LocalTaskHandler | undefined— get handler (used internally byrunTask)LocalTaskContext—skillKey,jobMemory,taskMemory,executionMemory,variables,xynthesized,smartInput,jobId,taskId,agentId,graphId,nodeId,prevNodeId,coreSkillId,masterSkillId,masterSkillActivityIdLocalTaskHandler—(args: { input: any; ctx: LocalTaskContext }) => Promise<any>
Execution tracing (metadata)
For graph runtimes (e.g. worex-graph), local task responses include consistent metadata so a trace can be stored per node:
metadata.durationMs— time taken by the handlermetadata.localSkillKey—skillKeythat was run (e.g."skills/node.callExportBatch","skills/skill.local:validateInput")
Downstream can store e.g. executionMemory._trace.nodes[nodeId] with taskKey, ok, durationMs, summary (from parsed.meta when present).
Exported API for local tasks: registerLocalTask, getLocalTask, registerBuiltInLocalTasks, LocalTaskContext, LocalTaskHandler (see API Reference and Types).
ExecutionType
executionType is optional on every request and defaults to DIRECT if not provided.
| Way | How you trigger it | What runs |
|-----|--------------------|-----------|
| NARRIX pre-processor | request.narrix set (e.g. from graph node metadata) | Resolve record → run NARRIX → inject _narrix into executionMemory/jobMemory; then run local task or DIRECT as below. |
| Local task | skillKey = local skill key (e.g. skills/skill.local:narrixRun) | Handler only; no enrichment, no LLM. |
| Executor only | Any other skillKey, executionType omitted or "direct" | Enrich → context → executor/LLM. No Narrix. |
| Narrix then execute | Same as executor, but executionType: "narrix-then-direct" + narrixInput | Narrix → append to taskMemory.narrix → then enrich → context → executor/LLM. |
Currently supported:
DIRECT— single execution call after task-level enrichment/context (default)NARRIX_THEN_DIRECT("narrix-then-direct") — run Narrix first, inject result into task memory and context, then run the same DIRECT path; requiresnarrixInput(or resolution from job memory vianarrixInput.$path). See Narrix then execute below for details.
Unsupported types throw an error with actionable context.
Narrix then execute
Narrix processes four input types (record, text, docs, chat) and supports several use cases (local skills/skill.local:narrixRun, narrix-then-direct, normalize, validate). For the four input objects, their payload shapes, and detailed use cases, see NARRIX-FOUR-OBJECTS-AND-USE-CASES.md.
Use narrix-then-direct when you want one runTask() call to scope data with Narrix (entity, signals, stories) and then run an LLM skill with that context—without calling skills/skill.local:narrixRun and then a second runTask() manually.
Request
Same as a normal executor call, plus:
executionType:"narrix-then-direct"or use the exported constantNARRIX_THEN_DIRECT.narrixInput(required): Either:- Full Narrix input:
{ medium, datasetId, record }(ortext/document/threadper medium). Same shape as the skills/skill.local:narrixRun (input contract) section elsewhere in this README. - Or
{ $path: "jobMemory.currentRecord" }(or anyjobMemory.*dot-path) to resolve fromrequest.jobMemory.
- Full Narrix input:
narrixScope(optional): Filters which signals and stories from Narrix are kept in task memory. Shape:includeSignals?: string[]— keep only signals whosecodeis in this array.excludeSignals?: string[]— drop signals whosecodeis in this array (ignored whenincludeSignalsis set).includeStories?: string[]— keep only stories whosenarrativeTypeIdis in this array.excludeStories?: string[]— drop stories whosenarrativeTypeIdis in this array (ignored whenincludeStoriesis set).- When omitted, all signals and stories pass through unfiltered.
- Example:
narrixScope: { includeSignals: ["OPEN_EGRESS_DETECTED", "PUBLIC_IP_EXPOSED"], includeStories: ["subnet-risk-summary"] }— the task only sees these specific signals and narratives from the full Narrix output.
Where Narrix output goes
- Task memory:
taskMemory.narrixis an array of Narrix run outputs ({ entity, signals, stories, meta }). The executor and LLM receive this in the enriched memory and in the generated context (e.g. "## Narrix Scoping" section). - Convenience: The latest run is also set on the skill input as
input.narrixContextso prompt templates can reference it directly. - Response: On success,
result.metadata.narrixcontains the last run's{ entity, signals, stories, meta, durationMs }.
Minimal example
import { runTask, NARRIX_THEN_DIRECT } from "@exellix/ai-tasks";
const result = await runTask({
skillKey: "tasks/security-risk-summary",
executionType: NARRIX_THEN_DIRECT,
narrixInput: {
medium: "record",
datasetId: "subnetEgress",
record: { graphized: { id: "subnet-1", cidr: "10.0.0.0/24" } },
},
input: { question: "Summarize the security posture of this subnet" },
jobId: "job-1",
agentId: "agent-1",
});
// result.parsed → LLM output (from the skill)
// result.metadata.narrix → { entity, signals, stories, meta, durationMs } from NarrixFailure behavior
If Narrix fails (e.g. ok: false, or NARRIX_DISABLED when USE_NARRIX_INGEST is not set), the function returns early with parsed: { ok: false, phase: "narrix", error, message } and metadata.narrix; the executor/LLM is not called.
Export
NARRIX_THEN_DIRECT is exported from @exellix/ai-tasks for use in executionType.
NARRIX task-level pre-processor
When a task request includes a narrix config (e.g. from graph node taskConfiguration.narrix), ai-tasks runs the NARRIX enrichment pipeline before executing the task and injects the result so the task handler (or LLM) can use it. This supports "narrix serving graph": graph nodes are domain tasks (e.g. assess reachability, assess posture); NARRIX is configuration on the node that enriches context, not a separate pipeline exposed as graph steps.
Request
Add optional narrix to RunTaskRequest:
narrix.datasetId(required): e.g."network.egress.subnets","network.vuln.instances".narrix.attachToField(optional): field name onexecutionMemoryfor the attachment; default"_narrix".narrix.engineConfigPath,narrix.packsRoot,narrix.deterministicSort,narrix.assumptionsPolicy: reserved for future use.
NARRIX builds the _narrix CNI attachment only. It does not trigger or configure web scoping — for web context add a standalone webScope PRE unit (below).
Web scoping and upstream packages
Web scoping is implemented by @exellix/web-scope, which wraps @x12i/web-queries (query planning + WebContext mapping) through @x12i/web-scoper-memorix (TTL caching + synthesized-knowledge persistence) and @x12i/search-adapter (Tavily normalization).
The only trigger is a PRE unit with strategyKey: "webScope". Its unitParams:
webQueryTemplate(required when enabled): Rendrix string rendered into the search query, e.g."How to solve {{input.vulnerability}}?".webQueryTemplates(optional):string[]→ each rendered into asearchManypack.enableWebScope(defaulttrue): on/off switch.options:WebScopeOptions(maxQueries,freshnessDays, domain filters,queryTemplates, …).
Requires TAVILY_API_KEY for live search. The rendered template is validated at config time (WEB_QUERY_TEMPLATE_REQUIRED / WEB_QUERY_TEMPLATE_UNRESOLVED); there is no input.question fallback. Playground input is record/facts only, referenced by the template via {{input.*}}.
Each WebContext.sources[] entry includes url, domain, snippet, retrievalStage, and meta.discoveredCount. Snippet/fetch behavior follows WebScoperConfig defaults in @x12i/web-queries (see that package README for caps and page fetch). Successful webScope units also trigger forced web synthesis and optional Memorix persistence (MONGO_URI, EXELLIX_NAME, TTL vars).
Downstream LLM context: On the DIRECT path with includeContextInPrompt, when Narrix output is in play, ai-tasks appends a markdown section from executionMemory.webContext (when ok === true) so the model sees source bodies, not only NARRIX signal codes. The synthesized-context PRE step uses the same markdown builder for policies that include web (e.g. narrix-web, narrix-web-memory, memory-web, auto when a web hit exists). Optional env WEB_CONTEXT_MARKDOWN_MAX_CHARS and optional SynthesisConfig.webEvidence cap and shape that markdown. Serialized memory for synthesis never embeds raw webContext as JSON; web appears as markdown only when the policy includes web.
Technical flow, failure behavior, and consumption patterns: documenations/web-scoping-in-ai-tasks.md.
Raw record resolution
The pre-processor needs a raw record to enrich. Resolution order:
executionMemory.input.rawexecutionMemory.inputs.<first key>.rawjobMemory.recordorjobMemory.currentRecordinput.recordorinput.raw
If no record is found, runTask throws with a clear error. sourceMeta is taken from executionMemory.input.sourceMeta or jobMemory.sourceMeta or {}.
Where the result goes
- Canonical:
executionMemory[attachToField](defaultexecutionMemory._narrix) — shape{ scoping: { stories, signals }, discovery: { stories, signals }, meta }. - Mirrored:
jobMemory._narrix— same attachment so handlers can read from eitherctx.executionMemory._narrixorctx.jobMemory._narrix. - Web scope: when a
webScopePRE unit runs,executionMemory.webContextis set to theWebScopeResult/WebScopePackResultfrom@exellix/web-scope(ok: truehit or structured miss/error). Successful scoping also runs web synthesis intoexecutionMemory.synthesizedContext. Independent of_narrix.
Local task handlers receive the enriched ctx. On the DIRECT path, context is added only when includeContextInPrompt === true. When NARRIX is in play, that context is the "## Scoping and discovery" section from executionMemory._narrix when present, plus a Web sources (primary evidence) section when web scoping returned webContext.ok === true (see above). We do not inject a "Narrix Pre-Processor" placeholder. The context generator (xontext) is not given _narrix when NARRIX is in play, so it cannot emit that section.
Per-node enrichment
Each runTask call with narrix set is independent. If a graph runs two nodes with narrix config, each gets its own NARRIX run (no shared state). Orchestrators (e.g. graph-engine) lift node.taskConfiguration.narrix to RunTaskRequest.narrix.
Example
await runTask({
skillKey: "tasks/assess-reachability",
narrix: { datasetId: "network.egress.subnets" },
executionMemory: { input: { raw: subnetRecord } },
input: { question: "Assess reachability based on scoping." },
});
// Handler or LLM sees ctx.executionMemory._narrix (scoping, discovery, meta)Intermediate steps (multi-step tasks)
When a task runs multiple logical steps in one invocation (e.g. a combined node that does "to-cni + enrich + triage"), the response can include an optional intermediateSteps array so consumers can inspect and report the chain without extra round-trips.
Response shape
intermediateSteps(optional): array of steps in execution order. Each step has:step(number): 1-based orderid(string): stable identifier (e.g."to-cni","engine-enrich","triage-q1-q6")ok(boolean): whether the step succeededsummary(optional): short description (e.g."cni built","enriched")error(optional): error message whenokis falseinputSummary(optional): summary input for reportingoutputExcerpt(optional): small excerpt of step output for debugging
Skills/tasks can return steps inside parsed (e.g. parsed.intermediateSteps); the SDK lifts them to the top-level response.intermediateSteps so consumers always read from the same place. Local tasks can return { output: {...}, intermediateSteps: [...] }
