npm.io
10.17.1 • Published 1 week ago

@exellix/ai-tasks

Licence
exellix-license
Version
10.17.1
Deps
24
Size
2.1 MB
Vulns
0
Weekly
0

@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/xynthesis in this repo already uses Catalox 6 / Mongo locally (4.9.13 prepared, not published). @exellix/ai-tasks and @exellix/catalox-ai-skills still use Firebase until @x12i/funcx@4.10.0 ships and catalox-ai-skills migrates. Phase 2 then aligns ai-skills-manager-ui and ai-tasks-playground-ui. Do not publish the stack until then. See documenations/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/splitmainSkillmerge/pick-best Synthesis uses invokeContract.reads + optional unitParams.memoryPathsnot 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):

  1. Validate nodePlan and reject forbidden legacy wire fields (executionPipeline, taskConfiguration, root modelConfig)
  2. if a local task handler is registered for skillKey → run it and return
  3. Loop nodePlan.executionUnits in order (PRE utilities, MAIN skill, POST utilities) via executeNodeFromPlan
  4. Return RunTaskResponse with optional executionTrace events

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.taskCore is removed from structured synthesis semantics; runtime derives cores from template declarations.
  • Synthesis mode selection is additive:
    • preferred: SynthesisConfig.synthesisMode: "markdown" | "structured"
    • legacy-compatible: SynthesisConfig.synthesisOutputFormat still works.
  • 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 Catalox ai-skills catalog in @exellix/ai-skills 4.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 at executionMemory.synthesizedContext for MAIN traceability unless a PRE step opts out via SynthesisConfig.xynthesizedOutput.alsoWriteLegacySynthesizedContext: false (see Xynthesized memory and smart input).
  • Optional RunTaskRequest.xynthesized holds job-scoped, task-scoped, and execution-scoped synthesized material for graph execution (distinct from raw memories). Optional smartInput matches SmartInputConfig from @exellix/ai-skills / Rendrix (paths as { title, path, required? }[]). Callers may still send legacy paths: string[]; runTask validates and normalizes each string to { title: path, path } before runSkill. Paths under xynthesized.* must use scope job, task, or execution; full path resolution for {{smartInput}} happens in the gateway/Rendrix stack. Optional smartInputRenderOptions is passed through like other RunSkillRequest fields.
  • 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 as RunSkillRequest: templateRenderOptions, templateTokens, smartInputRenderOptions (with smartInput). They are passed through unchanged to runSkill on the DIRECT / pipeline MAIN path.
  • Default parser options for all skill runs: set templateRendering on ExellixSkillsClientOptions when constructing ExellixSkillsClient (or configure templateRendering on a gateway instance you pass as options.gateway). Per-call overrides use templateRenderOptions on 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-tasks

Custom gateway / advanced injection: add @exellix/ai-skills and construct WorexClientTasks with your own client + executor.

npm install @exellix/ai-tasks @exellix/ai-skills

Bundled 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-strategies

That 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.input can be any JSON-compatible object (or string). input.question is optional.
  • Correlation (required): every runTask request must include non-empty agentId, jobTypeId, and taskTypeId (same as @exellix/ai-skills). See RUNTASK_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 parsed payload (for downstream graph nodes), not only prose in rawText.

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.missingSignals

getPackagedAiTasksMetadataDir() 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 output
Function-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:

  1. Validate nodePlan (graphenix.node-plan/v2.1) and reject forbidden wire fields. See RUNTASK_REQUEST.md.
  2. Local task dispatch: If getLocalTask(skillKey) returns a handler, run it and return (no LLM MAIN).
  3. Node execution: executeNodeFromPlan loops ordered executionUnits (PRE utilities, MAIN skill, POST utilities), appending executionTrace events.
  4. 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 string
  • modelUsed: string for LLM-backed steps; null for deterministic steps
  • metadata.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: Same xynthesized / smartInput as on the runTask request after normalization (pipeline PRE runs later and can mutate xynthesized before MAIN).
  • PRE synthesized-context: The bundle passed into template/rendering includes xynthesized and smartInput so synthesis prompts can reference them consistently with MAIN.
  • MAIN executor payload: Top-level xynthesized, smartInput (normalized), and smartInputRenderOptions (when set) are forwarded on the object passed to runSkill (alongside memories and context).
  • Template variables: variables (job bucket) and executionMemory.jobVariables / executionMemory.taskVariables are forwarded separately; use {{xynthesized…}}, {{smartInput}}, jobVariables.*, and taskVariables.* in templates (not a single flattened bag).
  • Response: When downstream returns a SmartInputRenderResult, runTask finalize exposes response.smartInputRenderResult and mirrors metadata.smartInputRenderResult (also reads metadata.smartInput when it matches that shape).
PRE synthesis destination (SynthesisConfig.xynthesizedOutput)

Optional on the synthesized-context step config:

  • destination: "job" | "task" | "execution" — writes under request.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 at outputKey and the new value are both plain objects, ai-tasks deep-merges them; otherwise the new value replaces.
  • alsoWriteLegacySynthesizedContext: When true or omitted, keep today’s behavior: persist structured/markdown artifacts on executionMemory.synthesizedContext (and synthesizedInput when applicable) for MAIN. When false, 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.execution
