npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@exellix/ai-tasks

v8.0.0

Published

Task orchestration for the Exellix stack: runTask() with local handlers or LLM-backed execution, task-scoped memory/context enrichment, and executor dispatch via @exellix/ai-skills. ERC-compliant.

Readme

@exellix/ai-tasks

Private Git/npm package for executing tasks using the Woreces execution stack.

Breaking — executionStrategies (required): Every runTask request must include executionStrategies: an array of FuncX MAIN wrappers 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. Default FuncX function ids (generic envelope via run(), @x12i/funcx ≥ 3.8.2 recommended): execution/plan, execution/evaluate-result (overridable via each row’s args.functionId when the alternate implementation uses the same envelope). Planner/optimizer responses are normalized with getRunJsonResult from @x12i/funcx/functions (also re-exported from this package as unwrapFuncxRunValuegetRunJsonResult).

FuncX catalog / hosting: Those function ids must exist in your FuncX content resolver for live run() calls — see documenations/funcx-catalog-hosting-checklist.md.

Execution pipeline (optional): You can use executionPipeline (array of pre/main/post steps) instead of a single executionType. PRE steps include synthesized-context; POST steps include audit (quality-gate loop) and polish (refinement checklist). See BREAKING-CHANGES.md for migration. When executionPipeline is omitted, existing executionType behavior is unchanged.

This package implements the canonical runTask() flow (every request carries executionStrategies — use [] for plain MAIN):

  1. if request.narrix is set (task-level pre-processor) → resolve raw record from executionMemory/jobMemory/input, run NARRIX (to-CNI + engine), build _narrix attachment (scoping/discovery/meta), inject into executionMemory and jobMemory; if narrix.enableWebScope === true, also run @exellix/narrix-web-scoper and set executionMemory.webContext (failures are non-fatal); then continue with the same request so the task sees enriched context
  2. if a local task handler is registered for skillKey → run it and return (no enrichment, no LLM); ctx includes optional xynthesized / smartInput when the caller supplied them
  3. if executionType === 'narrix-then-direct' and narrixInput is provided → resolve narrix input, run Narrix, append output to taskMemory.narrix, then run the standard DIRECT path (enrich → context → executor); response includes metadata.narrix
  4. inject bindingDefaultsDb for Xronox routing (defaulting to MONGO_LOGS_DB or logs-db)
  5. enrich job/task memory with task-scoped scoping (using skillKey)
  6. generate task-scoped context markdown only when requested (includeContextInPrompt === true; default is no context). When NARRIX is in play, context is the "## Scoping and discovery" section from executionMemory._narrix (buildNarrixPreProcessorContextMarkdown), plus—when web scoping returned a hit—a ## Web sources (primary evidence) block built from executionMemory.webContext (by default cleaned text is preferred over raw HTML: providerContent / content before providerRawContent / rawContent, then snippet; override on the DIRECT path is not exposed—synthesis PRE step can tune via SynthesisConfig.webEvidence.preferCleanContent). Summary/findings are labeled as hints only. When NARRIX is not in play, context comes from the context generator plus any taskMemory.narrix section.
  7. execute via executor using executionType (or the pipeline MAIN step when executionPipeline is set)
  8. on not-found, call registry diagnostics

