@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.
Maintainers
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 unwrapFuncxRunValue → getRunJsonResult).
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):
- if
request.narrixis set (task-level pre-processor) → resolve raw record from executionMemory/jobMemory/input, run NARRIX (to-CNI + engine), build_narrixattachment (scoping/discovery/meta), inject intoexecutionMemoryandjobMemory; ifnarrix.enableWebScope === true, also run@exellix/narrix-web-scoperand setexecutionMemory.webContext(failures are non-fatal); then continue with the same request so the task sees enriched context - if a local task handler is registered for
skillKey→ run it and return (no enrichment, no LLM);ctxincludes optionalxynthesized/smartInputwhen the caller supplied them - if
executionType === 'narrix-then-direct'andnarrixInputis provided → resolve narrix input, run Narrix, append output totaskMemory.narrix, then run the standard DIRECT path (enrich → context → executor); response includesmetadata.narrix - inject
bindingDefaultsDbfor Xronox routing (defaulting toMONGO_LOGS_DBorlogs-db) - enrich job/task memory with task-scoped scoping (using
skillKey) - 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 fromexecutionMemory._narrix(buildNarrixPreProcessorContextMarkdown), plus—when web scoping returned a hit—a ## Web sources (primary evidence) block built fromexecutionMemory.webContext(by default cleaned text is preferred over raw HTML:providerContent/contentbeforeproviderRawContent/rawContent, thensnippet; override on the DIRECT path is not exposed—synthesis PRE step can tune viaSynthesisConfig.webEvidence.preferCleanContent). Summary/findings are labeled as hints only. When NARRIX is not in play, context comes from the context generator plus anytaskMemory.narrixsection. - execute via executor using
executionType(or the pipeline MAIN step whenexecutionPipelineis set) - 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.taskCoreis removed from structured synthesis semantics; runtime derives cores from template declarations.- Synthesis mode selection is additive:
- preferred:
SynthesisConfig.synthesisMode: "markdown" | "structured" - legacy-compatible:
SynthesisConfig.synthesisOutputFormatstill works.
- preferred:
- Structured mode uses detected
templateCores+ resolved question, then builds clean MAIN context from validated synthesized payload. - Core discovery reads raw templates (instructions/prompt) via
WorecesSkillsClient.resolveRawTemplate(templates loaded from the Cataloxai-skillscatalog in@woroces/ai-skills4.1+) before normal rendering; if no core directives are declared, structured synthesis is rejected as a template-definition error. - Raw/enriched materials (
_narrix, memory bundle,webContext) remain in memory; synthesized output is also stored atexecutionMemory.synthesizedContextfor MAIN traceability unless a PRE step opts out viaSynthesisConfig.xynthesizedOutput.alsoWriteLegacySynthesizedContext: false(see Xynthesized memory and smart input). - Optional
RunTaskRequest.xynthesizedholds job-scoped, task-scoped, and execution-scoped synthesized material for graph execution (distinct from raw memories). OptionalsmartInputmatchesSmartInputConfigfrom@exellix/ai-skills/ Rendrix (pathsas{ title, path, required? }[]). Callers may still send legacypaths: string[];runTaskvalidates and normalizes each string to{ title: path, path }beforerunSkill. Paths underxynthesized.*must use scopejob,task, orexecution; full path resolution for{{smartInput}}happens in the gateway/Rendrix stack. OptionalsmartInputRenderOptionsis passed through like otherRunSkillRequestfields. - Backward compatibility is preserved by default: existing flows continue unchanged unless structured mode is explicitly selected.
Task responses may optionally include intermediateSteps when a task (or skill) runs multiple logical steps in one call (e.g. to-cni + enrich + triage); see Intermediate steps (multi-step tasks).
@exellix/ai-tasks 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 asRunSkillRequest:templateRenderOptions,templateTokens,smartInputRenderOptions(withsmartInput). They are passed through unchanged torunSkillon the DIRECT / pipeline MAIN path.- Default parser options for all skill runs: set
templateRenderingonWorecesSkillsClientOptionswhen constructingWorecesSkillsClient(or configuretemplateRenderingon a gateway instance you pass asoptions.gateway). Per-call overrides usetemplateRenderOptionson the request. - Synthesis, audit, polish, and AI scoping are executed via
@exellix/xynthesis(and therefore inherit xynthesis behavior for retries/repair and diagnostics when enabled).
| Mechanism | Where | Effect |
|-----------|--------|--------|
| 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-tasksCustom gateway / advanced injection: add @woroces/ai-skills and construct WorexClientTasks with your own client + executor.
npm install @exellix/ai-tasks @woroces/ai-skillsBundled 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-strategiesThat provisions apps/ai-tasks, all six native catalogs, bindings, descriptors, and canonical items.
Reading catalogs and items (from @exellix/ai-tasks)
Construct a Catalox instance (for example createCataloxFromEnv() from @exellix/ai-tasks, which re-exports it from @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.inputcan be any JSON-compatible object (or string).input.questionis optional. - Correlation (required): every
runTaskrequest must include non-emptyagentId,jobTypeId, andtaskTypeId(same as@exellix/ai-skills). SeeRUNTASK_REQUEST.md(including Graph task node → RunTaskRequest for authoring vs invoke). - When question matters: Only skills/templates that explicitly reference
{{question}}(or rely on question-driven synthesis) need a question. Utility tasks should not. - Output: Utility tasks should typically return a structured
parsedpayload (for downstream graph nodes), not only prose inrawText.
Example (utility task with no question):
import { runTask } from "@exellix/ai-tasks";
const res = await runTask({
agentId: "my-agent",
jobTypeId: "prepare-context-job",
taskTypeId: "prepare-context-task",
skillKey: "skills/graph.prepareContext",
input: {
record: { /* ... */ },
policy: { /* ... */ },
},
executionMemory: {},
jobMemory: {},
graphId: "graph-123",
nodeId: "prepare-context",
});
console.log(res.parsed);Example (utility task as a local deterministic handler):
import { runTask } from "@exellix/ai-tasks";
const res = await runTask({
skillKey: "skills/skill.local:validateInput",
input: {
recordsPath: { $path: "executionMemory.inputs.records" },
rules: { requirePaths: ["id"] },
},
executionMemory: { inputs: { records: [{ id: "a" }] } },
});
console.log(res.parsed); // { ok: true, meta: ... } (example shape)Structured decision output (response.parsed) + optional schema enforcement
For decision/utility nodes, prefer returning a stable object payload in response.parsed so graph outputMapping can reference fields deterministically (no markdown parsing).
Example decision task (built-in local handler):
import { runTask } from "@exellix/ai-tasks";
const res = await runTask({
skillKey: "skills/graph.decide-web-scope",
input: { localEvidenceSufficient: true },
taskKind: "decision",
outputValidation: {
schema: {
type: "object",
required: ["contractVersion", "shouldUseWeb", "reasonCodes", "missingSignals"],
properties: {
contractVersion: { type: "string" },
shouldUseWeb: { type: "boolean" },
reasonCodes: { type: "array", items: { type: "string" } },
missingSignals: { type: "array", items: { type: "string" } },
},
additionalProperties: false,
},
mode: "fail",
},
});
// Stable structured artifact for deterministic graph mapping:
// - res.parsed.shouldUseWeb
// - res.parsed.reasonCodes
// - res.parsed.missingSignalsgetPackagedAiTasksMetadataDir() returns the absolute path to the shipped .metadata folder (override with contentRegistryLocalPath in client options). forPackagedSkills is an alias of createDefaultWorexClientTasks.
Class-Based API (custom ai-skills client)
import { 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 outputFunction-Based API (Convenience)
import { runTask, ExecutionType } from "@exellix/ai-tasks";
// Automatically initializes SDK (ERC mode)
// executionType is optional, defaults to ExecutionType.DIRECT
const res = await runTask({
skillKey: "tasks/security-risk-summary",
agentId: "agent-1",
jobTypeId: "security-job-type",
taskTypeId: "security-task-type",
executionStrategies: [],
input: { assetId: "a-123" },
jobId: "job-1",
});RunTask Algorithm (Canonical)
Given a request, runTask() performs:
- Validate: Non-empty
agentId,jobTypeId,taskTypeId; requiredexecutionStrategiesarray (use[]for plain MAIN). WhensmartInputis present, validate shape (paths: array of non-empty strings or{ title, path, required? }entries; reject{}, non-arrays, invalid elements, unknown root keys—onlypathsis allowed). Paths underxynthesized.*must use scopejob,task, orexecution. 1b. CompiletaskConfiguration(whenRunTaskRequest.taskConfigurationis set): mapaiTaskStrategies.pre: "synthesis"and/oraiTaskProfile.inputSynthesis.enabledintoexecutionPipelinePREsynthesized-context+includeContextInPrompt: true. StriptaskConfigurationbefore execution. SeecompileTaskConfigurationOnRunTaskRequest. Graph-engine must forward the node blob and wire runtime input —reports/graph-engine-task-pre-synthesis-compile.md. - NARRIX pre-processor (if
request.narrixis set): Resolve raw record (see NARRIX task-level pre-processor), run NARRIX (to-CNI + engine), build_narrixattachment, setexecutionMemory[attachToField]andjobMemory._narrix. Continue with the updated request. - Structured Narrix (
narrixMode: "handler"): ResolvenarrixInput, run handler, merge intotaskMemory.narrix/inputas implemented; handlerctxincludesxynthesizedandsmartInput. - Local task dispatch: If
getLocalTask(skillKey)returns a handler, call{ input, ctx }.ctxincludesskillKey,jobMemory,taskMemory,executionMemory,variables,xynthesized,smartInput, and graph/correlation ids. ReturnsRunTaskResponse; skips LLM MAIN path below. - LLM path (pipeline or default MAIN): If
executionPipelineis non-empty, run PRE (synthesized-contextonly), then MAIN, then optional POST (audit/polish). PRE synthesis may writerequest.xynthesizedandresponse.xynthesizedPatch(details). Otherwise behave as a single MAINdirectstep. - MAIN execution (inside pipeline MAIN or default path): Build
memoryBundle = { jobMemory, taskMemory, executionMemory },enrichMemoriesWithScoping(skillKey, 'task', bundle), generatecontextwhenincludeContextInPrompt(or pipeline synthesis overrides context). Optionally runaiScopingintoinput.aiScoped. BuildenrichedInput: memories,context,input, top-levelxynthesizedandsmartInput, and **variables= passthroughJobTemplateVariables(request.variables)` (job bucket only; see Gateway template rendering). - Execute MAIN: Apply
executionStrategies(planner/optimizer FuncX wrappers when non-empty) and callrunSkill/ gateway via the configured executor. - Finalize response: Identity / task metadata as today; attach
xynthesizedPatchwhen PRE synthesis producedjob,task, orexecutionwrites; when the executor returnsSmartInputRenderResult, lift it toresponse.smartInputRenderResultandmetadata.smartInputRenderResult(also accepts shapedmetadata.smartInputfrom downstream). - 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 stringmodelUsed: string for LLM-backed steps;nullfor deterministic stepsmetadata.timing:{ startedAt, endedAt, durationMs }metadata.step:{ phase, type, stepId }(when applicable)
When upstream providers expose them (via @exellix/xynthesis / @exellix/ai-skills trace surfaces), trace tasks may also include usage/routing/cost under stable metadata keys.
Graph / smart-input hints (ai-tasks–specific): Trace entries for MAIN direct execution include metadata.smartInput: { paths } (echo of RunTaskRequest.smartInput.paths after normalization—{ title, path }[], or empty array). PRE synthesized-context entries may include metadata.xynthesized (inputPathsUsed, outputDestination, outputKey, patchKeys) when SynthesisConfig.xynthesizedOutput is set—keys only, not full synthesized payloads.
Xynthesized memory and smart input
Graph runtimes need structured execution context that is not the same as raw jobMemory / taskMemory / executionMemory: material synthesized once at job scope, scoped to a single node, or held in a run-wide execution bucket. RunTaskRequest supports:
| Field | Purpose |
|-------|---------|
| xynthesized?: { job?, task?, execution? } | job — synthesized context reusable across nodes in the same graph run (graph-engine merges patches from each runTask). task — scoped to this task/node invocation only. execution — run-wide execution bucket (distinct from job/task). Values are Record<string, unknown> buckets keyed by your outputKey (and any caller-supplied keys). |
| smartInput?: RunTaskSmartInput | Declares smart-input paths for Rendrix {{smartInput}} (via @exellix/ai-skills / gateway). Use Rendrix-native paths: { title, path, required? }[], or legacy paths: string[] (normalized before runSkill). Paths under xynthesized.* must use job, task, or execution (e.g. xynthesized.execution.priorAnalysis). The smartInput object allows only the paths property at the root. Optional smartInputRenderOptions on RunTaskRequest controls markdown folding for the macro (same merge rules as RunSkillRequest). Full dot-path resolution against live memory happens downstream (Rendrix / gateway). |
Validation (smartInput)
If smartInput is present, it must be a plain object with paths (array of non-empty strings or { title, path, required? }) and optional strict (boolean). Invalid: smartInput: {}, bad paths, extra root keys, or xynthesized.<scope>.* with <scope> not job, task, or execution. Prefer path prefixes jobVariables.* and taskVariables.* (legacy variables.* ≡ job scope when the engine resolves paths). { paths: [] } is valid.
At runTask() entry, invalid shape throws SmartInputValidationError (error.code, error.details, error.skillKey) — the task does not run (no NARRIX, no PRE xynthesis, no MAIN). Omitted smartInput is allowed.
Pre-flight validation (no LLM invoke)
Use these helpers before runTask() to inspect config and payload without calling xynthesis or the gateway:
| Function | Purpose |
|----------|---------|
| validateRunTaskConfig(request) | Static checks: agentId / jobTypeId / taskTypeId, executionStrategies, smartInput shape, modelConfig / llmCall numeric fields, executionPipeline (one MAIN, synthesis PRE context flag). Returns { ok, issues, errors, warnings }. |
| validateRunTaskInvoke({ request, templateResolver?, ... }) | Config checks plus whether expected paths resolve on the request (input, memories, xynthesized). Uses Rendrix renderSmartInput for required smart-input paths and optional analyzeTemplateResolution on raw templates. |
| analyzeExpectedRunTaskInput({ skillKey, smartInput?, instructions?, prompt?, templateResolver? }) | Returns expected dot-paths from smartInput.paths and path tokens in skill templates (Rendrix listTokens). |
import {
validateRunTaskConfig,
validateRunTaskInvoke,
analyzeExpectedRunTaskInput,
SmartInputValidationError,
isSmartInputValidationError,
} from "@exellix/ai-tasks";
// Config only (fast)
const configCheck = validateRunTaskConfig(runTaskRequest);
if (!configCheck.ok) {
for (const issue of configCheck.errors) {
console.error(issue.code, issue.path, issue.message);
}
}
// Config + payload + templates
const invokeCheck = await validateRunTaskInvoke({
request: runTaskRequest,
templateResolver: {
async resolveRawTemplate(skillKey, section) {
const r = await skillsClient.resolveRawTemplate(skillKey, section);
return r?.found ? r.content : undefined;
},
},
checkTemplateResolution: true,
});
if (!invokeCheck.ok) {
console.error(invokeCheck.errors);
}
// What paths does this node need?
const expected = await analyzeExpectedRunTaskInput({
skillKey: runTaskRequest.skillKey,
smartInput: runTaskRequest.smartInput,
prompt: "...",
});Each issue has code, severity (error | warning), message, and optional path (e.g. smartInput.paths[0].path, 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: Samexynthesized/smartInputas on therunTaskrequest after normalization (pipeline PRE runs later and can mutatexynthesizedbefore MAIN). - PRE
synthesized-context: The bundle passed into template/rendering includesxynthesizedandsmartInputso synthesis prompts can reference them consistently with MAIN. - MAIN executor payload: Top-level
xynthesized,smartInput(normalized), andsmartInputRenderOptions(when set) are forwarded on the object passed torunSkill(alongside memories andcontext). - Template variables:
variables(job bucket) andexecutionMemory.jobVariables/executionMemory.taskVariablesare forwarded separately; use{{xynthesized…}},{{smartInput}},jobVariables.*, andtaskVariables.*in templates (not a single flattened bag). - Response: When downstream returns a
SmartInputRenderResult,runTaskfinalizeexposesresponse.smartInputRenderResultand mirrorsmetadata.smartInputRenderResult(also readsmetadata.smartInputwhen it matches that shape).
PRE synthesis destination (SynthesisConfig.xynthesizedOutput)
Optional on the synthesized-context step config:
destination:"job"|"task"|"execution"— writes underrequest.xynthesized.job[outputKey],.task[outputKey], or.execution[outputKey](no redirect to other scopes).outputKey: string key for the synthesized value.mode:"replace"(default) or"merge"— for merge, if the existing value atoutputKeyand the new value are both plain objects, ai-tasks deep-merges them; otherwise the new value replaces.alsoWriteLegacySynthesizedContext: Whentrueor omitted, keep today’s behavior: persist structured/markdown artifacts onexecutionMemory.synthesizedContext(andsynthesizedInputwhen applicable) for MAIN. Whenfalse, skip that legacy persistence for this PRE step; MAIN still receives synthesized context markdown from the PRE step when produced.
Stored value: Prefer the full synthesis artifact when present (structured / question-driven / markdown artifact shape). If there is no artifact, ai-tasks stores { contextMarkdown } using the PRE step’s markdown string.
Response: RunTaskResponse.xynthesizedPatch has the same { job?, task?, execution? } shape so the graph-engine can merge into durable graph memory. Omitted when nothing was written.
Execution-scope example (PRE + smart-input):
executionPipeline: [
{
phase: "pre",
type: "synthesized-context",
config: {
xynthesizedOutput: { destination: "execution", outputKey: "priorAnalysis" },
},
},
{ phase: "main", type: "direct" },
],
smartInput: { paths: ["xynthesized.execution.priorAnalysis"] },
// response.xynthesizedPatch.execution.priorAnalysis → graph-engine merges into executionMemory.xynthesized.executionBuilder
TaskRequestBuilder: withXynthesized, withSmartInput, withSmartInputPaths (builds native { title, path } entries), withSmartInputRenderOptions, withXynthesizedJob, withXynthesizedTask, withXynthesizedExecution.
What ai-tasks does not do
Does not choose graph mappings, apply outputMapping, run the full graph, or own long-lived graph state—that belongs to the graph runtime. It allowlists xynthesized.* smart-input scopes (job, task, execution) but does not render {{smartInput}} or resolve paths against live memory (Rendrix / gateway).
Local Tasks
Local tasks are deterministic handlers (no LLM) that run inside runTask() before any memory enrichment or executor call. They are identified by skillKey. If a handler is registered for that skillKey, it runs with the task input and context; the return value is wrapped into a standard RunTaskResponse (e.g. parsed, metadata.durationMs, metadata.localSkillKey).
Built-in local tasks
| Skill key | Purpose |
|--------|--------|
| skills/node.callExport | Dynamically import a module, resolve an export (default or named), and call it. Supports $path-based argument resolution (e.g. input.payload, jobMemory.foo), optional job/global caching for instances. |
| skills/node.callExportBatch | Same module/export/cache semantics; creates or reuses a cached instance, then calls a batch method (e.g. enrichJson) with payload/options. Returns { ok, value, meta: { processed, errors, durationMs } }. |
| skills/skill.local:validateInput | Validate records at a $path (e.g. executionMemory.inputs.vulnInstances.records). Rules: requirePaths, graphTypeEquals. No mutation; returns { ok, meta: { total, valid, invalid } } or { ok: false, errors }. |
| skills/skill.local:normalizeNarrixResult | Normalize records to a stable “Narrix attachment” shape: ensure _narrix.meta.entity.entityKey, _narrix.scoping / _narrix.discovery and their arrays, joinCandidates. Returns { ok: true, value: { records } }. |
| skills/skill.local:narrixRun | Run one input through the Narrix pipeline (ingest → runner) for record, text, docs, or chat. Unified input: medium ("record" | "text" | "docs" | "chat") + datasetId + medium-specific payload. Returns { ok: true, entity, signals, stories, passes?, meta } or { ok: false, error, message?, meta? }. Legacy record shape (no medium) still supported. |
| skills/graph.collectEvidence | Opt-in reference local handler: collect evidence from structured requests (queries and/or URLs) and return a reusable EvidenceBundle for downstream graph nodes. No question required. |
Generic evidence collection (skills/graph.collectEvidence)
ai-tasks standardizes a generic, domain-agnostic evidence collection contract for staged graph execution. This is independent of NARRIX question-driven web scoping: callers provide structured requests; the result is a reusable EvidenceBundle suitable for storing in executionMemory and reusing downstream.
- Must-have: a stable input/output contract graphs can depend on cleanly.
- Reference handler: ai-tasks ships an opt-in local task (
skills/graph.collectEvidence) that implements the contract to reduce integration friction. Retrieval is not implicit; graphs opt in by selecting thisskillKey. - Backends: the reference handler may reuse existing internal search/fetch components, but the public contract does not expose NARRIX semantics (
narrix,webContext, web-scope templates, etc.). - Types: see
EvidenceCollectionInput/EvidenceBundleexported from@exellix/ai-tasks(implemented insrc/types/evidence-types.ts). - Current reference implementation notes:
- Supports query-driven discovery and direct URL fetch.
- Enforces policy limits like
maxRequests,maxSourcesPerRequest,maxTotalSources, domain allow/block lists, and snippet size caps. - Returns
value.sources[]+ per-requestvalue.requests[]. Ifextraction.returnFacts/extraction.returnSummaryare set, the reference handler currently returns emptyfacts: []/summary: ""(placeholders for future variants).
Recommended graph pattern:
const evidenceRes = await runTask({
skillKey: "skills/graph.collectEvidence",
input: {
requests: [
{ kind: "vendor_guidance", query: "ExampleProduct security advisory" },
{ kind: "exploitation_status", url: "https://example.com/advisory" },
],
policy: { maxTotalSources: 10, includeSnippets: true },
},
executionMemory,
jobMemory,
graphId,
nodeId,
});
// Store once, reuse later:
executionMemory.evidence = evidenceRes.parsed; // EvidenceBundlenode.callExport (input contract)
module: string (module name or path for dynamicimport())export: string (default"default") — export namecall:{ method: string, args?: any[] }—methodcan be"function"(call export as function),"create"(call and optionally cache), or any other string (call as method on cached/created instance)cache: optional{ scope: "global" | "job", key: string }- Args may use
{ $path: "input.x" }(orjobMemory.*,taskMemory.*,executionMemory.*,variables.*); resolved via dot-path before calling.
node.callExportBatch (input contract)
module,export,cache: same as abovecall.create:{ method, args? }— used to create/cache the instancecall.batch:{ method, args? }— method and args for the batch call (args can use$path)payload,options: typically passed via$pathincall.batch.args(e.g.input.payload,input.options)
skills/skill.local:validateInput (input contract)
recordsPath:{ $path: "..." }— path to records array (e.g.executionMemory.inputs.vulnInstances.records)rules.requirePaths: string[] — dot paths that must exist on each recordrules.graphTypeEquals: optional string — requiregraphized.meta.graphTypeto equal this valuedatasetId: optional (for validation rules)
skills/skill.local:normalizeNarrixResult (input contract)
recordsPath:{ $path: "..." }— path to records arrayattachRoot: string (default"_narrix") — key under which Narrix attachment livesdefaults.scopingPath,defaults.discoveryPath: optional paths under attachRoot (default"scoping","discovery")
skills/skill.local:narrixRun (input contract)
One handler supports four media: record, text, docs, chat. Send a unified input with medium and datasetId, plus the payload for that medium. For full object shapes and use cases, see NARRIX-FOUR-OBJECTS-AND-USE-CASES.md.
Unified input (recommended):
- Record:
{ medium: "record", datasetId: string, record: Record<string, unknown> } - Text:
{ medium: "text", datasetId: string, text: string, meta?: object } - Docs:
{ medium: "docs", datasetId: string, document: { pages: Array<{ text, pageId?, pageNumber?, ... }>, docId?, title? } } - Chat:
{ medium: "chat", datasetId: string, thread: { messages: Array<{ role, text, ... }>, threadId?, title?, participants? } }
Success response: { ok: true, entity, signals, stories, passes?, meta } — meta includes datasetId, packId, entityKind (and run metadata).
Failure response: { ok: false, error: string, message?: string, meta?: object } — e.g. NARRIX_DISABLED, NARRIX_INVALID_INPUT, RUNNER_PACK_NOT_FOUND, or ingest/runner error codes.
Feature flag: Set USE_NARRIX_INGEST=1 to enable the Narrix pipeline (use npm run test:with-narrix-ingest on Windows instead of prefixing the command). When unset, the handler returns { ok: false, error: "NARRIX_DISABLED" } without calling ingest or runner.
Debug logging: Set NARRIX_DEBUG=1 to log datasetId, medium, packId, entityKind, signal codes, and narrativeTypeIds on success (default: quiet).
See NARRIX-STATUS-REPORT.md for implementation details and test layout.
Custom local tasks
You can register your own handlers so they run when skillKey matches the registered key:
import {
registerLocalTask,
getLocalTask,
type LocalTaskContext,
type LocalTaskHandler,
} from "@exellix/ai-tasks";
const myHandler: LocalTaskHandler = async ({ input, ctx }) => {
// ctx: skillKey, jobMemory, taskMemory, executionMemory, variables, jobId, agentId, graphId, nodeId, ...
return { ok: true, value: input.someField };
};
registerLocalTask("skills/my.custom.task", myHandler);
// Later: runTask({ skillKey: "skills/my.custom.task", input: { someField: 1 } }) will run myHandler and skip enrichment/executorregisterLocalTask(skillKey: string, handler: LocalTaskHandler): void— register a handler forskillKeygetLocalTask(skillKey: string): LocalTaskHandler | undefined— get handler (used internally byrunTask)LocalTaskContext—skillKey,jobMemory,taskMemory,executionMemory,variables,xynthesized,smartInput,jobId,taskId,agentId,graphId,nodeId,prevNodeId,coreSkillId,masterSkillId,masterSkillActivityIdLocalTaskHandler—(args: { input: any; ctx: LocalTaskContext }) => Promise<any>
Execution tracing (metadata)
For graph runtimes (e.g. worex-graph), local task responses include consistent metadata so a trace can be stored per node:
metadata.durationMs— time taken by the handlermetadata.localSkillKey—skillKeythat was run (e.g."skills/node.callExportBatch","skills/skill.local:validateInput")
Downstream can store e.g. executionMemory._trace.nodes[nodeId] with taskKey, ok, durationMs, summary (from parsed.meta when present).
Exported API for local tasks: registerLocalTask, getLocalTask, registerBuiltInLocalTasks, LocalTaskContext, LocalTaskHandler (see API Reference and Types).
ExecutionType
executionType is optional on every request and defaults to DIRECT if not provided.
| Way | How you trigger it | What runs |
|-----|--------------------|-----------|
| NARRIX pre-processor | request.narrix set (e.g. from graph node metadata) | Resolve record → run NARRIX → inject _narrix into executionMemory/jobMemory; then run local task or DIRECT as below. |
| Local task | skillKey = local skill key (e.g. skills/skill.local:narrixRun) | Handler only; no enrichment, no LLM. |
| Executor only | Any other skillKey, executionType omitted or "direct" | Enrich → context → executor/LLM. No Narrix. |
| Narrix then execute | Same as executor, but executionType: "narrix-then-direct" + narrixInput | Narrix → append to taskMemory.narrix → then enrich → context → executor/LLM. |
Currently supported:
DIRECT— single execution call after task-level enrichment/context (default)NARRIX_THEN_DIRECT("narrix-then-direct") — run Narrix first, inject result into task memory and context, then run the same DIRECT path; requiresnarrixInput(or resolution from job memory vianarrixInput.$path). See Narrix then execute below for details.
Unsupported types throw an error with actionable context.
Narrix then execute
Narrix processes four input types (record, text, docs, chat) and supports several use cases (local skills/skill.local:narrixRun, narrix-then-direct, normalize, validate). For the four input objects, their payload shapes, and detailed use cases, see NARRIX-FOUR-OBJECTS-AND-USE-CASES.md.
Use narrix-then-direct when you want one runTask() call to scope data with Narrix (entity, signals, stories) and then run an LLM skill with that context—without calling skills/skill.local:narrixRun and then a second runTask() manually.
Request
Same as a normal executor call, plus:
executionType:"narrix-then-direct"or use the exported constantNARRIX_THEN_DIRECT.narrixInput(required): Either:- Full Narrix input:
{ medium, datasetId, record }(ortext/document/threadper medium). Same shape as the skills/skill.local:narrixRun (input contract) section elsewhere in this README. - Or
{ $path: "jobMemory.currentRecord" }(or anyjobMemory.*dot-path) to resolve fromrequest.jobMemory.
- Full Narrix input:
narrixScope(optional): Filters which signals and stories from Narrix are kept in task memory. Shape:includeSignals?: string[]— keep only signals whosecodeis in this array.excludeSignals?: string[]— drop signals whosecodeis in this array (ignored whenincludeSignalsis set).includeStories?: string[]— keep only stories whosenarrativeTypeIdis in this array.excludeStories?: string[]— drop stories whosenarrativeTypeIdis in this array (ignored whenincludeStoriesis set).- When omitted, all signals and stories pass through unfiltered.
- Example:
narrixScope: { includeSignals: ["OPEN_EGRESS_DETECTED", "PUBLIC_IP_EXPOSED"], includeStories: ["subnet-risk-summary"] }— the task only sees these specific signals and narratives from the full Narrix output.
Where Narrix output goes
- Task memory:
taskMemory.narrixis an array of Narrix run outputs ({ entity, signals, stories, meta }). The executor and LLM receive this in the enriched memory and in the generated context (e.g. "## Narrix Scoping" section). - Convenience: The latest run is also set on the skill input as
input.narrixContextso prompt templates can reference it directly. - Response: On success,
result.metadata.narrixcontains the last run's{ entity, signals, stories, meta, durationMs }.
Minimal example
import { runTask, NARRIX_THEN_DIRECT } from "@exellix/ai-tasks";
const result = await runTask({
skillKey: "tasks/security-risk-summary",
executionType: NARRIX_THEN_DIRECT,
narrixInput: {
medium: "record",
datasetId: "subnetEgress",
record: { graphized: { id: "subnet-1", cidr: "10.0.0.0/24" } },
},
input: { question: "Summarize the security posture of this subnet" },
jobId: "job-1",
agentId: "agent-1",
});
// result.parsed → LLM output (from the skill)
// result.metadata.narrix → { entity, signals, stories, meta, durationMs } from NarrixFailure behavior
If Narrix fails (e.g. ok: false, or NARRIX_DISABLED when USE_NARRIX_INGEST is not set), the function returns early with parsed: { ok: false, phase: "narrix", error, message } and metadata.narrix; the executor/LLM is not called.
Export
NARRIX_THEN_DIRECT is exported from @exellix/ai-tasks for use in executionType.
NARRIX task-level pre-processor
When a task request includes a narrix config (e.g. from graph node taskConfiguration.narrix), ai-tasks runs the NARRIX enrichment pipeline before executing the task and injects the result so the task handler (or LLM) can use it. This supports "narrix serving graph": graph nodes are domain tasks (e.g. assess reachability, assess posture); NARRIX is configuration on the node that enriches context, not a separate pipeline exposed as graph steps.
Request
Add optional narrix to RunTaskRequest:
narrix.datasetId(required): e.g."network.egress.subnets","network.vuln.instances".narrix.attachToField(optional): field name onexecutionMemoryfor the attachment; default"_narrix".narrix.engineConfigPath,narrix.packsRoot,narrix.deterministicSort,narrix.assumptionsPolicy: reserved for future use.narrix.enableWebScope(optional): whentrue, after a successful NARRIX run the SDK calls@exellix/narrix-web-scoperand stores the result onexecutionMemory.webContext(always set when enabled, including miss/error shapes). RequiresTAVILY_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 whenwebScopeTemplatesis not set.narrix.webScopeObjects(optional): merged into template context forwebScopeTemplates/ Handlebars.narrix.webScopeQuestions(optional): explicit list of web-scope questions whenwebScopeTemplatesis not set; usesscopeQuestionPackin@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 ofWebScoperConfig.scopingfrom@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:
executionMemory.input.rawexecutionMemory.inputs.<first key>.rawjobMemory.recordorjobMemory.currentRecordinput.recordorinput.raw
If no record is found, runTask throws with a clear error. sourceMeta is taken from executionMemory.input.sourceMeta or jobMemory.sourceMeta or {}.
Where the result goes
- Canonical:
executionMemory[attachToField](defaultexecutionMemory._narrix) — shape{ scoping: { stories, signals }, discovery: { stories, signals }, meta }. - Mirrored:
jobMemory._narrix— same attachment so handlers can read from eitherctx.executionMemory._narrixorctx.jobMemory._narrix. - Web scope: when
narrix.enableWebScope === true,executionMemory.webContextis set to theWebScoperResultfrom@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 orderid(string): stable identifier (e.g."to-cni","engine-enrich","triage-q1-q6")ok(boolean): whether the step succeededsummary(optional): short description (e.g."cni built","enriched")error(optional): error message whenokis falseinputSummary(optional): summary input for reportingoutputExcerpt(optional): small excerpt of step output for debugging
Skills/tasks can return steps inside parsed (e.g. parsed.intermediateSteps); the SDK lifts them to the top-level response.intermediateSteps so consumers always read from the same place. Local tasks can return { output: {...}, intermediateSteps: [...] }; 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:
executionPipelineis 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" }]. ExistingexecutionType(e.g.DIRECT,NARRIX_THEN_DIRECT) is still supported whenexecutionPipelineis not set. - PRE step
synthesized-context: A weak model (defaultgpt-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 itscontext. RequiresincludeContextInPrompt: true(or the PRE step’sconfig.autoEnableContext: true). Add{ phase: "pre", type: SYNTHESIZED_CONTEXT, config: SynthesisConfig }before the main step (SYNTHESIZED_CONTEXTis exported as the string"synthesized-context"). - Two LLM calls (typical): deterministic source material (policy + markdown/JSON assembly) → synthesis model → synthesized
context→ main 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, optionalsynthesisInputStrategy(policy/execution-memory-only/job-memory-only/task-memory-only/full-memory-bundle), optionalwebEvidence,customSynthesizingGuidelines,fallbackToDirect(defaultfalse),memoryPaths,synthesisPromptOverride,timeoutMs,maxOutputLength,autoEnableContext, optional mode selectorssynthesisMode(preferred) andsynthesisOutputFormat(legacy-compatible), plus optionalquestionPath,getQuestion,structuredMaxItemsPerSide,structuredMaxItemContentChars, plus optionalquestionDrivenandquestions, plus optionalxynthesizedOutput(destination:"job"|"task"|"execution",outputKey,mode,alsoWriteLegacySynthesizedContext) to mirror synthesis intorequest.xynthesizedand returnresponse.xynthesizedPatch(see Xynthesized memory and smart input). Full API: documenations/synthesized-context-guide.md. - Question-driven synthesis (opt-in): Set
questionDriven: trueand providequestions([{ id, question, source?: "record"|"web"|"both" }]). This mode runs one synthesis per question and writes a structured a