Builder

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 this skillKey.
  • 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 / EvidenceBundle exported from @exellix/ai-tasks (implemented in src/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-request value.requests[]. If extraction.returnFacts / extraction.returnSummary are set, the reference handler currently returns empty facts: [] / 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; // EvidenceBundle
node.callExport (input contract)
  • module: string (module name or path for dynamic import())
  • export: string (default "default") — export name
  • call: { method: string, args?: any[] }method can 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" } (or jobMemory.*, taskMemory.*, executionMemory.*, variables.*); resolved via dot-path before calling.
node.callExportBatch (input contract)
  • module, export, cache: same as above
  • call.create: { method, args? } — used to create/cache the instance
  • call.batch: { method, args? } — method and args for the batch call (args can use $path)
  • payload, options: typically passed via $path in call.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 record
  • rules.graphTypeEquals: optional string — require graphized.meta.graphType to equal this value
  • datasetId: optional (for validation rules)
skills/skill.local:normalizeNarrixResult (input contract)
  • recordsPath: { $path: "..." } — path to records array
  • attachRoot: string (default "_narrix") — key under which Narrix attachment lives
  • defaults.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/executor
  • registerLocalTask(skillKey: string, handler: LocalTaskHandler): void — register a handler for skillKey
  • getLocalTask(skillKey: string): LocalTaskHandler | undefined — get handler (used internally by runTask)
  • LocalTaskContextskillKey, jobMemory, taskMemory, executionMemory, variables, xynthesized, smartInput, jobId, taskId, agentId, graphId, nodeId, prevNodeId, coreSkillId, masterSkillId, masterSkillActivityId
  • LocalTaskHandler(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 handler
  • metadata.localSkillKeyskillKey that 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; requires narrixInput (or resolution from job memory via narrixInput.$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 constant NARRIX_THEN_DIRECT.
  • narrixInput (required): Either:
    • Full Narrix input: { medium, datasetId, record } (or text / document / thread per medium). Same shape as the skills/skill.local:narrixRun (input contract) section elsewhere in this README.
    • Or { $path: "jobMemory.currentRecord" } (or any jobMemory.* dot-path) to resolve from request.jobMemory.
  • narrixScope (optional): Filters which signals and stories from Narrix are kept in task memory. Shape:
    • includeSignals?: string[] — keep only signals whose code is in this array.
    • excludeSignals?: string[] — drop signals whose code is in this array (ignored when includeSignals is set).
    • includeStories?: string[] — keep only stories whose narrativeTypeId is in this array.
    • excludeStories?: string[] — drop stories whose narrativeTypeId is in this array (ignored when includeStories is 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.narrix is 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.narrixContext so prompt templates can reference it directly.
  • Response: On success, result.metadata.narrix contains 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 Narrix
Failure 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 on executionMemory for 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 a searchMany pack.
  • enableWebScope (default true): 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:

  1. executionMemory.input.raw
  2. executionMemory.inputs.<first key>.raw
  3. jobMemory.record or jobMemory.currentRecord
  4. input.record or input.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] (default executionMemory._narrix) — shape { scoping: { stories, signals }, discovery: { stories, signals }, meta }.
  • Mirrored: jobMemory._narrix — same attachment so handlers can read from either ctx.executionMemory._narrix or ctx.jobMemory._narrix.
  • Web scope: when a webScope PRE unit runs, executionMemory.webContext is set to the WebScopeResult / WebScopePackResult from @exellix/web-scope (ok: true hit or structured miss/error). Successful scoping also runs web synthesis into executionMemory.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 order
    • id (string): stable identifier (e.g. "to-cni", "engine-enrich", "triage-q1-q6")
    • ok (boolean): whether the step succeeded
    • summary (optional): short description (e.g. "cni built", "enriched")
    • error (optional): error message when ok is false
    • inputSummary (optional): summary input for reporting
    • outputExcerpt (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: [...] }; the SDK exposes intermediateSteps on the response and keeps only the rest in parsed. For narrix-then-direct, the runtime prepends a step { step: 1, id: "narrix", ok: true } and renumbers any steps from the direct phase.

Example
const res = await runTask({ skillKey: "tasks/combined-cni-enrich-triage", input: { ... } });

if (res.intermediateSteps) {
  for (const s of res.intermediateSteps) {
    console.log(`Step ${s.step}: ${s.id} ${s.ok ? "ok" : s.error}`);
  }
}
// res.parsed holds the final task output (intermediateSteps are not duplicated there)
Execution pipeline and synthesized-context (PRE step)

Breaking change: Execution can use an execution pipeline instead of a single executionType. See BREAKING-CHANGES.md for migration from executionType to executionPipeline.

  • Pipeline: executionPipeline is an array of steps with phases pre, main, and post. Order: all PRE steps (in order) → exactly one MAIN step → all POST steps (optional; built-in types: audit, polish). Default when omitted: [{ phase: "main", type: "direct" }]. Existing executionType (e.g. DIRECT, NARRIX_THEN_DIRECT) is still supported when executionPipeline is not set.
  • PRE step synthesized-context: A weak model (default gpt-5-nano) synthesizes source material (Narrix / memory / web per policy below) into a single context string; the MAIN task then runs with that string as its context. Requires includeContextInPrompt: true (or the PRE step’s config.autoEnableContext: true). Add { phase: "pre", type: SYNTHESIZED_CONTEXT, config: SynthesisConfig } before the main step (SYNTHESIZED_CONTEXT is exported as the string "synthesized-context").
  • Two LLM calls (typical): deterministic source material (policy + markdown/JSON assembly) → synthesis model → synthesized contextmain task model. Synthesis is not a pure string merge; it is a separate gateway invocation before the main skill.
  • SynthesisConfig (on the PRE step’s config): modelConfig, contextSourcePolicy, optional synthesisInputStrategy (policy / execution-memory-only / job-memory-only / task-memory-only / full-memory-bundle), optional webEvidence, customSynthesizingGuidelines, fallbackToDirect (default false), memoryPaths, synthesisPromptOverride, timeoutMs, maxOutputLength, autoEnableContext, optional mode selectors synthesisMode (preferred) and synthesisOutputFormat (legacy-compatible), plus optional questionPath, getQuestion, structuredMaxItemsPerSide, structuredMaxItemContentChars, plus optional questionDriven and questions, plus optional xynthesizedOutput (destination: "job" | "task" | "execution", outputKey, mode, alsoWriteLegacySynthesizedContext) to mirror synthesis into request.xynthesized and return response.xynthesizedPatch (see Xynthesized memory and smart input). Full API: documenations/synthesized-context-guide.md.
  • Question-driven synthesis (opt-in): Set questionDriven: true and provide questions ([{ id, question, source?: "record"|"web"|"both" }]). This mode runs one synthesis per question and writes a structured artifact to executionMemory.synthesizedContext with mode: "questionDriven" and stable access path executionMemory.synthesizedContext.answers.<id>.synthesis. Web-only questions degrade gracefully when executionMemory.webContext is unavailable (answer is present but marked unknown + reason).
  • Structured synthesis (opt-in): Set synthesisMode: "structured" (or legacy-compatible synthesisOutputFormat: "structured") so the synthesis model returns JSON matching SynthesizedPromptPayload (SynthesisInput, SynthesizedContext, SynthesizedItem, etc., exported from @exellix/ai-tasks). The package validates the shape, then builds main-step context markdown via buildSynthesizedContextMarkdown (task cores → question → local → supporting → unknowns → assumptions). Raw local vs web blocks are passed as local_raw / supporting_raw in the synthesis prompt (see resolveSourceMaterialParts). The task question is taken from getQuestion(request), else questionPath on request.input (default path question), else the rendered downstream user prompt (truncated). In structured mode, template core tokens are required in template content; no detected template cores is treated as a template-definition error. Structured artifacts are persisted at executionMemory.synthesizedContext. Templates: templates/synthesis/system-structured.md and user-structured.txt (override path still SYNTHESIS_TEMPLATES_PATH). For tests or custom execution, use setContextSynthesizer / getContextSynthesizer; runStructuredSynthesisGatewayCall mirrors the default gateway path.
  • Synthesis uses a real LLM call (AIGateway.invoke via xynthesis executeXynthesisAction) — not just string merging. Building source_material is deterministic; the PRE step then invokes the synthesis model (e.g. SYNTHESIS_MODEL / modelConfig) before the MAIN step runs. See documenations/synthesis-invocation-notes.md for details.
  • webEvidence: Optional tuning for the Web sources markdown block inside synthesis source material: preferCleanContent (default true), maxSources (default 5), dedupeByUrl (default true), maxTotalChars. Env WEB_CONTEXT_MARKDOWN_MAX_CHARS still applies when maxTotalChars is not set.
  • contextSourcePolicy (what becomes <source_material> in the synthesis prompt):
Policy Narrix section Web markdown Memory JSON
narrix-web Yes (required) If webContext hit No
narrix-web-memory If present If hit Yes — never embeds raw webContext in JSON
memory-web Stripped from JSON If hit Yes
memory-only No No Yes — never embeds raw webContext in JSON
auto Resolves at runtime

auto resolution: narrix-web-memory if Narrix output exists (executionMemory[attachToField] or coerced taskMemory.narrix); else memory-web if web scoping produced a usable hit; else memory-only. Legacy aliases: narrix-onlynarrix-web, narrix+memorynarrix-web-memory.

Synthesis output shape (default): templates/synthesis/system.md asks the weak model for three markdown sections in order: ## Local context (primary), ## Web evidence (supporting), ## Supporting sources (optional) (short traceability lines only). Override with synthesisPromptOverride if needed. With synthesisOutputFormat: "structured", output is JSON validated to the generic synthesis contract, then converted to markdown for the main step only.

Migration note: If you relied on auto with Narrix but without memory in the synthesizer input, set contextSourcePolicy: "narrix-web" explicitly. See BREAKING-CHANGES.md (Synthesis contextSourcePolicy and auto).

  • Templates: Synthesis prompts live in templates/synthesis/ — markdown mode: system.md, user.txt; structured mode: system-structured.md, user-structured.txt. Optional env SYNTHESIS_TEMPLATES_PATH for base path.
  • Caching: Optional synthesis result caching via nx-cache (same project) can be used to reduce cost for batch or repeated runs.
  • Non-streaming: Synthesis returns a single complete response; there is no streaming support for the synthesis step.
  • Builder: withExecutionPipeline(steps), withSynthesizedContextPreStep(modelOrConfig?) to add the PRE step and set includeContextInPrompt: true (pass a SynthesisConfig object for contextSourcePolicy, webEvidence, memoryPaths, xynthesizedOutput with destination: "job" | "task" | "execution", etc.). Also withXynthesized, withSmartInput, withSmartInputPaths, withSmartInputRenderOptions, withXynthesizedJob, withXynthesizedTask, withXynthesizedExecution for request-level fields.
  • Auditability fields in response metadata: synthesisEnabled, effectiveExecutionPipeline, synthesizedContextPresent, mainContextSource, detectedTemplateCores. Final response also includes normalized executionMemory and executionState.executionMemory.

Minimal example (explicit policy + web tuning):

import { SYNTHESIZED_CONTEXT } from "@exellix/ai-tasks";

executionPipeline: [
  {
    phase: "pre",
    type: SYNTHESIZED_CONTEXT,
    config: {
      contextSourcePolicy: "narrix-web-memory",
      webEvidence: { maxSources: 3, preferCleanContent: true },
      modelConfig: { model: "gpt-5-nano" },
    },
  },
  { phase: "main", type: "direct" },
],
includeContextInPrompt: true,
POST Step Strategies (post-execution)

The execution pipeline supports POST steps that run after the MAIN step and operate on its response. Two built-in strategies: audit (quality-gate loop with re-runs) and polish (refinement pass against a prioritized checklist). POST steps run in pipeline order; each step’s output becomes the next step’s input.

Pipeline example:

executionPipeline: [
  { phase: "pre",  type: "synthesized-context", config: { /* ... */ } },
  { phase: "main", type: "direct" },
  { phase: "post", type: "audit",  config: { gateway: { must: [...], should: [...] }, maxCycles: 3 } },
  { phase: "post", type: "polish", config: { checklist: [...], maxPasses: 2 } },
]

Metadata for each POST step is under response.metadata.postSteps.<type>. All POST steps’ intermediate steps are merged into response.intermediateSteps with continuous numbering (e.g. audit-cycle-1, audit-synthesis, audit-select, polish-pass-1).

Fact-guard (optional sub-action integrity)

Optional atom integrity checks for sub-actions only (PRE synthesis, POST audit/polish/scoping, execution-strategy planner/optimizer). MAIN runSkill is never guarded. Requires @exellix/fact-guard-* ^1.0.0 (transitive via this package).

When Behavior
factGuard absent or enabled: false Identical to pre–fact-guard behavior
FACT_GUARD_DISABLED=1 Emergency kill-switch — all guards bypassed

Config attachment:

Step Field
PRE synthesized-context SynthesisConfig.factGuard
POST audit AuditConfig.factGuard.audit / .synthesis (merge)
POST polish PolishConfig.factGuard
AI scoping RunTaskRequest.aiScopingOptions.factGuard
Execution strategy ExecutionStrategyInvocation.factGuard
{
  "enabled": true,
  "policyId": "prompt-context-synthesizer",
  "onViolation": "fallback",
  "maxRetries": 2,
  "onExhausted": "fallback"
}

Violations appear in structured logs (fact_guard.sub_action) and optionally on response.metadata.factGuard[stage] (trace mode). See documenations/synthesized-context-guide.md and ../fact-guard/README.md.

Tests: npm run test:fact-guard (mocked LLM; no API key required).

POST step: audit

Quality-gate loop: evaluate MAIN output against must and should checks, get actionable feedback, re-run the MAIN step when criteria aren’t met, then choose the best result (or run an optional synthesis merge of the top two candidates).

  • Config: AuditConfig: gateway.must, gateway.should (each { check, weight }), threshold (default 80), minCycles, maxCycles (default 3), selectionStrategy ("best" | "synthesis"), auditModelConfig, synthesisModelConfig, customAuditGuidelines, customSynthesisGuidelines, fallbackToBest (default true), auditTimeoutMs.
  • Output: Audit uses structured Markdown (Flex-MD style); the runtime parses it into checks and overall feedback and computes weightedScore.
  • Re-runs: When the loop continues, the MAIN step is re-invoked with the original context plus a feedback block from templates/post-steps/audit/feedback-injection.md.
  • Metadata: response.metadata.postSteps.audit: totalCycles, candidateScores, selectedCycleIndex, allMustPassed, synthesisUsed, auditResults, durationMs.
  • Caching: Audit (and synthesis) LLM calls use nx-cache when inputs and config are unchanged (no cache on re-runs with feedback).
  • Failure: If all cycles are exhausted and no candidate passes all must checks: with fallbackToBest: true (default) the best-scored candidate is returned and allMustPassed: false; with fallbackToBest: false an error is thrown.
POST step: polish

Single- or multi-pass refinement against a prioritized checklist. Does not re-run MAIN; always produces a refined output (or falls back to the last good output on failure).

  • Config: PolishConfig: checklist (each { instruction, priority: "high" | "medium" | "low" }), maxPasses (default 1), modelConfig, customGuidelines, includeOriginalContext, timeoutMs.
  • Output: Polish LLM returns JSON: { polishedOutput, changeNotes[] }.
  • Metadata: response.metadata.postSteps.polish: totalPasses, changeNotes (per pass), durationMs.
  • Failure: Polish never throws; on LLM/parse failure it returns the previous output and records ok: false in the step.
Minimal examples

Audit only:

const result = await runTask({
  skillKey: "tasks/security-risk-summary",
  input: { assetId: "a-123" },
  jobId: "job-1",
  agentId: "agent-1",
  executionPipeline: [
    { phase: "main", type: "direct" },
    {
      phase: "post",
      type: "audit",
      config: {
        gateway: {
          must: [{ check: "Output includes a severity rating (critical/high/medium/low)", weight: 10 }],
          should: [{ check: "Recommendations are ordered by priority", weight: 5 }],
        },
        threshold: 80,
        maxCycles: 3,
      },
    },
  ],
});
console.log(result.parsed);                    // best candidate output
console.log(result.metadata.postSteps?.audit); // totalCycles, candidateScores, allMustPassed, etc.

Polish only:

const result = await runTask({
  skillKey: "tasks/executive-summary",
  input: { report: "..." },
  executionPipeline: [
    { phase: "main", type: "direct" },
    {
      phase: "post",
      type: "polish",
      config: {
        checklist: [
          { instruction: "Remove jargon; use plain language", priority: "high" },
          { instruction: "Each paragraph ≤ 3 sentences", priority: "medium" },
        ],
        maxPasses: 2,
      },
    },
  ],
});
Chaining audit + polish

Common pattern: audit first to ensure quality, then polish to refine the approved candidate. Audit’s chosen output becomes polish’s input.

Builder
  • withAuditPostStep(config: AuditConfig) — Appends an audit POST step; ensures pipeline has a MAIN step.
  • withPolishPostStep(config: PolishConfig) — Appends a polish POST step; ensures pipeline has a MAIN step.

Example with builder (no need to set executionPipeline manually):

const request = new TaskRequestBuilder()
  .withSkillKey("tasks/security-risk-summary")
  .withInput({ assetId: "a-123" })
  .withJobId("job-1")
  .withAgentId("agent-1")
  .withAuditPostStep({
    gateway: {
      must: [{ check: "Output includes severity rating", weight: 10 }],
      should: [{ check: "Recommendations ordered by priority", weight: 5 }],
    },
    threshold: 80,
    maxCycles: 3,
  })
  .withPolishPostStep({
    checklist: [
      { instruction: "Use active voice throughout", priority: "high" },
      { instruction: "Remove redundant qualifiers", priority: "medium" },
    ],
  })
  .build();
const result = await runTask(request);
Template paths
  • Audit: templates/post-steps/audit/ (system.md, user.txt, feedback-injection.md, synthesis.md). Override: AUDIT_TEMPLATES_PATH (base path for the audit folder).
  • Polish: templates/post-steps/polish/ (system.md, user.txt). Override: POLISH_TEMPLATES_PATH.

Templates use Handlebars ({{variable}}, {{#each}}, {{#if}}).

Design decisions
  • Audit re-runs MAIN (not the audit model): The MAIN step has the full prompt and skill context; re-running it with audit feedback yields better corrections.
  • Polish does not re-run MAIN: Polish is surface-level (tone, format); a dedicated rewrite step is cheaper and sufficient.
  • Audit output is Markdown (Flex-MD), not forced JSON: More robust across models; runtime parses structured sections.
  • nx-cache for audit/synthesis: Reduces cost for repeated or batch runs with identical inputs.
  • No global cost guardrails (e.g. maxTotalPostCalls) in this release; control via maxCycles and maxPasses.
  • No streaming for POST steps; final output is returned after all POST steps complete.
Testing

POST steps are covered by unit and integration tests in test/post-steps/:

  • resolvePostStepConfig.test.ts — model and timeout resolution (config, env, fallback).
  • parseAuditOutput.test.ts — parsing audit structured markdown into checks and overall feedback.
  • parsePolishOutput.test.ts — parsing polish JSON (including code-fenced) into polishedOutput and changeNotes.
  • runAuditPostStep.test.ts — audit step with mocked gateway: passing/failing markdown, fallback, and audit-call failure.
  • runPolishPostStep.test.ts — polish step with mocked gateway: valid JSON, call failure fallback, multiple passes.
  • pipeline-post-steps.test.ts — pipeline integration: runTask with executionPipeline (main + audit, main + polish) and mocked executor/gateways; asserts metadata.postSteps and final output.

Run post-steps tests: npm test (includes dist-test/test/post-steps/*.js).

Types

Use RunTaskResponseWithSteps<TParsed> when you expect steps; it extends RunSkillResponse with intermediateSteps?: IntermediateStep[]. When POST steps run, response.metadata.postSteps is set (audit?: AuditPostStepMetadata, polish?: PolishPostStepMetadata). Exported POST step types: AuditConfig, AuditCheck, AuditResult, AuditCheckResult, AuditLLMOutput, AuditPostStepMetadata, PolishConfig, PolishChecklistItem, PolishLLMOutput, PolishPostStepMetadata, RunTaskResponsePostStepsMetadata. See the Types section and documenations/intermediate-steps.md for full details.


How completion budgets work (8.4+)

Orchestrators send model + sampling + reasoningEffort + (for xynthesis) outputExpectation. They do not send completion token counts.

Phase Package Caller sends Package owns
MAIN @exellix/ai-skills ≥6.4 modelConfig{ model, temperature, topP, reasoningEffort, … } Optimixer + Catalox skill catalog (maxTokens on ModelConfig is rejected); gateway @x12i/ai-gateway ≥10.1 supplies wire maxTokens and authoritative costUsd / costStatus on invoke metadata; one automatic re-invoke when output hits token limit and Optimixer returns retryPrediction
PRE / POST / scoping / utility @exellix/xynthesis ≥4.5 outputExpectation (required on every hop), optional reasoningEffort, temperature, topP resolveEffectiveMaxTokens inside executeXynthesisAction (default @x12i/funcx invoker); authoritative billing from funcx/gateway engines — xynthesis is pass-through only; same one-shot max-token retry via Optimixer retryPrediction

Xynthesis outputExpectation is sizing/density intent for Optimixer — not a literal token count. ai-tasks resolves a per-stage default when the caller omits it (resolveOutputExpectation / resolveLlmOutputExpectationForXynthesis).

Rejected on the wire (validation): modelConfig.maxTokens, llmCall.maxTokens, llmCall.maxTokensCap, deprecated xynthesisModel, root modelConfig.model.

Trace: use usage.maxTokensRequested / invokeSummary diagnostics after invoke — not pre-set caps on the request.

Billing (gateway 10.x): MAIN cost is authoritative from @x12i/ai-gateway invoke metadata only — ai-tasks and ai-skills do not re-price. When costStatus === 'unpriced', tokens are recorded but there is no authoritative USD total (do not price locally). See UPGRADING_AI_GATEWAY_10.md §1.


Model profile aliases (@x12i/ai-profiles)

@exellix/ai-tasks resolves profile aliases to concrete provider model ids via @x12i/ai-profiles at execution time. This applies to both execution paths:

Path Slot / field Resolved before
Xynthesis (PRE synthesis, POST audit/polish, AI scoping, utilities) preActionModel, postActionModel, step llmCall.model, RunUtilityRequest.exec.model executeXynthesisAction / runPostStepLlmCall
ai-skills MAIN skillModel, RunTaskRequest.llmCall.model runSkill (via DirectExecutionStrategy)

Accepted alias shapes (@x12i/ai-profiles v2.1+):

  • Profile/choice keys (required on stored modelConfig and xynthesis llmCall.model): cheap/default, pro/default, cyber/default, …
  • Concrete provider wire ids on MAIN only: openrouter/..., anthropic/claude-sonnet-4, bare vendor SKUs like gpt-4o-mini — accepted on skillModel / MAIN llmCall.model only

Rejected on stored config:

  • Bare profile names (cheap, pro) — use cheap/default, pro/default
  • profile@choice (cheap@default) — use profile/choice slash form
  • Legacy graph tiers (weak, strong), deprecated xynthesisModel, root modelConfig.model

List available keys: listAiTasksProfileChoices() / listAiTasksProfileChoiceKeys() (re-exported from @x12i/ai-profiles via @exellix/ai-tasks). Same registry as xynthesis (listAIProfiles / listAIProfileChoices on @exellix/xynthesis).

Example (canonical triplet — resolved inside runTask):

await runTask({
  skillKey: "tasks/analysis",
  agentId: "agent-1",
  jobTypeId: "analysis-job",
  taskTypeId: "analysis-task",
  executionStrategies: [],
  input: { topic: "Q1 risks" },
  modelConfig: {
    preActionModel: "cheap/default",   // PRE synthesis (xynthesis)
    postActionModel: "cheap/default",  // POST audit/polish (xynthesis)
    skillModel: "openai/gpt-5",         // MAIN skill (ai-skills → ai-gateway)
  },
});

Concrete ids pass through unchanged on skillModel / MAIN llmCall.model. preActionModel / postActionModel and xynthesis llmCall.model must stay profile/choice aliases — concrete ids are rejected with XYNTHESIS_CONCRETE_MODEL_REJECTED.

Client impact:

  • skillModel / MAIN llmCall.model: profile/choice alias or concrete provider wire id.
  • preActionModel / postActionModel: profile/choice only — graph-engine must not pre-resolve xynthesis slots to concrete ids.
  • Validation: validateRunTaskConfig / resolveRunTaskModelReferences enforce slot shapes before invoke; unknown aliases fail at resolution with XYNTHESIS_ALIAS_RESOLUTION_FAILED.
  • Helpers: resolveModelReference, resolveModelReferenceForXynthesis, resolveRunTaskModelReferences, resolveInvocationPlan, isRunTaskModelResolutionError, isResolvableModelAlias (true only for known profile/choice keys).
Pre-run invocation plan (resolveInvocationPlan)

Side-effect-free preview of which engine + wire model each phase will use (same routing path as execute):

import { resolveInvocationPlan } from "@exellix/ai-tasks";

const plan = await resolveInvocationPlan({
  profiles: {
    preActionModel: "cheap/default",
    skillModel: "openai/gpt-5",
    postActionModel: "cheap/default",
  },
  policy: {
    preferOpenRouter: true,          // default: read from env when omitted
    openrouterApiKeyPresent: true,   // fallback to vendor-direct when false
  },
});
// plan.phases[] → { phase: "preAction"|"skill"|"postAction", routing, modelId, engineLabel, ... }

OpenRouter routing (preferOpenRouter): when true and an OpenRouter API key is present, MAIN (ai-gateway) and PRE/POST (funcx) resolve to different wire shapes — skill phases use openrouter/... gateway ids; xynthesis phases use Funcx API slugs. Helpers: resolvePreferOpenRouterPolicy, readPreferOpenRouterFromEnv, detectOpenRouterApiKeyPresent, resolveProfileInvocationRouting. See @exellix/ai-skills PREFER_OPENROUTER_ROUTING.md.

Resolution uses the ai-profiles registry (auto: remote refresh with bundled fallback). See MODEL-CONFIGURATION.md and BREAKING-CHANGES.md (8.4+ triplet contract).


LLM call configuration (LlmCallConfig)

LlmCallConfig is the per-stage override knob set. On MAIN it composes with RunTaskRequest.modelConfig slots (preActionModel, skillModel, postActionModel) — when both are set, llmCall.* wins.

type LlmCallConfig = {
  model?: string;                        // provider/model id or ai-profiles alias (resolved at call time)
  temperature?: number;
  topP?: number;
  reasoningEffort?: OptimixerReasoningEffort; // overrides Catalox default when set
  maxOutputLength?: number;              // post-call character truncation
  timeoutMs?: number;
  outputExpectation?: OutputExpectation; // xynthesis Optimixer intent (not a token count)
};

Per-stage placement:

Stage Where to set llmCall
MAIN skill RunTaskRequest.llmCall (overrides modelConfig.skillModel)
AI scoping (per-instruction) RunTaskRequest.aiScoping[].llmCall
AI scoping (request fallback) RunTaskRequest.aiScopingOptions.llmCall
PRE synthesis SynthesisConfig.llmCall (on the synthesized-context PRE step)
POST audit (evaluator) AuditConfig.llmCall or AuditConfig.audit.llmCall
POST audit (synthesis-merge) AuditConfig.audit.synthesis.llmCall
POST polish PolishConfig.llmCall
Utility (xynthesis-finalize) RunUtilityRequest.exec.{model, temperature, topP, reasoningEffort, outputExpectation, ...}

Builder helpers (TaskRequestBuilder):

  • withLlmCall(llmCall) — main skill.
  • withAiScopingOptions({ llmCall, concurrency, timeoutMsPerItem }) — fallback for every aiScoping[] item.
  • withSynthesisLlmCall(llmCall) — PRE synthesized-context step.
  • withAuditLlmCall(llmCall, role?) — POST audit step (role: "audit" | "synthesis", omit for top-level).
  • withPolishLlmCall(llmCall) — POST polish step.
  • withXynthesized(memory) / withSmartInput(config) / withSmartInputPaths(paths) / withSmartInputRenderOptions(opts) — graph xynthesized / smartInput contract (see Xynthesized memory and smart input).
  • withXynthesizedJob(key, value) / withXynthesizedTask(key, value) / withXynthesizedExecution(key, value) — merge one key into xynthesized.job / xynthesized.task / xynthesized.execution.

Environment variables (POST steps + AI scoping): every stage reads from per-stage env vars first, then shared POST_STEP_* env vars, then the supplied fallback. Order is: config.<field> → step env → shared env → fallback.

Env var (per stage) Effect
AUDIT_MODEL, POLISH_MODEL, SYNTHESIS_MODEL, AI_SCOPING_MODEL Model override for that stage
AUDIT_TIMEOUT_MS, POLISH_TIMEOUT_MS, SYNTHESIS_TIMEOUT_MS, AI_SCOPING_TIMEOUT_MS Per-call timeout override
AUDIT_TEMPERATURE, POLISH_TEMPERATURE, SYNTHESIS_TEMPERATURE Temperature override
AUDIT_TOP_P, POLISH_TOP_P, SYNTHESIS_TOP_P Top-P override
AUDIT_MAX_OUTPUT_LENGTH, POLISH_MAX_OUTPUT_LENGTH, SYNTHESIS_MAX_OUTPUT_LENGTH Post-call char truncation override
AI_SCOPING_CONCURRENCY Max concurrent scoping calls (default 5)
AI_SCOPING_TIMEOUT_MS_PER_ITEM Per-item timeout in ms (default 30_000)
Shared env var Effect
POST_STEP_MODEL Default model for any POST step that didn't get a specific value
POST_STEP_TIMEOUT_MS Default timeout
POST_STEP_MAX_TOKENS_CAP Deprecated — rejected on llmCall; Optimixer owns caps
POST_STEP_TEMPERATURE Default temperature
POST_STEP_TOP_P Default Top-P
POST_STEP_MAX_OUTPUT_LENGTH Default char truncation

outputExpectation is intentionally NOT read from env vars (too structured).


Trace mode observability for LLM calls

When RunTaskRequest.executionMode === "trace" (or RunUtilityRequest.exec.executionMode === "trace"), @exellix/ai-tasks populates a structured LlmCallObservation for every LLM call attempted (success or failure) and surfaces them on response metadata:

result.metadata field Stage Notes
synthesizedContextLlmCalls?: LlmCallObservation[] PRE synthesis (markdown / question-driven / structured) one entry per question or branch
aiScopingLlmCalls?: LlmCallObservation[] AI scoping one entry per aiScoping[] instruction
aiScopingFailures?: AiScopingFailureRecord[] AI scoping always populated when any item failed (regardless of trace mode) — previously these were silently dropped
postSteps.audit.llmCalls?: LlmCallObservation[] POST audit (evaluator + synthesis-merge) per-cycle entries
postSteps.polish.llmCalls?: LlmCallObservation[] POST polish per-pass entries
intermediateSteps[].observation?: LlmCallObservation any failed POST step attached to the failure step so consumers can read the request/response context inline

LlmCallObservation is a discriminated union over source: "xynthesis" | "ai-skills":

type LlmCallObservation =
  | {
      source: "xynthesis"; stage: LlmCallStage; stepId?: string;
      request: LlmCallRequestSnapshot;
      summary?: InvokeAttemptSummary;  // from executeXynthesisAction on success / typed errors on failure
      debugTrace?: XynthesisDebugTrace; // present when executionMode: "trace" was forwarded
      durationMs: number; ok: boolean; error?: { name: string; message: string };
    }
  | {
      source: "ai-skills"; stage: "main-skill"; stepId?: string;
      request: LlmCallRequestSnapshot;
      diagnostics?: RunSkillDiagnostics; // from runSkill in trace mode
      durationMs: number; ok: boolean; error?: { name: string; message: string };
    };

InvokeAttemptSummary (xynthesis ≥ 4.5) carries { jobId, taskId, modelRequested, modelResolved, maxTokensFromCaller, maxTokensEffective, modelUsedFromProvider, usage, routing, costUsd, costStatus, traceKind, executionMetadata }. USD cost is authoritative from @x12i/funcx — xynthesis does not calculate pricing locally.

RunSkillDiagnostics (ai-skills ≥ 6.4) carries { trace: { step, timing, routing, usage, costUsd, costStatus, modelUsed, attempts[], rawProviderPayload?, invokeRequest? }, ... }. USD cost is authoritative from @x12i/ai-gateway ≥10.1 — ai-skills normalizes gateway metadata only (mapGatewayInvokeToTrace, traceToDiagnostics re-exported from @exellix/ai-tasks). Gateway costStatus: "priced" is represented as costUsd; "unpriced" surfaces as costStatus: "unpriced". For non-trace MAIN failures, read (error as Error & { metadata?: GatewayInvokeRejectionMetadata }).metadata (re-exported from @exellix/ai-tasks) — do not scrape error strings.

Typed errors (instanceof-friendly)

Failures from xynthesis call sites are returned as upstream typed errors that already carry rich context — @exellix/ai-tasks passes them through unchanged so callers can instanceof them:

Error class Carries Where
XynthesisInvokeError invokeSummary, cause xynthesis invoke failure (PRE / POST / scoping / utility)
XynthesisResponseParseError stage, invokeSummary, responseLength, responsePreview, actionType?, cause xynthesis parse failure
SkillExecutionTraceError RunSkillDiagnostics (incl. trace.invokeRequest echo) MAIN skill failure in trace mode
LlmCallContextError observation: LlmCallObservation, cause thin wrapper for the few sites where the upstream still throws untyped (raw setTimeout timeouts, MAIN skill outside trace mode, AIGateway repair fallback)

All of these are re-exported from @exellix/ai-tasks:

import {
  XynthesisInvokeError,
  XynthesisResponseParseError,
  SkillExecutionTraceError,
  LlmCallContextError,
  type LlmCallObservation,
  type LlmCallConfig,
  type InvokeAttemptSummary,
  type RunSkillDiagnostics,
  resolveOutputExpectation,
  ACTION_OUTPUT_DEFAULTS,
  mapGatewayInvokeToTrace,
  traceToDiagnostics,
  buildInvokeAttemptSummary,
  getModelCapabilities,
} from "@exellix/ai-tasks";
Worked example
import { runTask, type LlmCallConfig } from "@exellix/ai-tasks";

const llmCall: LlmCallConfig = {
  model: "gpt-5-mini",
  outputExpectation: { size: { kind: "absolute", maxWords: 600 }, density: "default" },
  temperature: 0.4,
  reasoningEffort: "medium",
};

const result = await runTask({
  skillKey: "tasks/security-risk-summary",
  agentId: "agent-1",
  jobTypeId: "security-job",
  taskTypeId: "security-task",
  executionStrategies: [],
  modelConfig: {
    preActionModel: "cheap/default",
    postActionModel: "cheap/default",
    skillModel: "openai/gpt-5",
  },
  input: { assetId: "a-123" },
  executionMode: "trace",
  llmCall,
  aiScopingOptions: { llmCall: { model: "cheap/default", outputExpectation: { size: { kind: "absolute", maxWords: 200 }, density: "concise" } } },
  // executionPipeline: [{ phase: "pre", type: "synthesized-context", config: { llmCall } }, { phase: "main", type: "direct" }],
});

const m = (result as any).metadata ?? {};
console.log("PRE synth calls:",   m.synthesizedContextLlmCalls?.length ?? 0);
console.log("AI scoping calls:",  m.aiScopingLlmCalls?.length ?? 0);
console.log("AI scoping fails:",  m.aiScopingFailures?.length ?? 0);
console.log("audit calls:",       m.postSteps?.audit?.llmCalls?.length ?? 0);
console.log("polish calls:",      m.postSteps?.polish?.llmCalls?.length ?? 0);

Graph Execution Support

Overview

@exellix/ai-tasks supports optional graph execution context fields that enable smarter activity tracking and identity for nodes executing within graph workflows. When executing tasks in a graph (e.g., via worex-graphs), you can provide additional context fields to improve activity tracking and debugging.

The client automatically passes these fields through to @exellix/ai-skills, which then passes them to @x12i/ai-gateway. The gateway maps them to the activity identity structure according to @x12i/activix-tracking v3.5.0+ mapping rules.

Optional Fields
graphId (optional)

Type: string
Description: Identifier for the graph execution context

Used when executing tasks within a graph to track which graph contains the task. This enables:

  • Activity grouping and filtering by graph
  • Better activity identity for graph-based workflows
  • Enhanced debugging and traceability

Example:

const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'graph-456',  // Graph identifier
  nodeId: 'node-789',     // Node identifier
});

Use Cases:

  • Graph workflow execution tracking
  • Multi-graph job orchestration
  • Graph-level activity aggregation
nodeId (optional)

Type: string
Description: Identifier for the specific node being executed

Used when executing tasks within a graph to track which specific node is being executed. When provided along with graphId, enables:

  • Precise activity identity for graph nodes
  • Node-level activity tracking and debugging
  • Better correlation between graph structure and executed activities

Example:

const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',  // Graph identifier
  nodeId: 'extract-emails-node',        // Node identifier
});

Use Cases:

  • Individual node execution tracking
  • Node-level activity analysis
  • Graph node debugging
prevNodeId (optional)

Type: string
Description: Identifier for the previous node in the graph execution flow

Used when executing tasks within a graph to track which node was executed before the current one. This enables:

  • Graph flow tracking and debugging
  • Understanding execution order in graph workflows
  • Better activity correlation between consecutive nodes

Example:

const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',
  nodeId: 'extract-emails-node',
  prevNodeId: 'validate-input-node',    // Previous node identifier
});

Use Cases:

  • Graph execution flow tracking
  • Understanding node execution sequence
  • Debugging graph workflow issues
coreSkillId (optional)

Type: string
Description: Alternative node identifier for graph execution

Used as an alternative to nodeId when executing tasks within a graph. This field is automatically mapped to identity.nodeId in activity tracking, with priority over nodeId but below skillId (from the underlying skill request).

Example:

const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',
  coreSkillId: 'q0',  // Alternative node identifier
});

Use Cases:

  • Alternative node identification (common in worex-graphs integration)
  • Node-level tracking when using coreSkillId naming convention
Field Mapping

The client automatically passes graph/node fields through @exellix/ai-skills to the underlying @x12i/ai-gateway, which maps them to the activity identity structure. The gateway uses the following priority rules:

Graph ID Mapping
  1. masterSkillId (primary) → identity.graphId
  2. graphId (fallback) → identity.graphId
Node ID Mapping
  1. skillId (primary, from underlying skill request) → identity.nodeId
  2. coreSkillId (alternative) → identity.nodeId
  3. nodeId (fallback) → identity.nodeId
Previous Node ID Mapping
  1. prevNodeIdidentity.prevNodeId (automatically mapped for graph flow tracking)
Preserved Fields
  • masterSkillIdidentity.masterSkillId (preserved for backward compatibility)
  • masterSkillActivityIdidentity.masterSkillActivityId (preserved for skill hierarchies)

This enables:

  • Smarter Identity: Activity records include graph and node context automatically
  • Better Grouping: Filter and aggregate activities by graph or node
  • Enhanced Debugging: Trace activities back to specific graph nodes
  • Improved Analytics: Analyze graph execution patterns and node performance
  • Flexible Integration: Support for multiple field naming conventions
Complete Field Mapping Guide
@exellix/ai-tasks Field @exellix/ai-skills Field @x12i/ai-gateway Field Identity Field Mapping Priority
masterSkillId masterSkillId masterSkillId (AIRequest) identity.graphId Priority 1 (primary)
graphId graphId graphId identity.graphId Priority 2 (fallback)
skillId (via skills) skillId (AIRequest) skillId (AIRequest) identity.nodeId Priority 1 (primary)
coreSkillId coreSkillId coreSkillId identity.nodeId Priority 2 (alternative)
nodeId nodeId nodeId identity.nodeId Priority 3 (fallback)
skillKey skillKey instructions - Direct mapping
jobId jobId jobId identity.jobId Direct mapping
agentId agentId agentId identity.agentId Direct mapping
masterSkillActivityId masterSkillActivityId masterSkillActivityId (AIRequest) identity.masterSkillActivityId Preserved

Note: masterSkillId is a legacy field that is still supported for backward compatibility. The new graphId and nodeId fields provide a cleaner API for graph-based workflows.

Usage Examples
Basic Graph Node Execution
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";

const tasks = new WorexClientTasks(skills, executor);

// Execute a task within a graph
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',
  nodeId: 'extract-emails-node',
});
Graph Execution with Legacy Fields
// Using legacy masterSkillId (still supported)
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  masterSkillId: 'workflow-data-processing',  // Maps to identity.graphId
  coreSkillId: 'extract-emails-node',          // Maps to identity.nodeId
});
Graph Execution with coreSkillId
// Using coreSkillId (common in worex-graphs)
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',
  coreSkillId: 'q0',  // Alternative node identifier
});
Graph Execution Without Node ID
// Graph-level execution (no specific node)
const result = await tasks.runTask({
  skillKey: 'tasks/process-data',
  executionType: ExecutionType.DIRECT,
  input: { data: '...' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',
  // nodeId not provided - still works
});
Non-Graph Execution (Backward Compatible)
// Standard execution (no graph context)
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  // graphId and nodeId not provided - works as before
});
Using TaskRequestBuilder with Graph Context
import { TaskRequestBuilder } from "@exellix/ai-tasks";

const builder = new TaskRequestBuilder();
const request = builder
  .withSkillKey('tasks/security-risk-summary')
  .withInput({ assetId: 'a-123' })
  .withGraphId('workflow-data-processing')
  .withNodeId('extract-emails-node')
  .withJobId('job-123')
  .withAiSkillsCorrelation('agent-1', 'job-type-id', 'task-type-id')
  .build();

const result = await tasks.runTask(request);
Using TaskRequestBuilder with Memory Management
import { TaskRequestBuilder } from "@exellix/ai-tasks";
import type { JobHistory, TaskHistory, ExecutionHistory } from "@exellix/ai-tasks";

const jobMemory: JobHistory = { /* previous task results */ };
const taskMemory: TaskHistory = { /* previous skill executions */ };
const executionMemory: ExecutionHistory = { /* execution context */ };

const builder = new TaskRequestBuilder();
const request = builder
  .withSkillKey('tasks/my-task')
  .withInput({ data: 'process' })
  .withJobMemory(jobMemory)
  .withTaskMemory(taskMemory)
  .withExecutionMemory(executionMemory)
  .withJobId('job-123')
  .withAiSkillsCorrelation('agent-1', 'job-type-id', 'task-type-id')
  .build();

const result = await tasks.runTask(request);
Using Convenience Methods with Graph Context
import { runTask, ExecutionType } from "@exellix/ai-tasks";

// Convenience method automatically initializes SDK (ERC mode)
const result = await runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'workflow-data-processing',
  nodeId: 'extract-emails-node',
});
Integration with Graph Runtimes

When integrating with graph execution runtimes (e.g., worex-graphs), you can pass graph/node fields directly - the client automatically passes them through to the underlying skills client and gateway.

The client automatically passes fields from graph runtimes to the gateway. You can pass fields directly without manual mapping. Production calls must also include jobTypeId, taskTypeId, and executionStrategies (often []); the snippets below omit them for brevity.

// worex-graphs sends fields directly - client handles passing through automatically
const requestFromGraph = {
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  jobId: 'job-123',
  agentId: 'agent-1',
  // jobTypeId, taskTypeId, executionStrategies: [] — required in real requests
  masterSkillId: 'graph-question-breakdown',  // Automatically mapped to identity.graphId
  coreSkillId: 'q0',                          // Automatically mapped to identity.nodeId
  masterSkillActivityId: 'job-123:q0',       // Preserved in identity
  input: { assetId: 'a-123' },
  // ... other fields
};

// Pass fields directly - client passes through automatically
const result = await tasks.runTask({
  skillKey: requestFromGraph.skillKey,
  executionType: ExecutionType.DIRECT,
  input: requestFromGraph.input,
  jobId: requestFromGraph.jobId,
  agentId: requestFromGraph.agentId,
  masterSkillId: requestFromGraph.masterSkillId,  // ✅ Automatically passed to gateway
  coreSkillId: requestFromGraph.coreSkillId,      // ✅ Automatically passed to gateway
  masterSkillActivityId: requestFromGraph.masterSkillActivityId,  // ✅ Automatically passed to gateway
  // ... other fields
});
Using New Graph Fields (Alternative)

Alternatively, you can map to the new direct fields (graphId/nodeId):

// Map to new graph fields
const result = await tasks.runTask({
  skillKey: requestFromGraph.skillKey,
  executionType: ExecutionType.DIRECT,
  input: requestFromGraph.input,
  jobId: requestFromGraph.jobId,
  agentId: requestFromGraph.agentId,
  graphId: requestFromGraph.masterSkillId,  // Direct graph identifier
  nodeId: requestFromGraph.coreSkillId,     // Direct node identifier
  // ... other fields
});
Xynthesized memory and smartInput from graph-engine

Graph runtimes can pass xynthesized (job, task, and execution buckets) and smartInput (Rendrix paths or legacy paths: string[]) on the same runTask request. ai-tasks normalizes legacy string paths, forwards smartInput / smartInputRenderOptions to local ctx, PRE synthesis, MAIN runSkill, and merged template variables; it does not apply graph output mapping or merge patches into a global store—that is the engine’s job after reading response.xynthesizedPatch (job, task, and/or execution slices) and any response.smartInputRenderResult from downstream. See Xynthesized memory and smart input.

Activity Record Structure

When graph context is provided, activity records include the mapped fields in the identity structure:

{
  _id: ObjectId('...'),
  jobId: 'job-123',
  agentId: 'agent-1',
  taskId: 'task-456',
  taskTypeId: 'abc123...',
  identity: {
    jobId: 'job-123',
    agentId: 'agent-1',
    taskId: 'task-456',
    taskTypeId: 'abc123...',
    graphId: 'workflow-data-processing',        // ← Automatically mapped from masterSkillId or graphId
    nodeId: 'extract-emails-node',             // ← Automatically mapped from skillId, coreSkillId, or nodeId
    masterSkillId: 'workflow-data-processing',  // ← Preserved if provided (backward compatibility)
    masterSkillActivityId: 'job-123:node-456'  // ← Preserved if provided (skill hierarchies)
  },
  request: { /* ... */ },
  config: { /* ... */ },
  // ... other fields
}
Benefits
  1. Enhanced Activity Tracking: Activities include graph and node context for better traceability
  2. Smarter Identity: Activity identity is enriched with graph/node information
  3. Better Debugging: Easier to correlate activities with graph structure
  4. Improved Analytics: Analyze execution patterns at graph and node levels
  5. Flexible Integration: Optional fields don't break existing integrations
  6. Backward Compatible: Legacy masterSkillId field still works
Migration Guide
For Existing Integrations

These fields are optional, so existing integrations continue to work without changes:

// ✅ Existing code continues to work
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  // graphId and nodeId not required
});
Adding Graph Support

To add graph execution support, simply include the optional fields:

// ✅ Add graph context for better tracking
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  jobId: 'job-123',
  agentId: 'agent-1',
  graphId: 'my-graph-id',    // Add graph context
  nodeId: 'my-node-id',      // Add node context (optional)
});
Migrating from Legacy Fields