executionPipeline: When executionPipeline is a non-empty array, PRE steps (e.g. synthesized-context) run first, then exactly one MAIN step, then optional POST steps. Memory enrichment and context generation in step 6 apply to the MAIN direct run; the synthesizer’s source material is controlled separately by contextSourcePolicy / webEvidence (see below).

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 WorecesSkillsClient.resolveRawTemplate (templates loaded from the Catalox ai-skills catalog in @woroces/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 reuses @woroces/ai-skills directly (private packages; naming leakage is explicitly acceptable).

Gateway template rendering (v4)

Task execution goes through WorecesSkillsClient.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 WorecesSkillsClientOptions when constructing WorecesSkillsClient (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 | |-----------|--------|--------| | WorecesSkillsClientOptions.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 @woroces/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 @woroces/ai-skills and construct WorexClientTasks with your own client + executor.

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

Bundled x12i stack (direct dependencies of this package): @x12i/catalox (catalog reads / publish script), @x12i/env (env conventions aligned with the Woreces stack), and @x12i/logxer (package log-level helpers such as resolvePackageLogsLevel used by the optional Activix client). 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-strategies-pre | Pre–core strategies: synthesisInputStrategy for the synthesized-context PRE step. | | ai-task-strategies-post | Post–core strategies: e.g. 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 FuncX wrappers 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 FuncX execution/plan before MAIN and can merge instructions, prompt, variables, and template tokens into the request. | | optimizer | { strategyKey: "optimizer", phase: "after", priority, maxIterations? } | Runs FuncX 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 wrapper defaultFunctionId, and only when the catalog row matches the already-validated strategy key, phase, request shape, and generic FuncX envelope.

Legacy monolith id ai-task-strategies is no longer written by this repo; use the catalogs above.

Publishing Firestore metadata

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 @woroces/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 @woroces/ai-skills). This package re-exports the Catalox skill surface from @woroces/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 { WorexClientSkills } from "@exellix/ai-tasks"; // re-export, or import from @woroces/ai-skills
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";

// skills client is the shared execution + helper layer
const skills = new WorexClientSkills({
  // ... 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: {
    xynthesisModel: "openai/gpt-5-nano",
    skillModel: "openai/gpt-5",
    temperature: 0.7,
    maxTokens: 2000
  },
  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: Non-empty agentId, jobTypeId, taskTypeId; required executionStrategies array (use [] for plain MAIN). When smartInput is present, validate shape (paths: array of non-empty strings or { title, path, required? } entries; reject {}, non-arrays, invalid elements, unknown root keys—only paths is allowed). Paths under xynthesized.* must use scope job, task, or execution. 1b. Compile taskConfiguration (when RunTaskRequest.taskConfiguration is set): map aiTaskStrategies.pre: "synthesis" and/or aiTaskProfile.inputSynthesis.enabled into executionPipeline PRE synthesized-context + includeContextInPrompt: true. Strip taskConfiguration before execution. See compileTaskConfigurationOnRunTaskRequest. Graph-engine must forward the node blob and wire runtime input — reports/graph-engine-task-pre-synthesis-compile.md.
  2. NARRIX pre-processor (if request.narrix is set): Resolve raw record (see NARRIX task-level pre-processor), run NARRIX (to-CNI + engine), build _narrix attachment, set executionMemory[attachToField] and jobMemory._narrix. Continue with the updated request.
  3. Structured Narrix (narrixMode: "handler"): Resolve narrixInput, run handler, merge into taskMemory.narrix / input as implemented; handler ctx includes xynthesized and smartInput.
  4. Local task dispatch: If getLocalTask(skillKey) returns a handler, call { input, ctx }. ctx includes skillKey, jobMemory, taskMemory, executionMemory, variables, xynthesized, smartInput, and graph/correlation ids. Returns RunTaskResponse; skips LLM MAIN path below.
  5. LLM path (pipeline or default MAIN): If executionPipeline is non-empty, run PRE (synthesized-context only), then MAIN, then optional POST (audit / polish). PRE synthesis may write request.xynthesized and response.xynthesizedPatch (details). Otherwise behave as a single MAIN direct step.
  6. MAIN execution (inside pipeline MAIN or default path): Build memoryBundle = { jobMemory, taskMemory, executionMemory }, enrichMemoriesWithScoping(skillKey, 'task', bundle), generate context when includeContextInPrompt (or pipeline synthesis overrides context). Optionally run aiScoping into input.aiScoped. Build enrichedInput: memories, context, input, top-level xynthesized and smartInput, and **variables = passthroughJobTemplateVariables(request.variables)` (job bucket only; see Gateway template rendering).
  7. Execute MAIN: Apply executionStrategies (planner/optimizer FuncX wrappers when non-empty) and call runSkill / gateway via the configured executor.
  8. Finalize response: Identity / task metadata as today; attach xynthesizedPatch when PRE synthesis produced job, task, or execution writes; when the executor returns SmartInputRenderResult, lift it to response.smartInputRenderResult and metadata.smartInputRenderResult (also accepts shaped metadata.smartInput from downstream).
  9. Not-found: Executor may invoke registry diagnostics when content is missing.

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, llmCall.maxTokensCap). See .docs/flow-io/ for flow-level wiring.

Skill request analysis (@exellix/ai-skills 5.6+)

@exellix/ai-tasks re-exports the Catalox-backed analysis and template-catalog helpers from @exellix/ai-skills so apps can depend on a single package:

| Export | Purpose | |--------|---------| | analyzeSkillRequest, buildSkillRequestAnalysisPacket, formatSkillRequestAnalysisMarkdown | Deterministic preflight for a flat RunSkillRequest (templates, smart input, output contract, gateway packet preview). | | analyzeRunTaskRequest(catalox, request, options?) | Same analysis on pickRunSkillRequestFields(request), plus optional merge of validateRunTaskConfig findings (includeAiTasksConfigValidation, default true). | | 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. |

Rendrix helpers used by validation are also re-exported: listTokens, analyzeTemplateResolution, renderSmartInput, SmartInputInvalidError, TemplateResolutionError, and related types.

import {
  analyzeRunTaskRequest,
  formatSkillRequestAnalysisMarkdown,
  buildSkillRequestAnalysisPacket,
} from "@exellix/ai-tasks";

const analysis = await analyzeRunTaskRequest(catalox, runTaskRequest, {
  logger,
  includeAiTasksConfigValidation: true,
});
console.log(formatSkillRequestAnalysisMarkdown(analysis.report));

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.enableWebScope (optional): when true, after a successful NARRIX run the SDK calls @exellix/narrix-web-scoper and stores the result on executionMemory.webContext (always set when enabled, including miss/error shapes). Requires TAVILY_API_KEY. Default off.
  • narrix.webScopeTemplates (optional): non-empty array → passed to the scoper as template-driven queries (athenix-parser tokens); replaces default query planning when set.
  • narrix.webScopeQuestionTemplate (optional): Handlebars template for the search question when webScopeTemplates is not set.
  • narrix.webScopeObjects (optional): merged into template context for webScopeTemplates / Handlebars.
  • narrix.webScopeQuestions (optional): explicit list of web-scope questions when webScopeTemplates is not set; uses scopeQuestionPack in @exellix/narrix-web-scoper. Each entry: { id?: string; question: string; source?: "manual" | "ai-driven" } (see documenations/web-scoping-in-ai-tasks.md).
  • narrix.webScoping (optional): per-request slice of WebScoperConfig.scoping from @exellix/narrix-web-scoper (snippets, caps, raw body, maxQueries, freshnessDays, etc.). Forwarded into the scoper so behavior matches upstream releases. Omit or use {} for package defaults.

Web scoping and upstream packages

Web scoping is implemented by @exellix/narrix-web-scoper (planning + WebContext mapping) and @exellix/search-adapter (Tavily normalization into SearchSource fields). This repo does not map provider JSON; it wires ScopeInput, optional narrix.webScoping, and runWebScope().

When narrix.webScoping.includeSourceSnippets is true, each WebContext.sources[] entry may include provider-layer text as documented in the web-scoper README: first-class providerContent / providerRawContent, legacy mirrors content / rawContent, snippet, snippetCharCount, provenance fields (contentOrigin, retrievalStage, contentSource, etc.). Defaults keep includeSourceSnippets off so those fields are omitted. Optional caps: maxSnippetCharsPerSource (Unicode cap on provider excerpts and the text chosen for snippet; also forwarded as snippetMaxChars on search requests when positive). maxTotalWebContextChars applies only to WebSource.snippet across sources (after per-source caps)—it does not shrink stored providerContent / providerRawContent. To request raw body text from the provider, set narrix.webScoping.snippetIncludeRawContent (e.g. true or "markdown"); it is forwarded as includeRawContent on search requests. sourceExcerptFrom selects which provider field feeds snippet (providerContent default, or providerRawContent, with deprecated aliases content / rawContent). Use @exellix/search-adapter and @exellix/narrix-web-scoper versions from the same release family as this package’s dependencies.

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 available: 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 narrix.enableWebScope === true, executionMemory.webContext is set to the WebScoperResult from @exellix/narrix-web-scoper (hit, miss, or structured error). It is independent of _narrix; NARRIX output remains attached even if web scoping fails.

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.available === 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 a