If you're using masterSkillId, you can optionally migrate to the new graphId field for clarity:

// Before (still works)
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  masterSkillId: 'graph-id',
  coreSkillId: 'node-id',
});

// After (cleaner API)
const result = await tasks.runTask({
  skillKey: 'tasks/security-risk-summary',
  executionType: ExecutionType.DIRECT,
  input: { assetId: 'a-123' },
  graphId: 'graph-id',  // More explicit
  nodeId: 'node-id',    // More explicit
});
TypeScript Types

All graph execution fields are properly typed in the TypeScript definitions:

interface RunTaskRequest {
  skillKey: string;
  agentId: string;       // Required by @exellix/ai-skills
  jobTypeId: string;
  taskTypeId: string;
  input: Record<string, any> | string;
  executionType: ExecutionType;
  // ... other fields ...
  graphId?: string;        // Graph execution context
  nodeId?: string;         // Node identifier
  coreSkillId?: string;    // Alternative node identifier
  masterSkillId?: string;  // Legacy graph identifier (still supported)
  // ... other fields ...
}
Notes
  • agentId / jobTypeId / taskTypeId are required on every runTask (see RUNTASK_REQUEST.md).
  • Graph/node fields (graphId, nodeId, …) remain optional for non-graph runs — provide them when executing tasks in graphs.
  • Fields are automatically passed through to the skills client and gateway, then mapped to activity identity.
  • Multiple field naming conventions supported:
    • New fields: graphId/nodeId/coreSkillId (recommended for new code)
    • Legacy fields: masterSkillId (still supported for backward compatibility)
  • Mapping priority ensures consistent identity structure regardless of field naming.
  • Best practice: Include graph/node context fields for graph task execution for maximum traceability.

API Reference

WorexClientTasks

Main class for task execution.

Constructor
constructor(
  skillsClient: WorexClientSkills,
  executor: {
    execute<TParsed = any>(
      input: any,
      onNotFound: (key: string) => Promise<void>
    ): Promise<RunTaskResponse<TParsed>>;
  }
)
Methods
runTask<TParsed>(input: RunTaskRequest): Promise<RunTaskResponse<TParsed>>

Executes a task with task-level enrichment and context generation.

const result = await tasks.runTask({
  skillKey: "tasks/security-risk-summary",
  agentId: "my-agent",
  jobTypeId: "security-risk-job",
  taskTypeId: "security-risk-task",
  // executionType is optional, defaults to ExecutionType.DIRECT
  input: { assetId: "a-123" },
  variables: { orgName: "Acme" },
  jobMemory: previousJobMemory,
  taskMemory: previousTaskMemory,
  executionMemory: previousExecutionMemory,
  jobId: "job-123"
});
Local task registry
  • registerLocalTask(skillKey: string, handler: LocalTaskHandler): void — Register a local task handler. When runTask is called with this skillKey, the handler runs instead of the normal enrichment/executor path.
  • getLocalTask(skillKey: string): LocalTaskHandler | undefined — Return the registered handler for skillKey, or undefined.
  • registerBuiltInLocalTasks(): void — Register built-in handlers (skills/node.callExport, skills/node.callExportBatch, skills/skill.local:validateInput, skills/skill.local:normalizeNarrixResult). The Narrix skill skills/skill.local:narrixRun (record/text/docs/chat) is registered via a side-effect import and is available when the package is loaded.
  • LocalTaskContext — Type: skillKey, jobMemory, taskMemory, executionMemory, variables, xynthesized, smartInput, jobId, taskId, agentId, graphId, nodeId, prevNodeId, coreSkillId, masterSkillId, masterSkillActivityId.
  • LocalTaskHandler — Type: (args: { input: any; ctx: LocalTaskContext }) => Promise<any>.
TaskRequestBuilder

Builder class for constructing task requests with a fluent API.

Methods
  • withSkillKey(skillKey: string): this - Set the skill key
  • withInput(input: Record<string, any> | string): this - Set input data
  • withExecutionStrategies(invocations: ExecutionStrategyInvocation[]): this — MAIN strategy invocations (required on the built request; defaults to [] in build() when unset)
  • withExecutionPipeline(steps: ExecutionStep[]): this - Set execution pipeline (pre/main/post steps)
  • withSynthesizedContextPreStep(modelOrConfig?: string | SynthesisConfig): this - Add synthesized-context PRE step and set includeContextInPrompt: true. Pass a SynthesisConfig object to set contextSourcePolicy, webEvidence, memoryPaths, etc.
  • withAuditPostStep(config: AuditConfig): this - Add audit POST step (quality-gate loop)
  • withPolishPostStep(config: PolishConfig): this - Add polish POST step (refinement checklist)
  • withVariables(variables: Record<string, any>): this - Set contextual variables
  • withContext(context: Record<string, any> | string): this - Set context
  • withKnowledge(knowledge: Record<string, any>): this - Set knowledge data
  • withJobMemory(jobMemory: JobHistory): this - Set job memory
  • withTaskMemory(taskMemory: TaskHistory): this - Set task memory
  • withExecutionMemory(executionMemory: ExecutionHistory): this - Set execution memory
  • withJobId(jobId: string): this - Set job ID
  • withAgentId(agentId: string): this - Set agent ID
  • withJobTypeId(jobTypeId: string): this - Set job type id (required before build())
  • withTaskTypeId(taskTypeId: string): this - Set task type id (required before build())
  • withAiSkillsCorrelation(agentId: string, jobTypeId: string, taskTypeId: string): this - Set all three correlation ids (recommended)
  • withGraphId(graphId: string): this - Set graph identifier
  • withNodeId(nodeId: string): this - Set node identifier
  • withPrevNodeId(prevNodeId: string): this - Set previous node identifier
  • withModelConfig(modelConfig: RunTaskModelConfig): this - Set the required 3-slot model triplet + tuning
  • withTemplateRenderOptions(templateRenderOptions: TemplateRenderOptions): this - Per-task gateway / parser template overrides (v4)
  • withTemplateTokens(templateTokens: GatewayTemplateTokens): this - Per-task templateTokens overlay
  • withTimeout(timeoutMs: number): this - Set timeout
  • withXynthesized(memory: XynthesizedMemory): this — optional xynthesized bucket (job / task / execution records)
  • withSmartInput(config: RunTaskSmartInput): this — passthrough smartInput (Rendrix paths or legacy string[])
  • withSmartInputPaths(paths: string[]): this — builds smartInput: { paths: paths.map(p => ({ title: p, path: p })) }
  • withSmartInputRenderOptions(opts: SmartInputRenderOptions): this — sets smartInputRenderOptions on the request (gateway/Rendrix merge)
  • withXynthesizedJob(key: string, value: unknown): this / withXynthesizedTask(key: string, value: unknown): this / withXynthesizedExecution(key: string, value: unknown): this — merge a single key into xynthesized.job / xynthesized.task / xynthesized.execution
  • build(): RunTaskRequest - Build the final request (executionStrategies defaults to [] when unset)
Example
import { TaskRequestBuilder } from "@exellix/ai-tasks";

const request = new TaskRequestBuilder()
  .withSkillKey("tasks/my-task")
  .withInput({ data: "process" })
  .withAiSkillsCorrelation("my-agent", "my-job-type", "my-task-type")
  .withExecutionStrategies([]) // [] = plain MAIN; omit only if you rely on build() defaulting strategies to []
  .withModelConfig({
    preActionModel: "cheap/default",
    postActionModel: "cheap/default",
    skillModel: "gpt-5",
    temperature: 0.7,
  })
  .withJobMemory(jobMemory)
  .withTaskMemory(taskMemory)
  .withExecutionMemory(executionMemory)
  .withJobId("job-123")
  .build();

const result = await tasks.runTask(request);

Types

RunTaskRequest
interface RunTaskRequest {
  skillKey: string;                    // "tasks/security-risk-summary"
  agentId: string;                     // Required — @exellix/ai-skills correlation
  jobTypeId: string;                   // Required — catalog / telemetry job classification
  taskTypeId: string;                  // Required — catalog / telemetry task classification
  /** Required: MAIN strategy invocations; use [] for plain gateway MAIN (see BREAKING-CHANGES / RUNTASK_REQUEST). */
  executionStrategies: ExecutionStrategyInvocation[];
  /** Optional guarded metadata rows from catalogId `execution-strategy`; code validation remains authoritative. */
  executionStrategyCatalogItems?: TaskStrategyItemData[];
  input: Record<string, any> | string; // Task payload (canonical user/host data)
  executionType?: ExecutionType | string;  // Optional; DIRECT default when using plain MAIN; narrix-then-direct when configured
  narrixInput?: NarrixRunInput | { $path: string };  // Required when narrixMode infers handler without preprocessor narrix
  narrixScope?: NarrixScope;            // When set with narrix handler path, filters which signals/stories go to task memory
  /** Task-level NARRIX pre-processor: datasetId, attachToField (CNI attachment only; see [NARRIX task-level pre-processor](#narrix-task-level-pre-processor)). */
  narrix?: NarrixPreProcessorConfig;
  includeContextInPrompt?: boolean;    // Opt-in: when true, task-scoped context is added to the prompt; default is false (no context)

  /** Optional: pre / main / post steps. When set (non-empty), runs PRE (e.g. synthesized-context), one MAIN, then POST. See [Execution pipeline](#execution-pipeline-and-synthesized-context-pre-step). */
  executionPipeline?: ExecutionStep[];

  /** executionMode: "trace" — ordered debugTrace + richer LLM observability (see [Trace mode](#trace-mode-authoritative-ordered-debug-trace)). */
  executionMode?: "default" | "trace";

  /** Merged into template variables before explicit variables (MAIN path). See mergeSkillTemplateVariables. */
  jobContext?: Record<string, unknown>;

  /** Job-, task-, and execution-scoped synthesized buckets for graph execution (distinct from raw memories). */
  xynthesized?: XynthesizedMemory;

  /** Rendrix smart-input paths or legacy `paths: string[]` (normalized before MAIN); root allows only `paths`. */
  smartInput?: RunTaskSmartInput;
  /** Optional Rendrix options for the `{{smartInput}}` macro (merged on gateway like `RunSkillRequest`). */
  smartInputRenderOptions?: SmartInputRenderOptions;

  // Optional fields
  variables?: Record<string, any>;      // Contextual variables
  context?: Record<string, any> | string;
  knowledge?: Record<string, any>;
  jobMemory?: JobHistory;               // History of previous task results
  taskMemory?: TaskHistory;              // History of skills executed
  executionMemory?: ExecutionHistory;    // History of execution context (execution-level memory)
  modelConfig: RunTaskModelConfig;      // Required 3-slot triplet + optional tuning
  jobId?: string;

  // Graph execution context (optional)
  graphId?: string;                    // Graph identifier for activity tracking
  nodeId?: string;                      // Node identifier for activity tracking
  coreSkillId?: string;                 // Alternative node identifier (maps to identity.nodeId)

  // Legacy fields (still supported for backward compatibility)
  masterSkillId?: string;               // Legacy graph identifier (maps to identity.graphId)
  masterSkillActivityId?: string;       // Preserved for skill hierarchies

  timeoutMs?: number;

  /** Per-invoke template parser options (merged on gateway defaults). See [Gateway template rendering (v4)](#gateway-template-rendering-v4). */
  templateRenderOptions?: TemplateRenderOptions;
  /** Highest-priority template token overlay for this invoke. */
  templateTokens?: GatewayTemplateTokens;
}

NarrixPreProcessorConfig is exported from this package. It configures the CNI attachment only and carries no web-scope fields — web scoping is a standalone webScope PRE unit.

The closed JSON schema for hosts and codegen is documenations/schemas/v1/run-task-request.json (includes xynthesized and smartInput). Narrative contract: RUNTASK_REQUEST.md. For smart input, the TypeScript types exported from this package (RunTaskSmartInput, SmartInputConfig) are authoritative; the JSON schema / RUNTASK narrative may still emphasize string paths only until updated to Rendrix { title, path } entries.

TemplateRenderOptions and GatewayTemplateTokens are re-exported from this package (see Gateway template rendering (v4)).

RunTaskResponse<TParsed>

ai-tasks extends the skills response with optional identity, xynthesizedPatch (job, task, and/or execution writes from PRE synthesis when configured), intermediateSteps, debugTrace (when executionMode: "trace"), and observability-related metadata extensions. Underlying shape:

type RunTaskResponse<TParsed = any> = RunSkillResponse<TParsed> & {
  /** When downstream returns smart-input diagnostics markdown + resolved paths. */
  smartInputRenderResult?: SmartInputRenderResult;
  identity?: Record<string, unknown>;
  xynthesizedPatch?: XynthesizedMemory;
  intermediateSteps?: IntermediateStep[];
  debugTrace?: { tasks: DebugTraceTask[] };
  aiTasksObservability?: AiTasksObservabilityDigest; // usually attached by orchestrators, not by default
};

interface RunSkillResponse<TParsed = any> {
  skillKey: string;
  rawText: string;
  flexMd: {
    frame: string;
    payloads: Record<string, string>;
  };
  parsed: TParsed;
  metadata: {
    instructionVersion?: string;
    activityId?: string;
    durationMs?: number;    // Present for local tasks (execution tracing)
    localSkillKey?: string;   // Present for local tasks (e.g. "skills/node.callExportBatch")
    narrix?: {              // Present when narrix-then-direct ran and Narrix succeeded (or failed; see parsed.phase)
      entity: { entityKind: string; entityKey: string };
      signals: unknown[];
      stories: unknown[];
      meta: Record<string, unknown>;
      durationMs?: number;
    };
  };
}

Task responses may include an optional top-level intermediateSteps (array of IntermediateStep) for combined/multi-step tasks; see Intermediate steps (multi-step tasks).

IntermediateStep, IntermediateSteps, RunTaskResponseWithSteps

For multi-step tasks, use the extended response type and step shape:

import type { IntermediateStep, IntermediateSteps, RunTaskResponseWithSteps } from "@exellix/ai-tasks";

interface IntermediateStep {
  step: number;        // 1-based order
  id: string;          // e.g. "to-cni", "engine-enrich"
  ok: boolean;
  summary?: string;
  error?: string;
  inputSummary?: Record<string, unknown> | string;
  outputExcerpt?: Record<string, unknown>;
}

type IntermediateSteps = IntermediateStep[];

interface RunTaskResponseWithSteps<TParsed = any> extends RunSkillResponse<TParsed> {
  intermediateSteps?: IntermediateSteps;
}

See documenations/intermediate-steps.md for full documentation and examples.

ExecutionPhase, ExecutionStep, SynthesisConfig, ContextSourcePolicy, WebEvidenceConfig, structured synthesis types

Used with executionPipeline and the synthesized-context PRE step. Import from @exellix/ai-tasks (and use SYNTHESIZED_CONTEXT for the PRE step type string):

import type {
  ExecutionPhase,
  ExecutionStep,
  ExecutionStrategyInvocation,
  SynthesisConfig,
  ContextSourcePolicy,
  WebEvidenceConfig,
  SynthesisInput,
  SynthesizedPromptPayload,
  SynthesizedContext,
  SynthesizedItem,
  ContextSynthesizer,
  XynthesizedMemory,
  SmartInputConfig,
  SmartInputRenderOptions,
  SmartInputRenderResult,
  RunTaskSmartInput,
} from "@exellix/ai-tasks";
import { SYNTHESIZED_CONTEXT } from "@exellix/ai-tasks";

// ExecutionPhase = "pre" | "main" | "post"
// ExecutionStep: { phase, type: SYNTHESIZED_CONTEXT | "direct" | "audit" | "polish", config? }
// SynthesisConfig: … synthesisOutputFormat?: "markdown" | "structured", questionPath?, getQuestion?, structuredMaxItemsPerSide?, …
// ContextSourcePolicy: "auto" | "narrix-web" | "narrix-web-memory" | "memory-web" | "memory-only" | "narrix-only" | "narrix+memory"
// WebEvidenceConfig: preferCleanContent?, maxSources?, dedupeByUrl?, maxTotalChars?

Semantics and the full policy table: Execution pipeline and synthesized-context.

ExecutionType
enum ExecutionType {
  DIRECT = "direct"
}

The package also exports the constant NARRIX_THEN_DIRECT = "narrix-then-direct" for use with executionType when running Narrix first, then the executor. See Narrix then execute.

NarrixScope

Optional filter for which signals and stories from Narrix are written to task memory (used with executionType: "narrix-then-direct"):

interface NarrixScope {
  includeSignals?: string[];   // Signal codes to keep (allowlist)
  excludeSignals?: string[];   // Signal codes to drop (blocklist; ignored when includeSignals is set)
  includeStories?: string[];   // narrativeTypeIds to keep (allowlist)
  excludeStories?: string[];   // narrativeTypeIds to drop (blocklist; ignored when includeStories is set)
}
NarrixPreProcessorConfig

Exported type for RunTaskRequest.narrix. Includes datasetId, optional attachToField, and reserved fields. It carries no web-scope fields. See NARRIX task-level pre-processor and documenations/web-scoping-in-ai-tasks.md.

import type { NarrixPreProcessorConfig } from "@exellix/ai-tasks";
RunTaskModelConfig

Required on every runTask() — three slots plus optional tuning (no maxTokens, no legacy model / xynthesisModel):

import type { RunTaskModelConfig } from "@exellix/ai-tasks";

interface RunTaskModelConfig {
  preActionModel: string;   // ai-profiles alias (xynthesis PRE)
  skillModel: string;       // alias or concrete id (ai-skills MAIN)
  postActionModel: string;  // ai-profiles alias (xynthesis POST)
  temperature?: number;
  topP?: number;
  reasoningEffort?: OptimixerReasoningEffort;
  frequencyPenalty?: number;
  presencePenalty?: number;
}

Example:

const modelConfig: RunTaskModelConfig = {
  preActionModel: "cheap/default",
  postActionModel: "cheap/default",
  skillModel: "cyber",
  temperature: 0.7,
  topP: 0.9,
  reasoningEffort: "medium",
};

const result = await runTask({
  skillKey: "tasks/analysis",
  executionType: ExecutionType.DIRECT,
  input: { data: "analyze" },
  modelConfig
});

Note: Root RunTaskRequest.modelConfig uses canonical slots (RunTaskModelConfig: preActionModel, skillModel, postActionModel). Profile/choice keys are validated at runTask entry and resolved at each xynthesis / ai-skills call boundary via @x12i/ai-profiles v2. See Model profile aliases and MODEL-CONFIGURATION.md.

JobHistory, TaskHistory, and ExecutionHistory

History objects for managing execution context across workflows:

import type { JobHistory, TaskHistory, ExecutionHistory } from "@exellix/ai-tasks";

// JobHistory: Contains history of all previous task results in the job
const jobMemory: JobHistory = {
  // Previous task results
};

// TaskHistory: Contains history of skills executed up to this task
const taskMemory: TaskHistory = {
  // Previous skill executions
};

// ExecutionHistory: Contains history of execution context (execution-level memory)
const executionMemory: ExecutionHistory = {
  // Execution state, intermediate results, execution-level variables
  previousAttempts: 2,
  lastError: "timeout",
  intermediateResults: { step1: "completed" }
};
  • JobHistory and TaskHistory are re-exported from @x12i/execution-memory-manager for convenience
  • ExecutionHistory is defined in this package as Record<string, any> for execution-level memory

See HISTORY-OBJECTS.md for detailed documentation on history objects and HISTORY-OBJECTS-DOWNSTREAM.md for information on how they flow downstream.


Content Registry Structure (v2.0.2+)

To ensure robust resolution with @x12i/ai-gateway v7.0.0+, it is recommended to provide both .instructions and .prompt files for each skill in your .metadata/skills directory.

  • {skillId}.instructions: Contains the system instructions (persona, constraints, output format).
  • {skillId}.prompt: Contains the user prompt template (e.g., {{input}}). With gateway template v4, {{input}} is supplied from merged working memory (see Gateway template rendering (v4)); use templateRenderOptions.subPathSearch when templates need paths like {{execution.summary}} backed by nested task input.

See SKILL-CONTENT-GUIDE.md for a detailed migration and implementation guide.


Environment Variables

See .env.example for complete configuration. Key variables:

Used by ai-tasks:

  • MONGO_LOGS_DB - [OPTIONAL] Default database for activity logs (defaults to logs-db), passed as bindingDefaultsDb to the skills client.

POST step (shared):

  • POST_STEP_MODEL - [OPTIONAL] Fallback model for all POST step LLM calls when no step-specific env is set.
  • POST_STEP_TIMEOUT_MS - [OPTIONAL] Fallback timeout in ms for POST step LLM calls (default 30000).

Audit POST step:

  • AUDIT_MODEL - [OPTIONAL] Model for audit evaluator (→ POST_STEP_MODEL → gateway default).
  • AUDIT_SYNTHESIS_MODEL - [OPTIONAL] Model for synthesis merge when selectionStrategy: "synthesis" (→ POST_STEP_MODEL → MAIN step model).
  • AUDIT_TIMEOUT_MS - [OPTIONAL] Timeout per audit LLM call (→ POST_STEP_TIMEOUT_MS30000).
  • AUDIT_TEMPLATES_PATH - [OPTIONAL] Base path for audit templates (default <project>/templates/post-steps/audit).

Polish POST step:

  • POLISH_MODEL - [OPTIONAL] Model for polish LLM (→ POST_STEP_MODEL → gateway default).
  • POLISH_TIMEOUT_MS - [OPTIONAL] Timeout per polish call (→ POST_STEP_TIMEOUT_MS30000).
  • POLISH_TEMPLATES_PATH - [OPTIONAL] Base path for polish templates (default <project>/templates/post-steps/polish).

Required for content and providers (validated by ai-skills):

  • GITHUB_TOKEN - [REQUIRED] GitHub token for content registry access
  • GITHUB_REPO_URL - [REQUIRED] Content registry repository URL
  • OPENAI_API_KEY or GROK_API_KEY - [REQUIRED] AI provider key (or OPEN_ROUTER_KEY / OPENROUTER_API_KEY if ai-skills accepts OpenRouter-only)

Downstream (used by ai-skills → ai-gateway → ai-provider-router, ai-activities-tracking): ai-tasks does not read these; they affect LLM and activity tracking when using this package.

  • OPEN_ROUTER_KEY or OPENROUTER_API_KEY - [OPTIONAL] Use OpenRouter for LLM calls; no provider registration needed when set.
  • MONGO_LOGS_COLLECTION - [OPTIONAL] Activities collection (default in ai-activities-tracking: cognitive-activities).
  • MONGO_BAD_REQUESTS_COLLECTION - [OPTIONAL] Bad-requests collection (default: ai-bad-requests).

Synthesis (synthesized-context PRE step):

  • SYNTHESIS_MODEL - [OPTIONAL] Default model for synthesis (e.g. gpt-5-nano).
  • SYNTHESIS_TIMEOUT_MS - [OPTIONAL] Timeout for synthesis call (default 30000).
  • SYNTHESIS_MAX_OUTPUT_LENGTH - [OPTIONAL] Max length of synthesis output (characters).
  • SYNTHESIS_TEMPLATES_PATH - [OPTIONAL] Base path for templates/synthesis/system.md and user.txt (default: project root).

Other:

  • MONGODB_URI - [OPTIONAL] MongoDB for context enrichment
  • CONTENT_REGISTRY_LOCAL_PATH - [OPTIONAL] Local registry path
  • USE_NARRIX_INGEST - [OPTIONAL] Set to "1" to enable the Narrix pipeline for skills/skill.local:narrixRun (record/text/docs/chat). When unset, the handler returns NARRIX_DISABLED.
  • NARRIX_DEBUG - [OPTIONAL] Set to "1" to log datasetId, medium, packId, entityKind, signal codes, and narrativeTypeIds when the Narrix handler runs.
  • TAVILY_API_KEY - [OPTIONAL] Required when using webScope PRE units (via @exellix/web-scope@x12i/web-queries + @x12i/search-adapter / Tavily). When missing or invalid, executionMemory.webContext reflects ok: false; the task still completes.
  • MONGO_URI, EXELLIX_NAME, MEMORIX_KNOWLEDGE_DB - [OPTIONAL] Memorix TTL caching and synthesized-knowledge persistence for web scope (via @x12i/web-scoper-memorix).
  • WEB_CONTEXT_MARKDOWN_MAX_CHARS - [OPTIONAL] Positive integer: max Unicode length of the Web sources markdown block appended to task context / synthesis source material (tail truncated with a marker). Omit for no downstream cap (upstream maxTotalWebContextChars / maxSnippetCharsPerSource still apply in the scoper).

For a concise breakdown of what ai-tasks uses vs downstream, see documenations/downstream-environment.md.


Examples

Basic Usage
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";

const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);

const result = await tasks.runTask({
  skillKey: "tasks/security-risk-summary",
  executionType: ExecutionType.DIRECT,
  input: { assetId: "a-123" },
  variables: { orgName: "MyOrganization" },
  modelConfig: {
    preActionModel: "cheap/default",
    postActionModel: "cheap/default",
    skillModel: "openai/gpt-5",
    temperature: 0.7
  }
});

console.log(result.flexMd.payloads);
With Memory Management
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
import type { JobHistory, TaskHistory, ExecutionHistory } from "@exellix/ai-tasks";

const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);

const jobMemory: JobHistory = {
  // Previous task results
};

const taskMemory: TaskHistory = {
  // Previous skill executions
};

const executionMemory: ExecutionHistory = {
  previousAttempts: 1,
  intermediateResults: { analysis: "in-progress" }
};

const result = await tasks.runTask({
  skillKey: "tasks/professional-decision",
  executionType: ExecutionType.DIRECT,
  input: { scenario: "Should we migrate to cloud?" },
  variables: { orgName: "EnterpriseCorp" },
  jobMemory,
  taskMemory,
  executionMemory,
  jobId: "job-456"
});
With Model Configuration
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";

const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);

// Configure model per-request (@exellix/ai-skills correlation ids required)
const result = await tasks.runTask({
  skillKey: "tasks/analysis",
  agentId: "agent-1",
  jobTypeId: "analysis-job",
  taskTypeId: "analysis-task",
  executionStrategies: [],
  executionType: ExecutionType.DIRECT,
  input: { data: "analyze this" },
  modelConfig: {
    preActionModel: "cheap/default",
    postActionModel: "cheap/default",
    skillModel: "gpt-5",
    temperature: 0.7,
    topP: 0.9,
  },
});

// Or using the builder
import { TaskRequestBuilder } from "@exellix/ai-tasks";

const request = new TaskRequestBuilder()
  .withSkillKey("tasks/analysis")
  .withInput({ data: "analyze this" })
  .withAiSkillsCorrelation("agent-1", "analysis-job", "analysis-task")
  .withExecutionStrategies([])
  .withModelConfig({
    preActionModel: "cheap/default",
    postActionModel: "cheap/default",
    skillModel: "gpt-5",
    temperature: 0.5,
  })
  .build();

const result2 = await tasks.runTask(request);
With Graph Execution Context
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";

const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);

// Execute a task within a graph workflow
const result = await tasks.runTask({
  skillKey: "tasks/security-risk-summary",
  executionType: ExecutionType.DIRECT,
  input: { assetId: "a-123" },
  variables: { orgName: "TechCorp" },
  modelConfig: {
    model: "gpt-5",
    temperature: 0.7
  },
  jobId: "job-123",
  agentId: "agent-1",
  graphId: "workflow-data-processing",  // Graph identifier for activity tracking
  nodeId: "extract-emails-node"         // Node identifier for activity tracking
});

console.log(result.flexMd.payloads);
// Graph and node context are automatically passed through to activity tracking

Activix — task activity tracking

@x12i/activix (8.4+) provides multi-phase activity tracking: each runTask() invocation emits several phase records (local, narrix, pipeline_pre, direct, audit, polish, narrix_then_direct). All phase records share the same correlationId (runtime taskId), so you can group them into one logical task run.

Operational checklist: documenations/activix.md · Full reference: docs/activix.md

How wiring works (Activix 8.4+)

ai-tasks always calls Activix lifecycle APIs on AI activity phases when Mongo is configured. Activix decides whether rows are persisted — do not branch in application code on a local write toggle.

runTask() phase  →  startRecord / completeRecord / failRecord
                              ↓
              activix-collections legend state
              track → Mongo write    |    off → same API, skip write
Layer Owns
ai-tasks Construct Activix when MONGO_URI or MONGO_LOGS_URI is set; call lifecycle APIs per phase; pass runContext, outer, domain metadata
Activix recordId, status, startTime, endTime, duration; read legend state and skip or perform Mongo writes
Ops Toggle persistence via activix-collections (state: track | off) or setCollectionTrackingState() — no redeploy required

Lifecycle fields (status, timestamps, primary key) are never set by ai-tasks; Activix manages them on every write.

Enable / disable

Client construction (whether phase APIs are called):

  • On by default when MONGO_URI or MONGO_LOGS_URI is set.
  • Opt out entirely: ACTIVIX_ENABLED=false — no Activix client, no API calls (zero overhead for local dev/CI).
  • ACTIVIX_ENABLED=true is no longer required to turn tracking on.

Mongo persistence (whether rows reach the database):

  • Controlled by Activix collection legend state in activix-collections (default track).
  • Set off to skip writes while keeping the same API surface: await ax.setCollectionTrackingState('task-activities', 'off').
  • Legend state is cached (default TTL 600 s); override with ACTIVIX_COLLECTION_TRACKING_STATE_TTL_SEC.

All Activix failures are non-fatal — a MongoDB outage never surfaces to the runTask() caller.

Configuration
Variable Default Description
MONGO_URI / MONGO_LOGS_URI Required to construct Activix (first set wins)
ACTIVIX_ENABLED (unset) Set to false to opt out of client construction
ACTIVIX_COLLECTION task-activities Dev/CI override only; package hard-codes task-activities in source
ACTIVIX_STALE_TTL_MS 300000 TTL (ms) before a stuck started record is marked timeout
ACTIVIX_COLLECTION_TRACKING_STATE_TTL_SEC 600 Legend state cache TTL (Activix 8.4+)
ACTIVIX_LOGS_LEVEL (see below) Injected Activix logger threshold (@x12i/logxer, prefix ACTIVIX). Falls back to ACTIVIX_LOG_LEVEL, then warn. Values: verbose | debug | info | warn | error | off / silent / none.
ACTIVIX_LOG_LEVEL warn Legacy alias when ACTIVIX_LOGS_LEVEL is unset
AI_TASKS_LOGS_LEVEL warn ai-tasks diagnostics threshold (warnCode + .metadata/log-diagnostics.json). Legacy alias: AI_TASKS_LOG_LEVEL.

Debug tooling (logxer 4.4.2): getAiTasksJobLogs({ jobId, level, limit }) queries in-process structured log envelopes captured during runTask(). Correlation ids are set via runAiTasksWithLogContext / patchRunTaskLogContext.

Query API: getTaskActivities(taskId) returns all phase records for one run (by correlationId).

What is recorded

Each record in the task-activities collection captures:

  • correlationId, phase — join key for grouping phase records into one runTask() invocation
  • runContext — correlation envelope: sessionId matches correlationId; taskId, skillId, graph/node ids live here (not a top-level identity blob)
  • outer — required I/O tier (input / output / metadata); minimal placeholders until completion metadata is merged
  • skillKey, executionType, jobId, agentId, graphId, nodeId — from the request
  • hasPipeline, hasNarrix, hasNarrixInput — execution path flags
  • status, startTime, endTime, durationauto-managed by Activix
  • instructionVersion, durationMs, stepCount — from the response on success (phase-dependent)
  • error / errorMessage, errorCode — on failure
Stale record cleanup

Crashed or hung tasks leave records stuck in started. Call ax.markStaleRecords() from a periodic job (e.g. every 5 minutes) in the host process to flip them to timeout.


Development Notes

  • This is a private package; we optimize for reuse and speed.
  • @exellix/ai-skills is treated as the shared helper + execution layer.
  • Naming leakage is explicitly acceptable (private monorepo + private packages).
  • Task-level enrichment always uses level: 'task' (not 'skill').
  • The executor must be a "no-enrichment execute primitive" to avoid double-enrichment.

Troubleshooting

"Content not found" Error
  • Verify skill key format: "tasks/security-risk-summary" (not "security-risk-summary")
  • Check content registry contains the task
  • Verify GITHUB_TOKEN and GITHUB_REPO_URL are set correctly
"Repository not accessible"
  • Verify GITHUB_TOKEN has repo scope for private repos
  • Check GITHUB_REPO_URL points to an existing repository
"Environment validation failed"
  • Ensure at least one AI provider key is set (OPENAI_API_KEY or GROK_API_KEY)
  • Verify GITHUB_TOKEN and GITHUB_REPO_URL are set
  • Check .env file is loaded (if using dotenv)

Development

# Install dependencies (requires @exellix scope registry/auth for narrix packages)
npm install

# Build
npm run build

# Run tests (all narrix tests under test/narrix/; requires build first)
npm test

# Run full Narrix integration tests (record, text, docs, chat + 14 record scenarios)
npm run test:with-narrix-ingest
# Bash/macOS/Linux alternative: USE_NARRIX_INGEST=1 npm test
# PowerShell: $env:USE_NARRIX_INGEST='1'; npm test

# Run real E2E for intermediateSteps (real executor + real skill test-intermediate-steps; requires LLM + gateway env like other E2Es)
# Also requires Catalox/Firebase: FIREBASE_PROJECT_ID and GOOGLE_SERVICE_ACCOUNT_BASE64 (tests load repo-root `.env` first; skip gate requires both to be set).
npm run test:e2e:intermediateSteps
# Bash alternative: RUN_INTERMEDIATE_STEPS_E2E=1 npm test

# Run real E2E for synthesized-context (requires a provider API key in .env)
# Keys recognized by the gate: OPENROUTER_API_KEY, OPENAI_API_KEY, OPENAI_KEY, OPEN_ROUTER_KEY, GROK_API_KEY.
# Optional: SYNTHESIS_MODEL, LLM_MODEL_STRONG, LLM_MODEL_NORMAL, LLM_MODEL, POST_STEP_MODEL (else gateway default).
# Runs three opt-in tests in test/synthesis/e2e-synthesis-real.test.ts:
#   (1) default markdown synthesis PRE step + main skill,
#   (2) runStructuredSynthesisGatewayCall (real gateway JSON → parse → buildSynthesizedContextMarkdown),
#   (3) synthesisOutputFormat: "structured" PRE + main skill (memory-only source + input.question).
# The synthesis model must follow JSON instructions for (2) and (3). Uses ~3 synthesis + ~2 main LLM calls.
RUN_SYNTHESIS_E2E=1 npm test
# Or: npm run test:e2e:synthesis
#
# Xynthesis + Mongo / Activix + sidekick hardening: documenations/xynthesis-upstream-fixes-checklist.md

# Development mode
npm run dev

Narrix tests: When USE_NARRIX_INGEST is not set, only the "feature flag off" test runs; the rest are skipped. Use npm run test:with-narrix-ingest (or set the env var per shell above) to run contract and integration tests; live catalog tests also need .archive/packages/… (see BREAKING-CHANGES-NARRIX-V5.md). Optional: NARRIX_DEBUG=1 for verbose handler logging.

NARRIX task-level pre-processor tests: test/narrix/preProcessorAdapter.integration.test.ts (adapter produces record from jobMemory/executionMemory/input shapes), test/narrix/buildNarrixAttachment.test.ts (attachment from runner result with/without passes), and test/narrix/preProcessorE2E.test.ts (throw when narrix set but no record; e2e handler receives ctx.executionMemory._narrix when ingest is enabled and the narrix seed archive is present).


License

ISC

Repository

GitHub

Keywords