@exellix/ai-tasks
Private Git/npm package for executing tasks using the Exellix execution stack (@exellix/ai-tasks orchestrates @exellix/ai-skills MAIN + @exellix/xynthesis PRE/POST).
Catalox 6 train (in progress):
@exellix/xynthesisin this repo already uses Catalox 6 / Mongo locally (4.9.13 prepared, not published).@exellix/ai-tasksand@exellix/catalox-ai-skillsstill use Firebase until@x12i/funcx@4.10.0ships and catalox-ai-skills migrates. Phase 2 then alignsai-skills-manager-uiandai-tasks-playground-ui. Do not publish the stack until then. Seedocumenations/catalox-6-rollout.md.
Breaking — executionStrategies (required): Every runTask request must include executionStrategies: an array of MAIN strategy invocations or [] for plain gateway MAIN. The old executionStrategyKey field is removed. See BREAKING-CHANGES.md and RUNTASK_REQUEST.md. Supported MAIN execution is exactly: direct via executionStrategies: [], planner before MAIN, optimizer after MAIN, or planner + optimizer together. 8.5+: planner and optimizer run via @exellix/xynthesis sidekick actions (execution-plan, execution-evaluate-result) — not direct FuncX run() from ai-tasks. Catalog rows use runtimeKind: "xynthesis-action" and defaultSidekickAction.
Graphenix node execution (10.x — graph-engine / Studio)
runTask() requires a frozen nodePlan (planVersion: graphenix.node-plan/v2.1). Graph-engine compiles authoring JSON once; ai-tasks does not accept taskConfiguration or executionPipeline on the wire.
| Layer | Units | Memory contract |
|---|---|---|
| Layer 1 (pre-SKILL) | webScope, narrativePreprocessor, xynthesis pre-actions |
Enrich executionMemory (webContext, _narrix, …) |
| Layer 2 (in-SKILL) | synthesis PRE → plan/split → mainSkill → merge/pick-best |
Synthesis uses invokeContract.reads + optional unitParams.memoryPaths — not authoring preInputStrategy |
Authoring: aiTaskStrategies.pre: "synthesis" enables the context step only. Paths come from inputsConfig / smartInput.paths at compile time.
Compiled plan (graphenix 2.1.0+): synthesis unit must include non-empty invokeContract.reads and optional unitParams.memoryPaths. unitParams.synthesisInputStrategy is forbidden on the plan wire — validation error LEGACY_SYNTHESIS_INPUT_STRATEGY_ON_PLAN. The Catalox template key comes from supportedCapabilities.synthesisContext on the skill row only (@exellix/ai-skills ^6.11.0).
Dependencies (10.13+): @x12i/graphenix-executable-contracts ^2.15.0, @exellix/web-scope ^1.2.1, @exellix/ai-skills ^7.0.2, @exellix/xynthesis ^4.9.3, @x12i/funcx ^4.9.20, @x12i/ai-gateway ^11.0.0, @x12i/rendrix ^4.3.0, @exellix/catalox-ai-skills ^1.0.2. Requires Graphenix 2.15+ compiled nodePlan for synthesis/webScope IO.
Synthesis memory (10.8+): three template paths — executionMemory.synthesizedContext.contextMarkdown (job context), executionMemory.synthesizedInput.sourceMaterial (task input), executionMemory.synthesizedWebContext.contextMarkdown (web digest). webScope PRE runs forced synthesis to synthesizedWebContext. See BREAKING-CHANGES.md.
Gateway server tools on MAIN (llmRuntime) — 10.5+
When a mainSkill unit includes Graphenix llmRuntime, ai-tasks forwards serverTools, openrouter, and serverToolInstructionMode to @exellix/ai-skills (never to @x12i/ai-gateway directly). Tool metadata returns on RunTaskResponse.metadata and optional top-level mirrors; optional toolMetadataPatch.execution.aiTooling when llmRuntime.output.toolMetadata is configured.
| Path | Source | Destination |
|---|---|---|
| webScope (PRE) | externalPreUtility / strategyKey: webScope |
executionMemory.webContext, executionMemory.synthesizedWebContext (forced synthesis) |
| serverTools (MAIN) | mainSkill.llmRuntime.serverTools |
response.metadata.*, optional toolMetadataPatch |
Root RunTaskRequest.serverTools / openrouter are forbidden when nodePlan is set — use per-unit llmRuntime only. See documenations/gateway-server-tools.md.
Downstream handoff: graph-model-change-2026-06-13.md, graphenix-format-alignment-fr.md, graphenix-format-fact-guard-fr.md, ai-engine-runtime-runtask-integration.md. Wire fixture: test/fixtures/run-task/graph-engine-pre-synthesis.json.
Breaking — 8.4+ LLM invoke contract (no legacy): Every runTask requires modelConfig.preActionModel, modelConfig.skillModel, and modelConfig.postActionModel. Do not send xynthesisModel, root modelConfig.model, maxTokens, maxTokensCap, or graph-era tier names (weak / strong). Token budgets are owned by Optimixer in @exellix/ai-skills (MAIN) and @exellix/xynthesis (PRE/POST). Send outputExpectation (xynthesis) and optional reasoningEffort, temperature, topP. See How completion budgets work and Model profile aliases.
Stack alignment (8.4+)
| Package | Role in runTask |
Minimum (this repo) |
|---|---|---|
@x12i/ai-profiles |
Profile/choice → wire model (catalogLane required on resolve) |
3.0.0 |
@exellix/xynthesis |
PRE synthesis, POST audit/polish, scoping, utilities, execution-strategy sidekick actions (executeXynthesisAction via @x12i/funcx) |
≥ 4.9.2 |
@exellix/fact-guard-* |
Optional sub-action atom integrity (PRE/POST only; disabled by default) | ^1.0.0 (monorepo: file:; see ../fact-guard/RELEASE.md) |
@exellix/ai-skills |
MAIN runSkill (Optimixer caps, gateway server-tools pass-through, provider failure taxonomy) |
≥ 7.0.1 |
@x12i/funcx |
PRE/POST sidekick built-ins, orchestration run(), logging (direct in ai-tasks; default invoker in xynthesis) |
≥ 4.9.20 |
@x12i/ai-gateway |
LLM wire for MAIN skill (transitive via ai-skills) | ≥ 11.0.0 |
Gateway 11.x (via @exellix/ai-skills ≥ 7.0.2): The gateway owns billing — read response.metadata.costUsd, costStatus, and token usage only; do not post-price with @x12i/ai-tools. Every invoke requires explicit model and maxTokens (Optimixer supplies maxTokens on MAIN before gateway.invoke()). Removed upstream: default model, flex-md token auto-fill, packaged instruction blocks, and the 500 ms between-call rate limit. On Activix rows, prefer root cost / costUsd and outer.cost over outer.metadata for billing. Failed invokes expose error.metadata (GatewayInvokeRejectionMetadata with providerSubCode, operatorHint, providerAttempts when the fallback chain exhausts — gateway ≥ 10.4.4). Full migration rules: UPGRADING_AI_GATEWAY_10.md in @exellix/ai-skills.
Stored modelConfig slots must use explicit ai-profiles profile/choice keys (cheap/default, pro/default, …). Bare profile names (cheap, pro), profile@choice, registry shortcuts, and legacy tier aliases (weak / strong) are rejected at validation — not expanded at invoke.
Monorepo dev: npm test syncs sibling xynthesis/dist and ai-skills/dist into node_modules before compile (see scripts/ensure-xynthesis-dist.mjs and scripts/ensure-ai-skills-dist.mjs).
Execution model (10.x): Every runTask() requires a frozen nodePlan (graphenix.node-plan/v2.1). See RUNTASK_REQUEST.md for the authoritative wire contract and documenations/node-execution-graphenix.md for runtime behavior.
This package implements the canonical runTask() flow (every request carries executionStrategies — use [] for plain MAIN):
- Validate
nodePlanand reject forbidden legacy wire fields (executionPipeline,taskConfiguration, rootmodelConfig) - if a local task handler is registered for
skillKey→ run it and return - Loop
nodePlan.executionUnitsin order (PRE utilities, MAIN skill, POST utilities) viaexecuteNodeFromPlan - Return
RunTaskResponsewith optionalexecutionTraceevents
Legacy root executionPipeline, executionType, and compileTaskConfigurationOnRunTaskRequest are not supported at runtime in 10.x.
Template core and core-aware synthesis (additive)
runTask supports template-core-aware structured synthesis in PRE synthesized-context:
- Template content declares one or more core directives in Athenix
{{core:...}}format (for example{{core:analysis}}); closed list:question,action,plan,objective,decision,comparison,classification,evaluation,analysis,summary,generation,extraction. RunTaskRequest.taskCoreis removed from structured synthesis semantics; runtime derives cores from template declarations.- Synthesis mode selection is additive:
- preferred:
SynthesisConfig.synthesisMode: "markdown" | "structured" - legacy-compatible:
SynthesisConfig.synthesisOutputFormatstill works.
- preferred:
- Structured mode uses detected
templateCores+ resolved question, then builds clean MAIN context from validated synthesized payload. - Core discovery reads raw templates (instructions/prompt) via
ExellixSkillsClient.resolveRawTemplate(templates loaded from the Cataloxai-skillscatalog in@exellix/ai-skills4.1+) before normal rendering; if no core directives are declared, structured synthesis is rejected as a template-definition error. - Raw/enriched materials (
_narrix, memory bundle,webContext) remain in memory; synthesized output is also stored atexecutionMemory.synthesizedContextfor MAIN traceability unless a PRE step opts out viaSynthesisConfig.xynthesizedOutput.alsoWriteLegacySynthesizedContext: false(see Xynthesized memory and smart input). - Optional
RunTaskRequest.xynthesizedholds job-scoped, task-scoped, and execution-scoped synthesized material for graph execution (distinct from raw memories). OptionalsmartInputmatchesSmartInputConfigfrom@exellix/ai-skills/ Rendrix (pathsas{ title, path, required? }[]). Callers may still send legacypaths: string[];runTaskvalidates and normalizes each string to{ title: path, path }beforerunSkill. Paths underxynthesized.*must use scopejob,task, orexecution; full path resolution for{{smartInput}}happens in the gateway/Rendrix stack. OptionalsmartInputRenderOptionsis passed through like otherRunSkillRequestfields. - Backward compatibility is preserved by default: existing flows continue unchanged unless structured mode is explicitly selected.
Task responses may optionally include intermediateSteps when a task (or skill) runs multiple logical steps in one call (e.g. to-cni + enrich + triage); see Intermediate steps (multi-step tasks).
@exellix/ai-tasks depends on @exellix/ai-skills (MAIN) and @exellix/xynthesis (PRE/POST) and re-exports their catalog, invoke, and observability surfaces for single-package consumers.
Gateway template rendering (v4)
Task execution goes through ExellixSkillsClient.runSkill (or the skills client’s executor), which uses gateway.invoke() — not invokeChat() — so instruction, prompt, and context templates are built via buildMessages, nx-content resolution, and @x12i/rendrix render. The gateway rejects a top-level input field on invoke requests; runSkill / runAudit map caller input (and related fields) into workingMemory.input (merged with variables, memories, object context, knowledge) so .prompt templates can use {{input}}.
On the runTask MAIN path, variables is the job/graph bucket forwarded as-is (align with executionMemory.jobVariables). executionMemory.taskVariables holds node scope. input, xynthesized, and smartInput are separate top-level fields — ai-tasks does not fold them into variables. Templates and Rendrix resolve jobVariables.*, taskVariables.*, xynthesized.*, and smartInput against the forwarded payload. Optional host helper mergeSkillTemplateVariables merges maps outside default MAIN.
In this package:
runTask({ ... })accepts the same optional fields asRunSkillRequest:templateRenderOptions,templateTokens,smartInputRenderOptions(withsmartInput). They are passed through unchanged torunSkillon the DIRECT / pipeline MAIN path.- Default parser options for all skill runs: set
templateRenderingonExellixSkillsClientOptionswhen constructingExellixSkillsClient(or configuretemplateRenderingon a gateway instance you pass asoptions.gateway). Per-call overrides usetemplateRenderOptionson the request. - Synthesis, audit, polish, and AI scoping are executed via
@exellix/xynthesis(and therefore inherit xynthesis behavior for retries/repair and diagnostics when enabled).
| Mechanism | Where | Effect |
|---|---|---|
ExellixSkillsClientOptions.templateRendering |
Client constructor | Sets gateway defaults when this package (or your app) constructs AIGateway. If you inject options.gateway, set templateRendering on that instance. |
RunTaskRequest.templateRenderOptions |
Per runTask |
Deep-merged on gateway defaults for that invoke only. |
RunTaskRequest.templateTokens |
Per runTask |
Passed through as gateway templateTokens (highest overlay priority during render). |
Types TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions, SmartInputRenderResult, RunTaskSmartInput, and GatewayTemplateTokens are re-exported from @exellix/ai-tasks (skill/gateway/Rendrix definitions). mergeSkillTemplateVariables is exported for callers that reproduce the same merge outside runTask. TaskRequestBuilder supports .withTemplateRenderOptions(...) and .withTemplateTokens(...), plus .withXynthesized, .withSmartInput, .withSmartInputPaths, .withSmartInputRenderOptions(...), .withXynthesizedJob, .withXynthesizedTask, .withXynthesizedExecution (see Xynthesized memory and smart input).
Errors: TemplateResolutionError (or codes like TEMPLATE_RESOLUTION_ERROR / TEMPLATE_VARIABLE_MISSING) means a required template path was undefined after the gateway merged memory. Fix variables / memories / input, use optional fragments ({{path \|}}) in templates, or see the v4 guide for legacy silentMissingMustTokens. Template resolution failures are not treated as missing registry content (they do not go through RegistryManager.diagnose the same way as “content not found”).
Full protocol (MUST vs optional tokens, subPathSearch, errors): GATEWAY_TEMPLATE_PROTOCOL_V4.md in @exellix/ai-skills. Nx-content layout: @x12i/ai-gateway Content Resolver — Upstream Guide.
Install
Packaged skills (recommended): depend only on @exellix/ai-tasks; it bundles the execution stack and ships .metadata templates.
npm install @exellix/ai-tasks
Custom gateway / advanced injection: add @exellix/ai-skills and construct WorexClientTasks with your own client + executor.
npm install @exellix/ai-tasks @exellix/ai-skills
Bundled x12i stack (direct dependencies of this package): @x12i/catalox (catalog reads / publish script), @x12i/env (env conventions aligned with the Exellix stack), and @x12i/logxer 4.6.0+ — createLogxer, warnCode/errorCode with shipped diagnostic catalog (.metadata/log-diagnostics.json), DebugLogAbstract, fieldEvidence, runWithLogContext (wired in runTask() via runAiTasksWithLogContext), and in-process getAiTasksJobLogs for debug tooling. Per-package thresholds: AI_TASKS_LOGS_LEVEL, ACTIVIX_LOGS_LEVEL. Older names logs-gateway and nx-config2 are not direct dependencies here.
Narrix (v5+)
From v5.0.0, the local skills/skill.local:narrixRun path and narrixInput for narrix-then-direct require a unified shape: always set medium (record | text | docs | chat), datasetId, and the payload fields for that medium. Routing uses @exellix/narrix-runner v2 (runByQuestion / runByNarrative) and @exellix/narrix-catalox for catalog hints—there is no narrix-packs-library or legacy “record job without medium” compatibility.
Migration: see BREAKING-CHANGES-NARRIX-V5.md. Live Narrix tests: npm run test:narrix:live (requires .archive/packages/… seed; see that doc).
Catalox catalogs (upstream from this package)
@exellix/ai-tasks publishes task-strategy metadata to Catalox under appId: ai-tasks, and re-exports read helpers so apps can list catalogs, descriptors, and items without adding @x12i/catalox as a direct dependency (types such as Catalox, CataloxContext, AppCatalogBootstrap are re-exported from the task-strategies surface).
AI Tasks catalogs
| Catalog ID | Role |
|---|---|
ai-task-synthesis-input-strategies (alias ai-task-strategies-pre) |
Synthesis template keys for supportTemplates.synthesisContext on the PRE step (memory scope is invokeContract.reads on nodePlan, not per-node authoring enums). |
ai-task-pre-orchestration-strategies |
PRE fan-out: plan, split. |
ai-task-post-orchestration-strategies |
POST fan-in: merge, pick-best, audit-loop. |
ai-task-strategies-post |
Post–core audit selectionStrategy (best / synthesis). |
ai-task-input-strategies |
Optional authoring hints: inputStrategyKey on RunTaskRequest (does not drive Narrix invocation). |
execution-strategy |
Preferred catalog for supported MAIN execution metadata: direct, planner, optimizer. |
ai-task-execution-strategies |
Compatibility MAIN wrappers metadata catalog: same seeded rows as execution-strategy. |
ai-task-main-execution-wrappers |
Compatibility MAIN execution-strategy catalog: same seeded rows as execution-strategy. |
ai-task-narrix-modes |
Narrix invocation: narrixMode (off | preprocessor | handler). |
Runtime does not require Catalox to be configured for these keys. Catalogs seed console metadata, and execution-strategy rows may be passed to runTask as executionStrategyCatalogItems for guarded metadata consumption. The runtime still code-validates supported strategy keys, phases, and retry rules; malformed or conflicting catalog rows are ignored. Consumption order: optional Narrix → optional web (preprocessor path) → optional Xynthesis PRE steps → MAIN.
Supported MAIN Execution Strategies
The runtime supports three MAIN execution strategy records:
| Strategy | Request shape | Runtime behavior |
|---|---|---|
direct |
executionStrategies: [] |
Plain MAIN. Calls runSkill once. direct is catalog metadata only and is not valid as a row inside executionStrategies[]. |
planner |
{ strategyKey: "planner", phase: "before", priority } |
Runs xynthesis execution-plan before MAIN and can merge instructions, prompt, variables, and template tokens into the request. |
optimizer |
{ strategyKey: "optimizer", phase: "after", priority, maxIterations? } |
Runs xynthesis execution-evaluate-result after MAIN. If not satisfied, retries MAIN with feedback until satisfied or maxIterations is reached. |
planner must use phase: "before" and optimizer must use phase: "after". optimizer.maxIterations controls MAIN attempts; when omitted it falls back to AI_TASKS_OPTIMIZER_MAX_ITERATIONS or the package default. Per-invocation args.functionId always wins over catalog metadata and code defaults.
execution-strategy catalog items are structured metadata, not an open behavior plug-in. Safe runtime consumption is currently limited to fields explicitly marked in safeRuntimeFields, such as defaultSidekickAction / runtimeKind: "xynthesis-action", and only when the catalog row matches the already-validated strategy key, phase, and request shape.
Legacy monolith id ai-task-strategies is no longer written by this repo; use the catalogs above.
Publishing Firestore metadata
Note: Task-strategy catalog publishing still uses Firebase / Firestore (
@x12i/catalox/firebase) until the Catalox 6 rollout completes for@exellix/catalox-ai-skills.
From a clone of this repo, with Firebase env in .env as required by createCataloxFromEnv() (see @exellix/ai-skills): FIREBASE_PROJECT_ID (required), GOOGLE_SERVICE_ACCOUNT_BASE64 (required: service account JSON, base64-encoded), and optional FIRESTORE_DATABASE_ID:
npm run publish:task-strategies
That provisions apps/ai-tasks, all six native catalogs, bindings, descriptors, and canonical items.
Reading catalogs and items (from @exellix/ai-tasks)
Construct a Catalox instance (for example createCataloxFromEnv() from @exellix/ai-tasks, which re-exports it from @exellix/ai-skills), then use any of:
| Export | Purpose |
|---|---|
defaultAiTasksCataloxContext() |
Build CataloxContext with appId: "ai-tasks". |
listAiTasksAppCatalogs(catalox, context?) |
Discovery list: catalog id, label, access, source mode. |
getAiTasksAppCatalogBootstrap(catalox, context?) |
All descriptors visible to the app (identity, query fields, capabilities). |
getAiTasksTaskStrategiesCatalogDescriptor(catalox, catalogId, context?) |
One catalog’s descriptor (any of the six catalog ids above). |
listPreCoreTaskStrategies / listPostCoreTaskStrategies |
Normalized rows for pre / post synthesis–audit catalogs. |
listInputStrategies / listExecutionStrategyCatalogItems / listExecutionStrategies / listMainExecutionWrappers / listNarrixModes |
Normalized rows for input, preferred execution metadata, compatibility execution catalogs, MAIN wrappers, and Narrix mode catalogs. |
getAiTasksTaskStrategiesCatalogSnapshot(catalox, context?, query?) |
All seven task-strategy catalogs in one round-trip (pre, post, input, executionStrategy, execution, mainExecutionWrappers, narrixModes). |
Raw rows (full UnifiedCatalogItem) if you need them:
import {
createCataloxFromEnv,
defaultAiTasksCataloxContext,
AI_TASK_PRE_STRATEGIES_CATALOG_ID,
} from "@exellix/ai-tasks";
const catalox = createCataloxFromEnv();
const ctx = defaultAiTasksCataloxContext();
const { listOutcome, items } = await catalox.listCatalogItems(
ctx,
AI_TASK_PRE_STRATEGIES_CATALOG_ID,
{ limit: 100 }
);
Skill templates (ai-skills app)
Skill definitions and template bodies for execution live under Catalox appId: ai-skills (owned by @exellix/ai-skills). This package re-exports the Catalox skill surface from @exellix/ai-skills via the same entrypoint (e.g. listAiSkillsCatalogItems, fetchSkillTemplatesFromCatalox, createCataloxFromEnv, initFirebaseAdminFromEnv, …) so you can view and edit skill catalog items while depending only on @exellix/ai-tasks.
Usage
Packaged skills (default client)
import {
createDefaultWorexClientTasks,
ExecutionType,
} from "@exellix/ai-tasks";
const client = createDefaultWorexClientTasks();
// Optional before first runTask if nx-content needs time to connect:
await client.whenReady({ timeoutMs: 60_000 });
const result = await client.runTask({
skillKey: "tasks/your-skill",
agentId: "my-agent",
jobTypeId: "my-job-type",
taskTypeId: "my-task-type",
executionStrategies: [],
executionType: ExecutionType.DIRECT,
input: { question: "..." },
jobContext: { graphRunId: "..." },
});
await client.dispose();
Utility tasks (non-question tasks)
ai-tasks supports utility tasks: tasks that run from structured inputs and memory/context to produce structured, machine-consumable output, without requiring a user-facing question.
- Contract:
RunTaskRequest.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.missingSignals
getPackagedAiTasksMetadataDir() returns the absolute path to the shipped .metadata folder (override with contentRegistryLocalPath in client options). forPackagedSkills is an alias of createDefaultWorexClientTasks.
Class-Based API (custom ai-skills client)
import { ExellixSkillsClient, WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
// `WorexClientSkills` is a type alias for `ExellixSkillsClient`
// skills client is the shared execution + helper layer
const skills = new ExellixSkillsClient({
// ... your existing ai-skills config (gateway/provider/router)
});
// Get executor from skills client (no-enrichment execute primitive)
const executor = skills.getExecutor();
// tasks client orchestrates task-level execution
const tasks = new WorexClientTasks(skills, executor);
const res = await tasks.runTask({
skillKey: "tasks/security-risk-summary",
// executionType is optional, defaults to ExecutionType.DIRECT
input: { assetId: "a-123", windowDays: 30 },
// optional
variables: { orgName: "Acme" },
modelConfig: {
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "openai/gpt-5",
temperature: 0.7,
topP: 0.9,
},
jobId: "job-1",
agentId: "agent-1",
});
console.log(res.rawContent);
console.log(res.parsed); // if your pipeline supports parsed output
Function-Based API (Convenience)
import { runTask, ExecutionType } from "@exellix/ai-tasks";
// Automatically initializes SDK (ERC mode)
// executionType is optional, defaults to ExecutionType.DIRECT
const res = await runTask({
skillKey: "tasks/security-risk-summary",
agentId: "agent-1",
jobTypeId: "security-job-type",
taskTypeId: "security-task-type",
executionStrategies: [],
input: { assetId: "a-123" },
jobId: "job-1",
});
RunTask Algorithm (Canonical)
Given a request, runTask() performs:
- Validate
nodePlan(graphenix.node-plan/v2.1) and reject forbidden wire fields. See RUNTASK_REQUEST.md. - Local task dispatch: If
getLocalTask(skillKey)returns a handler, run it and return (no LLM MAIN). - Node execution:
executeNodeFromPlanloops orderedexecutionUnits(PRE utilities, MAIN skill, POST utilities), appendingexecutionTraceevents. - Finalize response: Attach observability, xynthesized patches, and trace metadata as configured.
Legacy steps involving root executionPipeline, executionType, or runtime taskConfiguration compile are not executed in 10.x.
Trace mode (authoritative ordered debug trace)
Set executionMode: "trace" to opt in to an authoritative, ordered per-step execution trace. When enabled, the response includes:
debugTrace.tasks: DebugTraceTask[]
Each entry represents a real unit of work that executed (pre steps, main skills/gateway call, post steps) and includes:
taskType:"pre-execution" | "ai-task" | "post-execution"details: short human stringmodelUsed: string for LLM-backed steps;nullfor deterministic stepsmetadata.timing:{ startedAt, endedAt, durationMs }metadata.step:{ phase, type, stepId }(when applicable)
When upstream providers expose them (via @exellix/xynthesis / @exellix/ai-skills trace surfaces), trace tasks may also include usage/routing/cost under stable metadata keys.
Graph / smart-input hints (ai-tasks–specific): Trace entries for MAIN direct execution include metadata.smartInput: { paths } (echo of RunTaskRequest.smartInput.paths after normalization—{ title, path }[], or empty array). PRE synthesized-context entries may include metadata.xynthesized (inputPathsUsed, outputDestination, outputKey, patchKeys) when SynthesisConfig.xynthesizedOutput is set—keys only, not full synthesized payloads.
Xynthesized memory and smart input
Graph runtimes need structured execution context that is not the same as raw jobMemory / taskMemory / executionMemory: material synthesized once at job scope, scoped to a single node, or held in a run-wide execution bucket. RunTaskRequest supports:
| Field | Purpose |
|---|---|
xynthesized?: { job?, task?, execution? } |
job — synthesized context reusable across nodes in the same graph run (graph-engine merges patches from each runTask). task — scoped to this task/node invocation only. execution — run-wide execution bucket (distinct from job/task). Values are Record<string, unknown> buckets keyed by your outputKey (and any caller-supplied keys). |
smartInput?: RunTaskSmartInput |
Declares smart-input paths for Rendrix {{smartInput}} (via @exellix/ai-skills / gateway). Use Rendrix-native paths: { title, path, required? }[], or legacy paths: string[] (normalized before runSkill). Paths under xynthesized.* must use job, task, or execution (e.g. xynthesized.execution.priorAnalysis). The smartInput object allows only the paths property at the root. Optional smartInputRenderOptions on RunTaskRequest controls markdown folding for the macro (same merge rules as RunSkillRequest). Full dot-path resolution against live memory happens downstream (Rendrix / gateway). |
Validation (smartInput)
If smartInput is present, it must be a plain object with paths (array of non-empty strings or { title, path, required? }) and optional strict (boolean). Invalid: smartInput: {}, bad paths, extra root keys, or xynthesized.<scope>.* with <scope> not job, task, or execution. Prefer path prefixes jobVariables.* and taskVariables.* (legacy variables.* ≡ job scope when the engine resolves paths). { paths: [] } is valid.
At runTask() entry, invalid shape throws SmartInputValidationError (error.code, error.details, error.skillKey) — the task does not run (no NARRIX, no PRE xynthesis, no MAIN). Omitted smartInput is allowed.
Pre-flight validation (no LLM invoke)
Use these helpers before runTask() to inspect config and payload without calling xynthesis or the gateway:
| Function | Purpose |
|---|---|
validateRunTaskConfig(request) |
Static checks: agentId / jobTypeId / taskTypeId, executionStrategies, smartInput shape, modelConfig / llmCall numeric fields, executionPipeline (one MAIN, synthesis PRE context flag). Returns { ok, issues, errors, warnings }. |
validateRunTaskInvoke({ request, templateResolver?, ... }) |
Config checks plus whether expected paths resolve on the request (input, memories, xynthesized). Uses Rendrix renderSmartInput for required smart-input paths and optional analyzeTemplateResolution on raw templates. |
analyzeExpectedRunTaskInput({ skillKey, smartInput?, instructions?, prompt?, templateResolver? }) |
Returns expected dot-paths from smartInput.paths and path tokens in skill templates (Rendrix listTokens). |
import {
validateRunTaskConfig,
validateRunTaskInvoke,
analyzeExpectedRunTaskInput,
SmartInputValidationError,
isSmartInputValidationError,
} from "@exellix/ai-tasks";
// Config only (fast)
const configCheck = validateRunTaskConfig(runTaskRequest);
if (!configCheck.ok) {
for (const issue of configCheck.errors) {
console.error(issue.code, issue.path, issue.message);
}
}
// Config + payload + templates
const invokeCheck = await validateRunTaskInvoke({
request: runTaskRequest,
templateResolver: {
async resolveRawTemplate(skillKey, section) {
const r = await skillsClient.resolveRawTemplate(skillKey, section);
return r?.found ? r.content : undefined;
},
},
checkTemplateResolution: true,
});
if (!invokeCheck.ok) {
console.error(invokeCheck.errors);
}
// What paths does this node need?
const expected = await analyzeExpectedRunTaskInput({
skillKey: runTaskRequest.skillKey,
smartInput: runTaskRequest.smartInput,
prompt: "...",
});
Each issue has code, severity (error | warning), message, and optional path (e.g. smartInput.paths[0].path, modelConfig.preActionModel). See .docs/flow-io/ for flow-level wiring.
Skill catalog helpers (runtime re-exports)
@exellix/ai-tasks re-exports Catalox-backed catalog and execution helpers from @exellix/ai-skills so orchestrators can depend on one package for runTask plus template maintenance:
| Export | Purpose |
|---|---|
extractTokenNamesFromStrings, getSkillTokens, getSkillContent, modifySkillContent, createSubSkill, deleteSubSkill, SKILL_TEMPLATE_ROLES_ORDER |
Template catalog introspection and edits (Catalox). |
runSkillRequestFromFlat, resolveAiEngineIdForRunSkill, SkillExecutionTraceError, trace helpers |
Flat skill execution and trace utilities. |
Re-consider direct functions (not runTask)
Revision skills are direct skill wrappers from @exellix/ai-skills, re-exported on @exellix/ai-tasks for orchestrators that already depend on the task SDK. Do not invoke these via runTask — call them on ExellixSkillsClient or bind the standalone functions with a runSkill implementation.
| Export | Skill key | Result |
|---|---|---|
fillReConsiderationGap |
RECONSIDER_FILL_GAP_SKILL_KEY (re-consider.fill-gap) |
{ gapOutput } |
reviewReConsiderationResponse |
RECONSIDER_REVIEW_SKILL_KEY |
{ reviewStatus, reviewOutput } |
rewriteReConsiderationScope |
RECONSIDER_REWRITE_SCOPE_SKILL_KEY |
{ correctedScope } |
rewriteReConsiderationFull |
RECONSIDER_REWRITE_FULL_SKILL_KEY |
{ correctedAnswer } |
calibrateReConsiderationDecision |
RECONSIDER_CALIBRATE_DECISION_SKILL_KEY |
{ calibratedOutput } |
Also exported: stringifyForPrompt, the five build*ReConsideration*RunSkillRequest builders, and types such as ReConsiderationRuntime, ReviewStatus, and ReConsiderationRunSkill. Shared runtime fields: jobId, taskId, agentId, jobTypeId, taskTypeId, modelConfig. Callers own merge/apply of returned fragments. See @exellix/ai-skills README for token roots and examples.
import {
ExellixSkillsClient,
fillReConsiderationGap,
RECONSIDER_FILL_GAP_SKILL_KEY,
} from "@exellix/ai-tasks";
const client = new ExellixSkillsClient(/* … */);
const result = await client.fillReConsiderationGap(
{
input: "What are the risks?",
originalInstructions: "Answer with evidence.",
originalResponse: priorAnswer,
gapExplained: "Missing evidence for Section B.",
},
{
jobId: "job-1",
taskId: "task-1",
agentId: "agent-1",
jobTypeId: "job-type",
taskTypeId: "task-type",
modelConfig: { model: "gpt-4o-mini" },
}
);
// Or: await fillReConsiderationGap.call({ runSkill: client.runSkill.bind(client) }, input, runtime);
Studio preflight (invoke packet review, markdown analysis reports, FuncX ai-skills/analyze-gateway-invoke-request) is not part of the runtime stack — use your studio / skills-manager layer. See documenations/studio-skill-invoke-preflight.md for a suggested design (requirements, former behavior, finding catalog, optional FuncX step). Also see @exellix/ai-skills SKILLS_MANAGER_CLIENT_GUIDE.md.
Rendrix helpers used by validation are also re-exported: listTokens, analyzeTemplateResolution, renderSmartInput, SmartInputInvalidError, TemplateResolutionError, and related types.
Runtime surfaces
- Narrix preprocessor / Narrix handler
ctx: Samexynthesized/smartInputas on therunTaskrequest after normalization (pipeline PRE runs later and can mutatexynthesizedbefore MAIN). - PRE
synthesized-context: The bundle passed into template/rendering includesxynthesizedandsmartInputso synthesis prompts can reference them consistently with MAIN. - MAIN executor payload: Top-level
xynthesized,smartInput(normalized), andsmartInputRenderOptions(when set) are forwarded on the object passed torunSkill(alongside memories andcontext). - Template variables:
variables(job bucket) andexecutionMemory.jobVariables/executionMemory.taskVariablesare forwarded separately; use{{xynthesized…}},{{smartInput}},jobVariables.*, andtaskVariables.*in templates (not a single flattened bag). - Response: When downstream returns a
SmartInputRenderResult,runTaskfinalizeexposesresponse.smartInputRenderResultand mirrorsmetadata.smartInputRenderResult(also readsmetadata.smartInputwhen it matches that shape).
PRE synthesis destination (SynthesisConfig.xynthesizedOutput)
Optional on the synthesized-context step config:
destination:"job"|"task"|"execution"— writes underrequest.xynthesized.job[outputKey],.task[outputKey], or.execution[outputKey](no redirect to other scopes).outputKey: string key for the synthesized value.mode:"replace"(default) or"merge"— for merge, if the existing value atoutputKeyand the new value are both plain objects, ai-tasks deep-merges them; otherwise the new value replaces.alsoWriteLegacySynthesizedContext: Whentrueor omitted, keep today’s behavior: persist structured/markdown artifacts onexecutionMemory.synthesizedContext(andsynthesizedInputwhen applicable) for MAIN. Whenfalse, skip that legacy persistence for this PRE step; MAIN still receives synthesized context markdown from the PRE step when produced.
Stored value: Prefer the full synthesis artifact when present (structured / question-driven / markdown artifact shape). If there is no artifact, ai-tasks stores { contextMarkdown } using the PRE step’s markdown string.
Response: RunTaskResponse.xynthesizedPatch has the same { job?, task?, execution? } shape so the graph-engine can merge into durable graph memory. Omitted when nothing was written.
Execution-scope example (PRE + smart-input):
executionPipeline: [
{
phase: "pre",
type: "synthesized-context",
config: {
xynthesizedOutput: { destination: "execution", outputKey: "priorAnalysis" },
},
},
{ phase: "main", type: "direct" },
],
smartInput: { paths: ["xynthesized.execution.priorAnalysis"] },
// response.xynthesizedPatch.execution.priorAnalysis → graph-engine merges into executionMemory.xynthesized.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 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; // EvidenceBundle
node.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/executor
registerLocalTask(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 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 onexecutionMemoryfor the attachment; default"_narrix".narrix.engineConfigPath,narrix.packsRoot,narrix.deterministicSort,narrix.assumptionsPolicy: reserved for future use.
NARRIX builds the _narrix CNI attachment only. It does not trigger or configure web scoping — for web context add a standalone webScope PRE unit (below).
Web scoping and upstream packages
Web scoping is implemented by @exellix/web-scope, which wraps @x12i/web-queries (query planning + WebContext mapping) through @x12i/web-scoper-memorix (TTL caching + synthesized-knowledge persistence) and @x12i/search-adapter (Tavily normalization).
The only trigger is a PRE unit with strategyKey: "webScope". Its unitParams:
webQueryTemplate(required when enabled): Rendrix string rendered into the search query, e.g."How to solve {{input.vulnerability}}?".webQueryTemplates(optional):string[]→ each rendered into asearchManypack.enableWebScope(defaulttrue): on/off switch.options:WebScopeOptions(maxQueries,freshnessDays, domain filters,queryTemplates, …).
Requires TAVILY_API_KEY for live search. The rendered template is validated at config time (WEB_QUERY_TEMPLATE_REQUIRED / WEB_QUERY_TEMPLATE_UNRESOLVED); there is no input.question fallback. Playground input is record/facts only, referenced by the template via {{input.*}}.
Each WebContext.sources[] entry includes url, domain, snippet, retrievalStage, and meta.discoveredCount. Snippet/fetch behavior follows WebScoperConfig defaults in @x12i/web-queries (see that package README for caps and page fetch). Successful webScope units also trigger forced web synthesis and optional Memorix persistence (MONGO_URI, EXELLIX_NAME, TTL vars).
Downstream LLM context: On the DIRECT path with includeContextInPrompt, when Narrix output is in play, ai-tasks appends a markdown section from executionMemory.webContext (when ok === true) so the model sees source bodies, not only NARRIX signal codes. The synthesized-context PRE step uses the same markdown builder for policies that include web (e.g. narrix-web, narrix-web-memory, memory-web, auto when a web hit exists). Optional env WEB_CONTEXT_MARKDOWN_MAX_CHARS and optional SynthesisConfig.webEvidence cap and shape that markdown. Serialized memory for synthesis never embeds raw webContext as JSON; web appears as markdown only when the policy includes web.
Technical flow, failure behavior, and consumption patterns: documenations/web-scoping-in-ai-tasks.md.
Raw record resolution
The pre-processor needs a raw record to enrich. Resolution order:
executionMemory.input.rawexecutionMemory.inputs.<first key>.rawjobMemory.recordorjobMemory.currentRecordinput.recordorinput.raw
If no record is found, runTask throws with a clear error. sourceMeta is taken from executionMemory.input.sourceMeta or jobMemory.sourceMeta or {}.
Where the result goes
- Canonical:
executionMemory[attachToField](defaultexecutionMemory._narrix) — shape{ scoping: { stories, signals }, discovery: { stories, signals }, meta }. - Mirrored:
jobMemory._narrix— same attachment so handlers can read from eitherctx.executionMemory._narrixorctx.jobMemory._narrix. - Web scope: when a
webScopePRE unit runs,executionMemory.webContextis set to theWebScopeResult/WebScopePackResultfrom@exellix/web-scope(ok: truehit or structured miss/error). Successful scoping also runs web synthesis intoexecutionMemory.synthesizedContext. Independent of_narrix.
Local task handlers receive the enriched ctx. On the DIRECT path, context is added only when includeContextInPrompt === true. When NARRIX is in play, that context is the "## Scoping and discovery" section from executionMemory._narrix when present, plus a Web sources (primary evidence) section when web scoping returned webContext.ok === true (see above). We do not inject a "Narrix Pre-Processor" placeholder. The context generator (xontext) is not given _narrix when NARRIX is in play, so it cannot emit that section.
Per-node enrichment
Each runTask call with narrix set is independent. If a graph runs two nodes with narrix config, each gets its own NARRIX run (no shared state). Orchestrators (e.g. graph-engine) lift node.taskConfiguration.narrix to RunTaskRequest.narrix.
Example
await runTask({
skillKey: "tasks/assess-reachability",
narrix: { datasetId: "network.egress.subnets" },
executionMemory: { input: { raw: subnetRecord } },
input: { question: "Assess reachability based on scoping." },
});
// Handler or LLM sees ctx.executionMemory._narrix (scoping, discovery, meta)
Intermediate steps (multi-step tasks)
When a task runs multiple logical steps in one invocation (e.g. a combined node that does "to-cni + enrich + triage"), the response can include an optional intermediateSteps array so consumers can inspect and report the chain without extra round-trips.
Response shape
intermediateSteps(optional): array of steps in execution order. Each step has:step(number): 1-based orderid(string): stable identifier (e.g."to-cni","engine-enrich","triage-q1-q6")ok(boolean): whether the step succeededsummary(optional): short description (e.g."cni built","enriched")error(optional): error message whenokis falseinputSummary(optional): summary input for reportingoutputExcerpt(optional): small excerpt of step output for debugging
Skills/tasks can return steps inside parsed (e.g. parsed.intermediateSteps); the SDK lifts them to the top-level response.intermediateSteps so consumers always read from the same place. Local tasks can return { output: {...}, intermediateSteps: [...] }; 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 artifact toexecutionMemory.synthesizedContextwithmode: "questionDriven"and stable access pathexecutionMemory.synthesizedContext.answers.<id>.synthesis. Web-only questions degrade gracefully whenexecutionMemory.webContextis unavailable (answer is present but marked unknown + reason). - Structured synthesis (opt-in): Set
synthesisMode: "structured"(or legacy-compatiblesynthesisOutputFormat: "structured") so the synthesis model returns JSON matchingSynthesizedPromptPayload(SynthesisInput,SynthesizedContext,SynthesizedItem, etc., exported from@exellix/ai-tasks). The package validates the shape, then builds main-stepcontextmarkdown viabuildSynthesizedContextMarkdown(task cores → question → local → supporting → unknowns → assumptions). Raw local vs web blocks are passed aslocal_raw/supporting_rawin the synthesis prompt (seeresolveSourceMaterialParts). The task question is taken fromgetQuestion(request), elsequestionPathonrequest.input(default pathquestion), else the rendered downstream user prompt (truncated). In structured mode, template core tokens are required in template content; no detected template cores is treated as a template-definition error. Structured artifacts are persisted atexecutionMemory.synthesizedContext. Templates:templates/synthesis/system-structured.mdanduser-structured.txt(override path stillSYNTHESIS_TEMPLATES_PATH). For tests or custom execution, usesetContextSynthesizer/getContextSynthesizer;runStructuredSynthesisGatewayCallmirrors the default gateway path. - Synthesis uses a real LLM call (
AIGateway.invokevia xynthesisexecuteXynthesisAction) — not just string merging. Buildingsource_materialis deterministic; the PRE step then invokes the synthesis model (e.g.SYNTHESIS_MODEL/modelConfig) before the MAIN step runs. See documenations/synthesis-invocation-notes.md for details. webEvidence: Optional tuning for the Web sources markdown block inside synthesis source material:preferCleanContent(defaulttrue),maxSources(default5),dedupeByUrl(defaulttrue),maxTotalChars. EnvWEB_CONTEXT_MARKDOWN_MAX_CHARSstill applies whenmaxTotalCharsis not set.contextSourcePolicy(what becomes<source_material>in the synthesis prompt):
| Policy | Narrix section | Web markdown | Memory JSON |
|---|---|---|---|
narrix-web |
Yes (required) | If webContext hit |
No |
narrix-web-memory |
If present | If hit | Yes — never embeds raw webContext in JSON |
memory-web |
Stripped from JSON | If hit | Yes |
memory-only |
No | No | Yes — never embeds raw webContext in JSON |
auto |
Resolves at runtime |
auto resolution: narrix-web-memory if Narrix output exists (executionMemory[attachToField] or coerced taskMemory.narrix); else memory-web if web scoping produced a usable hit; else memory-only. Legacy aliases: narrix-only → narrix-web, narrix+memory → narrix-web-memory.
Synthesis output shape (default): templates/synthesis/system.md asks the weak model for three markdown sections in order: ## Local context (primary), ## Web evidence (supporting), ## Supporting sources (optional) (short traceability lines only). Override with synthesisPromptOverride if needed. With synthesisOutputFormat: "structured", output is JSON validated to the generic synthesis contract, then converted to markdown for the main step only.
Migration note: If you relied on auto with Narrix but without memory in the synthesizer input, set contextSourcePolicy: "narrix-web" explicitly. See BREAKING-CHANGES.md (Synthesis contextSourcePolicy and auto).
- Templates: Synthesis prompts live in templates/synthesis/ — markdown mode:
system.md,user.txt; structured mode:system-structured.md,user-structured.txt. Optional envSYNTHESIS_TEMPLATES_PATHfor base path. - Caching: Optional synthesis result caching via nx-cache (same project) can be used to reduce cost for batch or repeated runs.
- Non-streaming: Synthesis returns a single complete response; there is no streaming support for the synthesis step.
- Builder:
withExecutionPipeline(steps),withSynthesizedContextPreStep(modelOrConfig?)to add the PRE step and setincludeContextInPrompt: true(pass aSynthesisConfigobject forcontextSourcePolicy,webEvidence,memoryPaths,xynthesizedOutputwithdestination: "job" | "task" | "execution", etc.). AlsowithXynthesized,withSmartInput,withSmartInputPaths,withSmartInputRenderOptions,withXynthesizedJob,withXynthesizedTask,withXynthesizedExecutionfor request-level fields. - Auditability fields in response metadata:
synthesisEnabled,effectiveExecutionPipeline,synthesizedContextPresent,mainContextSource,detectedTemplateCores. Final response also includes normalizedexecutionMemoryandexecutionState.executionMemory.
Minimal example (explicit policy + web tuning):
import { SYNTHESIZED_CONTEXT } from "@exellix/ai-tasks";
executionPipeline: [
{
phase: "pre",
type: SYNTHESIZED_CONTEXT,
config: {
contextSourcePolicy: "narrix-web-memory",
webEvidence: { maxSources: 3, preferCleanContent: true },
modelConfig: { model: "gpt-5-nano" },
},
},
{ phase: "main", type: "direct" },
],
includeContextInPrompt: true,
POST Step Strategies (post-execution)
The execution pipeline supports POST steps that run after the MAIN step and operate on its response. Two built-in strategies: audit (quality-gate loop with re-runs) and polish (refinement pass against a prioritized checklist). POST steps run in pipeline order; each step’s output becomes the next step’s input.
Pipeline example:
executionPipeline: [
{ phase: "pre", type: "synthesized-context", config: { /* ... */ } },
{ phase: "main", type: "direct" },
{ phase: "post", type: "audit", config: { gateway: { must: [...], should: [...] }, maxCycles: 3 } },
{ phase: "post", type: "polish", config: { checklist: [...], maxPasses: 2 } },
]
Metadata for each POST step is under response.metadata.postSteps.<type>. All POST steps’ intermediate steps are merged into response.intermediateSteps with continuous numbering (e.g. audit-cycle-1, audit-synthesis, audit-select, polish-pass-1).
Fact-guard (optional sub-action integrity)
Optional atom integrity checks for sub-actions only (PRE synthesis, POST audit/polish/scoping, execution-strategy planner/optimizer). MAIN runSkill is never guarded. Requires @exellix/fact-guard-* ^1.0.0 (transitive via this package).
| When | Behavior |
|---|---|
factGuard absent or enabled: false |
Identical to pre–fact-guard behavior |
FACT_GUARD_DISABLED=1 |
Emergency kill-switch — all guards bypassed |
Config attachment:
| Step | Field |
|---|---|
PRE synthesized-context |
SynthesisConfig.factGuard |
POST audit |
AuditConfig.factGuard.audit / .synthesis (merge) |
POST polish |
PolishConfig.factGuard |
| AI scoping | RunTaskRequest.aiScopingOptions.factGuard |
| Execution strategy | ExecutionStrategyInvocation.factGuard |
{
"enabled": true,
"policyId": "prompt-context-synthesizer",
"onViolation": "fallback",
"maxRetries": 2,
"onExhausted": "fallback"
}
Violations appear in structured logs (fact_guard.sub_action) and optionally on response.metadata.factGuard[stage] (trace mode). See documenations/synthesized-context-guide.md and ../fact-guard/README.md.
Tests: npm run test:fact-guard (mocked LLM; no API key required).
POST step: audit
Quality-gate loop: evaluate MAIN output against must and should checks, get actionable feedback, re-run the MAIN step when criteria aren’t met, then choose the best result (or run an optional synthesis merge of the top two candidates).
- Config:
AuditConfig:gateway.must,gateway.should(each{ check, weight }),threshold(default 80),minCycles,maxCycles(default 3),selectionStrategy("best"|"synthesis"),auditModelConfig,synthesisModelConfig,customAuditGuidelines,customSynthesisGuidelines,fallbackToBest(default true),auditTimeoutMs. - Output: Audit uses structured Markdown (Flex-MD style); the runtime parses it into checks and overall feedback and computes
weightedScore. - Re-runs: When the loop continues, the MAIN step is re-invoked with the original context plus a feedback block from
templates/post-steps/audit/feedback-injection.md. - Metadata:
response.metadata.postSteps.audit:totalCycles,candidateScores,selectedCycleIndex,allMustPassed,synthesisUsed,auditResults,durationMs. - Caching: Audit (and synthesis) LLM calls use nx-cache when inputs and config are unchanged (no cache on re-runs with feedback).
- Failure: If all cycles are exhausted and no candidate passes all
mustchecks: withfallbackToBest: true(default) the best-scored candidate is returned andallMustPassed: false; withfallbackToBest: falsean error is thrown.
POST step: polish
Single- or multi-pass refinement against a prioritized checklist. Does not re-run MAIN; always produces a refined output (or falls back to the last good output on failure).
- Config:
PolishConfig:checklist(each{ instruction, priority: "high" | "medium" | "low" }),maxPasses(default 1),modelConfig,customGuidelines,includeOriginalContext,timeoutMs. - Output: Polish LLM returns JSON:
{ polishedOutput, changeNotes[] }. - Metadata:
response.metadata.postSteps.polish:totalPasses,changeNotes(per pass),durationMs. - Failure: Polish never throws; on LLM/parse failure it returns the previous output and records
ok: falsein the step.
Minimal examples
Audit only:
const result = await runTask({
skillKey: "tasks/security-risk-summary",
input: { assetId: "a-123" },
jobId: "job-1",
agentId: "agent-1",
executionPipeline: [
{ phase: "main", type: "direct" },
{
phase: "post",
type: "audit",
config: {
gateway: {
must: [{ check: "Output includes a severity rating (critical/high/medium/low)", weight: 10 }],
should: [{ check: "Recommendations are ordered by priority", weight: 5 }],
},
threshold: 80,
maxCycles: 3,
},
},
],
});
console.log(result.parsed); // best candidate output
console.log(result.metadata.postSteps?.audit); // totalCycles, candidateScores, allMustPassed, etc.
Polish only:
const result = await runTask({
skillKey: "tasks/executive-summary",
input: { report: "..." },
executionPipeline: [
{ phase: "main", type: "direct" },
{
phase: "post",
type: "polish",
config: {
checklist: [
{ instruction: "Remove jargon; use plain language", priority: "high" },
{ instruction: "Each paragraph ≤ 3 sentences", priority: "medium" },
],
maxPasses: 2,
},
},
],
});
Chaining audit + polish
Common pattern: audit first to ensure quality, then polish to refine the approved candidate. Audit’s chosen output becomes polish’s input.
Builder
withAuditPostStep(config: AuditConfig)— Appends an audit POST step; ensures pipeline has a MAIN step.withPolishPostStep(config: PolishConfig)— Appends a polish POST step; ensures pipeline has a MAIN step.
Example with builder (no need to set executionPipeline manually):
const request = new TaskRequestBuilder()
.withSkillKey("tasks/security-risk-summary")
.withInput({ assetId: "a-123" })
.withJobId("job-1")
.withAgentId("agent-1")
.withAuditPostStep({
gateway: {
must: [{ check: "Output includes severity rating", weight: 10 }],
should: [{ check: "Recommendations ordered by priority", weight: 5 }],
},
threshold: 80,
maxCycles: 3,
})
.withPolishPostStep({
checklist: [
{ instruction: "Use active voice throughout", priority: "high" },
{ instruction: "Remove redundant qualifiers", priority: "medium" },
],
})
.build();
const result = await runTask(request);
Template paths
- Audit:
templates/post-steps/audit/(system.md,user.txt,feedback-injection.md,synthesis.md). Override:AUDIT_TEMPLATES_PATH(base path for the audit folder). - Polish:
templates/post-steps/polish/(system.md,user.txt). Override:POLISH_TEMPLATES_PATH.
Templates use Handlebars ({{variable}}, {{#each}}, {{#if}}).
Design decisions
- Audit re-runs MAIN (not the audit model): The MAIN step has the full prompt and skill context; re-running it with audit feedback yields better corrections.
- Polish does not re-run MAIN: Polish is surface-level (tone, format); a dedicated rewrite step is cheaper and sufficient.
- Audit output is Markdown (Flex-MD), not forced JSON: More robust across models; runtime parses structured sections.
- nx-cache for audit/synthesis: Reduces cost for repeated or batch runs with identical inputs.
- No global cost guardrails (e.g.
maxTotalPostCalls) in this release; control viamaxCyclesandmaxPasses. - No streaming for POST steps; final output is returned after all POST steps complete.
Testing
POST steps are covered by unit and integration tests in test/post-steps/:
- resolvePostStepConfig.test.ts — model and timeout resolution (config, env, fallback).
- parseAuditOutput.test.ts — parsing audit structured markdown into checks and overall feedback.
- parsePolishOutput.test.ts — parsing polish JSON (including code-fenced) into
polishedOutputandchangeNotes. - runAuditPostStep.test.ts — audit step with mocked gateway: passing/failing markdown, fallback, and audit-call failure.
- runPolishPostStep.test.ts — polish step with mocked gateway: valid JSON, call failure fallback, multiple passes.
- pipeline-post-steps.test.ts — pipeline integration:
runTaskwithexecutionPipeline(main + audit, main + polish) and mocked executor/gateways; assertsmetadata.postStepsand final output.
Run post-steps tests: npm test (includes dist-test/test/post-steps/*.js).
Types
Use RunTaskResponseWithSteps<TParsed> when you expect steps; it extends RunSkillResponse with intermediateSteps?: IntermediateStep[]. When POST steps run, response.metadata.postSteps is set (audit?: AuditPostStepMetadata, polish?: PolishPostStepMetadata). Exported POST step types: AuditConfig, AuditCheck, AuditResult, AuditCheckResult, AuditLLMOutput, AuditPostStepMetadata, PolishConfig, PolishChecklistItem, PolishLLMOutput, PolishPostStepMetadata, RunTaskResponsePostStepsMetadata. See the Types section and documenations/intermediate-steps.md for full details.
How completion budgets work (8.4+)
Orchestrators send model + sampling + reasoningEffort + (for xynthesis) outputExpectation. They do not send completion token counts.
| Phase | Package | Caller sends | Package owns |
|---|---|---|---|
| MAIN | @exellix/ai-skills ≥6.4 |
modelConfig → { model, temperature, topP, reasoningEffort, … } |
Optimixer + Catalox skill catalog (maxTokens on ModelConfig is rejected); gateway @x12i/ai-gateway ≥10.1 supplies wire maxTokens and authoritative costUsd / costStatus on invoke metadata; one automatic re-invoke when output hits token limit and Optimixer returns retryPrediction |
| PRE / POST / scoping / utility | @exellix/xynthesis ≥4.5 |
outputExpectation (required on every hop), optional reasoningEffort, temperature, topP |
resolveEffectiveMaxTokens inside executeXynthesisAction (default @x12i/funcx invoker); authoritative billing from funcx/gateway engines — xynthesis is pass-through only; same one-shot max-token retry via Optimixer retryPrediction |
Xynthesis outputExpectation is sizing/density intent for Optimixer — not a literal token count. ai-tasks resolves a per-stage default when the caller omits it (resolveOutputExpectation / resolveLlmOutputExpectationForXynthesis).
Rejected on the wire (validation): modelConfig.maxTokens, llmCall.maxTokens, llmCall.maxTokensCap, deprecated xynthesisModel, root modelConfig.model.
Trace: use usage.maxTokensRequested / invokeSummary diagnostics after invoke — not pre-set caps on the request.
Billing (gateway 10.x): MAIN cost is authoritative from @x12i/ai-gateway invoke metadata only — ai-tasks and ai-skills do not re-price. When costStatus === 'unpriced', tokens are recorded but there is no authoritative USD total (do not price locally). See UPGRADING_AI_GATEWAY_10.md §1.
Model profile aliases (@x12i/ai-profiles)
@exellix/ai-tasks resolves profile aliases to concrete provider model ids via @x12i/ai-profiles at execution time. This applies to both execution paths:
| Path | Slot / field | Resolved before |
|---|---|---|
| Xynthesis (PRE synthesis, POST audit/polish, AI scoping, utilities) | preActionModel, postActionModel, step llmCall.model, RunUtilityRequest.exec.model |
executeXynthesisAction / runPostStepLlmCall |
| ai-skills MAIN | skillModel, RunTaskRequest.llmCall.model |
runSkill (via DirectExecutionStrategy) |
Accepted alias shapes (@x12i/ai-profiles v2.1+):
- Profile/choice keys (required on stored
modelConfigand xynthesisllmCall.model):cheap/default,pro/default,cyber/default, … - Concrete provider wire ids on MAIN only:
openrouter/...,anthropic/claude-sonnet-4, bare vendor SKUs likegpt-4o-mini— accepted onskillModel/ MAINllmCall.modelonly
Rejected on stored config:
- Bare profile names (
cheap,pro) — usecheap/default,pro/default profile@choice(cheap@default) — useprofile/choiceslash form- Legacy graph tiers (
weak,strong), deprecatedxynthesisModel, rootmodelConfig.model
List available keys: listAiTasksProfileChoices() / listAiTasksProfileChoiceKeys() (re-exported from @x12i/ai-profiles via @exellix/ai-tasks). Same registry as xynthesis (listAIProfiles / listAIProfileChoices on @exellix/xynthesis).
Example (canonical triplet — resolved inside runTask):
await runTask({
skillKey: "tasks/analysis",
agentId: "agent-1",
jobTypeId: "analysis-job",
taskTypeId: "analysis-task",
executionStrategies: [],
input: { topic: "Q1 risks" },
modelConfig: {
preActionModel: "cheap/default", // PRE synthesis (xynthesis)
postActionModel: "cheap/default", // POST audit/polish (xynthesis)
skillModel: "openai/gpt-5", // MAIN skill (ai-skills → ai-gateway)
},
});
Concrete ids pass through unchanged on skillModel / MAIN llmCall.model. preActionModel / postActionModel and xynthesis llmCall.model must stay profile/choice aliases — concrete ids are rejected with XYNTHESIS_CONCRETE_MODEL_REJECTED.
Client impact:
skillModel/ MAINllmCall.model: profile/choice alias or concrete provider wire id.preActionModel/postActionModel: profile/choice only — graph-engine must not pre-resolve xynthesis slots to concrete ids.- Validation:
validateRunTaskConfig/resolveRunTaskModelReferencesenforce slot shapes before invoke; unknown aliases fail at resolution withXYNTHESIS_ALIAS_RESOLUTION_FAILED. - Helpers:
resolveModelReference,resolveModelReferenceForXynthesis,resolveRunTaskModelReferences,resolveInvocationPlan,isRunTaskModelResolutionError,isResolvableModelAlias(true only for knownprofile/choicekeys).
Pre-run invocation plan (resolveInvocationPlan)
Side-effect-free preview of which engine + wire model each phase will use (same routing path as execute):
import { resolveInvocationPlan } from "@exellix/ai-tasks";
const plan = await resolveInvocationPlan({
profiles: {
preActionModel: "cheap/default",
skillModel: "openai/gpt-5",
postActionModel: "cheap/default",
},
policy: {
preferOpenRouter: true, // default: read from env when omitted
openrouterApiKeyPresent: true, // fallback to vendor-direct when false
},
});
// plan.phases[] → { phase: "preAction"|"skill"|"postAction", routing, modelId, engineLabel, ... }
OpenRouter routing (preferOpenRouter): when true and an OpenRouter API key is present, MAIN (ai-gateway) and PRE/POST (funcx) resolve to different wire shapes — skill phases use openrouter/... gateway ids; xynthesis phases use Funcx API slugs. Helpers: resolvePreferOpenRouterPolicy, readPreferOpenRouterFromEnv, detectOpenRouterApiKeyPresent, resolveProfileInvocationRouting. See @exellix/ai-skills PREFER_OPENROUTER_ROUTING.md.
Resolution uses the ai-profiles registry (auto: remote refresh with bundled fallback). See MODEL-CONFIGURATION.md and BREAKING-CHANGES.md (8.4+ triplet contract).
LLM call configuration (LlmCallConfig)
LlmCallConfig is the per-stage override knob set. On MAIN it composes with RunTaskRequest.modelConfig slots (preActionModel, skillModel, postActionModel) — when both are set, llmCall.* wins.
type LlmCallConfig = {
model?: string; // provider/model id or ai-profiles alias (resolved at call time)
temperature?: number;
topP?: number;
reasoningEffort?: OptimixerReasoningEffort; // overrides Catalox default when set
maxOutputLength?: number; // post-call character truncation
timeoutMs?: number;
outputExpectation?: OutputExpectation; // xynthesis Optimixer intent (not a token count)
};
Per-stage placement:
| Stage | Where to set llmCall |
|---|---|
| MAIN skill | RunTaskRequest.llmCall (overrides modelConfig.skillModel) |
| AI scoping (per-instruction) | RunTaskRequest.aiScoping[].llmCall |
| AI scoping (request fallback) | RunTaskRequest.aiScopingOptions.llmCall |
| PRE synthesis | SynthesisConfig.llmCall (on the synthesized-context PRE step) |
| POST audit (evaluator) | AuditConfig.llmCall or AuditConfig.audit.llmCall |
| POST audit (synthesis-merge) | AuditConfig.audit.synthesis.llmCall |
| POST polish | PolishConfig.llmCall |
| Utility (xynthesis-finalize) | RunUtilityRequest.exec.{model, temperature, topP, reasoningEffort, outputExpectation, ...} |
Builder helpers (TaskRequestBuilder):
withLlmCall(llmCall)— main skill.withAiScopingOptions({ llmCall, concurrency, timeoutMsPerItem })— fallback for everyaiScoping[]item.withSynthesisLlmCall(llmCall)— PREsynthesized-contextstep.withAuditLlmCall(llmCall, role?)— POST audit step (role: "audit" | "synthesis", omit for top-level).withPolishLlmCall(llmCall)— POST polish step.withXynthesized(memory)/withSmartInput(config)/withSmartInputPaths(paths)/withSmartInputRenderOptions(opts)— graphxynthesized/smartInputcontract (see Xynthesized memory and smart input).withXynthesizedJob(key, value)/withXynthesizedTask(key, value)/withXynthesizedExecution(key, value)— merge one key intoxynthesized.job/xynthesized.task/xynthesized.execution.
Environment variables (POST steps + AI scoping): every stage reads from per-stage env vars first, then shared POST_STEP_* env vars, then the supplied fallback. Order is: config.<field> → step env → shared env → fallback.
| Env var (per stage) | Effect |
|---|---|
AUDIT_MODEL, POLISH_MODEL, SYNTHESIS_MODEL, AI_SCOPING_MODEL |
Model override for that stage |
AUDIT_TIMEOUT_MS, POLISH_TIMEOUT_MS, SYNTHESIS_TIMEOUT_MS, AI_SCOPING_TIMEOUT_MS |
Per-call timeout override |
AUDIT_TEMPERATURE, POLISH_TEMPERATURE, SYNTHESIS_TEMPERATURE |
Temperature override |
AUDIT_TOP_P, POLISH_TOP_P, SYNTHESIS_TOP_P |
Top-P override |
AUDIT_MAX_OUTPUT_LENGTH, POLISH_MAX_OUTPUT_LENGTH, SYNTHESIS_MAX_OUTPUT_LENGTH |
Post-call char truncation override |
AI_SCOPING_CONCURRENCY |
Max concurrent scoping calls (default 5) |
AI_SCOPING_TIMEOUT_MS_PER_ITEM |
Per-item timeout in ms (default 30_000) |
| Shared env var | Effect |
|---|---|
POST_STEP_MODEL |
Default model for any POST step that didn't get a specific value |
POST_STEP_TIMEOUT_MS |
Default timeout |
POST_STEP_MAX_TOKENS_CAP |
Deprecated — rejected on llmCall; Optimixer owns caps |
POST_STEP_TEMPERATURE |
Default temperature |
POST_STEP_TOP_P |
Default Top-P |
POST_STEP_MAX_OUTPUT_LENGTH |
Default char truncation |
outputExpectation is intentionally NOT read from env vars (too structured).
Trace mode observability for LLM calls
When RunTaskRequest.executionMode === "trace" (or RunUtilityRequest.exec.executionMode === "trace"), @exellix/ai-tasks populates a structured LlmCallObservation for every LLM call attempted (success or failure) and surfaces them on response metadata:
result.metadata field |
Stage | Notes |
|---|---|---|
synthesizedContextLlmCalls?: LlmCallObservation[] |
PRE synthesis (markdown / question-driven / structured) | one entry per question or branch |
aiScopingLlmCalls?: LlmCallObservation[] |
AI scoping | one entry per aiScoping[] instruction |
aiScopingFailures?: AiScopingFailureRecord[] |
AI scoping | always populated when any item failed (regardless of trace mode) — previously these were silently dropped |
postSteps.audit.llmCalls?: LlmCallObservation[] |
POST audit (evaluator + synthesis-merge) | per-cycle entries |
postSteps.polish.llmCalls?: LlmCallObservation[] |
POST polish | per-pass entries |
intermediateSteps[].observation?: LlmCallObservation |
any failed POST step | attached to the failure step so consumers can read the request/response context inline |
LlmCallObservation is a discriminated union over source: "xynthesis" | "ai-skills":
type LlmCallObservation =
| {
source: "xynthesis"; stage: LlmCallStage; stepId?: string;
request: LlmCallRequestSnapshot;
summary?: InvokeAttemptSummary; // from executeXynthesisAction on success / typed errors on failure
debugTrace?: XynthesisDebugTrace; // present when executionMode: "trace" was forwarded
durationMs: number; ok: boolean; error?: { name: string; message: string };
}
| {
source: "ai-skills"; stage: "main-skill"; stepId?: string;
request: LlmCallRequestSnapshot;
diagnostics?: RunSkillDiagnostics; // from runSkill in trace mode
durationMs: number; ok: boolean; error?: { name: string; message: string };
};
InvokeAttemptSummary (xynthesis ≥ 4.5) carries { jobId, taskId, modelRequested, modelResolved, maxTokensFromCaller, maxTokensEffective, modelUsedFromProvider, usage, routing, costUsd, costStatus, traceKind, executionMetadata }. USD cost is authoritative from @x12i/funcx — xynthesis does not calculate pricing locally.
RunSkillDiagnostics (ai-skills ≥ 6.4) carries { trace: { step, timing, routing, usage, costUsd, costStatus, modelUsed, attempts[], rawProviderPayload?, invokeRequest? }, ... }. USD cost is authoritative from @x12i/ai-gateway ≥10.1 — ai-skills normalizes gateway metadata only (mapGatewayInvokeToTrace, traceToDiagnostics re-exported from @exellix/ai-tasks). Gateway costStatus: "priced" is represented as costUsd; "unpriced" surfaces as costStatus: "unpriced". For non-trace MAIN failures, read (error as Error & { metadata?: GatewayInvokeRejectionMetadata }).metadata (re-exported from @exellix/ai-tasks) — do not scrape error strings.
Typed errors (instanceof-friendly)
Failures from xynthesis call sites are returned as upstream typed errors that already carry rich context — @exellix/ai-tasks passes them through unchanged so callers can instanceof them:
| Error class | Carries | Where |
|---|---|---|
XynthesisInvokeError |
invokeSummary, cause |
xynthesis invoke failure (PRE / POST / scoping / utility) |
XynthesisResponseParseError |
stage, invokeSummary, responseLength, responsePreview, actionType?, cause |
xynthesis parse failure |
SkillExecutionTraceError |
RunSkillDiagnostics (incl. trace.invokeRequest echo) |
MAIN skill failure in trace mode |
LlmCallContextError |
observation: LlmCallObservation, cause |
thin wrapper for the few sites where the upstream still throws untyped (raw setTimeout timeouts, MAIN skill outside trace mode, AIGateway repair fallback) |
All of these are re-exported from @exellix/ai-tasks:
import {
XynthesisInvokeError,
XynthesisResponseParseError,
SkillExecutionTraceError,
LlmCallContextError,
type LlmCallObservation,
type LlmCallConfig,
type InvokeAttemptSummary,
type RunSkillDiagnostics,
resolveOutputExpectation,
ACTION_OUTPUT_DEFAULTS,
mapGatewayInvokeToTrace,
traceToDiagnostics,
buildInvokeAttemptSummary,
getModelCapabilities,
} from "@exellix/ai-tasks";
Worked example
import { runTask, type LlmCallConfig } from "@exellix/ai-tasks";
const llmCall: LlmCallConfig = {
model: "gpt-5-mini",
outputExpectation: { size: { kind: "absolute", maxWords: 600 }, density: "default" },
temperature: 0.4,
reasoningEffort: "medium",
};
const result = await runTask({
skillKey: "tasks/security-risk-summary",
agentId: "agent-1",
jobTypeId: "security-job",
taskTypeId: "security-task",
executionStrategies: [],
modelConfig: {
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "openai/gpt-5",
},
input: { assetId: "a-123" },
executionMode: "trace",
llmCall,
aiScopingOptions: { llmCall: { model: "cheap/default", outputExpectation: { size: { kind: "absolute", maxWords: 200 }, density: "concise" } } },
// executionPipeline: [{ phase: "pre", type: "synthesized-context", config: { llmCall } }, { phase: "main", type: "direct" }],
});
const m = (result as any).metadata ?? {};
console.log("PRE synth calls:", m.synthesizedContextLlmCalls?.length ?? 0);
console.log("AI scoping calls:", m.aiScopingLlmCalls?.length ?? 0);
console.log("AI scoping fails:", m.aiScopingFailures?.length ?? 0);
console.log("audit calls:", m.postSteps?.audit?.llmCalls?.length ?? 0);
console.log("polish calls:", m.postSteps?.polish?.llmCalls?.length ?? 0);
Graph Execution Support
Overview
@exellix/ai-tasks supports optional graph execution context fields that enable smarter activity tracking and identity for nodes executing within graph workflows. When executing tasks in a graph (e.g., via worex-graphs), you can provide additional context fields to improve activity tracking and debugging.
The client automatically passes these fields through to @exellix/ai-skills, which then passes them to @x12i/ai-gateway. The gateway maps them to the activity identity structure according to @x12i/activix-tracking v3.5.0+ mapping rules.
Optional Fields
graphId (optional)
Type: string
Description: Identifier for the graph execution context
Used when executing tasks within a graph to track which graph contains the task. This enables:
- Activity grouping and filtering by graph
- Better activity identity for graph-based workflows
- Enhanced debugging and traceability
Example:
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'graph-456', // Graph identifier
nodeId: 'node-789', // Node identifier
});
Use Cases:
- Graph workflow execution tracking
- Multi-graph job orchestration
- Graph-level activity aggregation
nodeId (optional)
Type: string
Description: Identifier for the specific node being executed
Used when executing tasks within a graph to track which specific node is being executed. When provided along with graphId, enables:
- Precise activity identity for graph nodes
- Node-level activity tracking and debugging
- Better correlation between graph structure and executed activities
Example:
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing', // Graph identifier
nodeId: 'extract-emails-node', // Node identifier
});
Use Cases:
- Individual node execution tracking
- Node-level activity analysis
- Graph node debugging
prevNodeId (optional)
Type: string
Description: Identifier for the previous node in the graph execution flow
Used when executing tasks within a graph to track which node was executed before the current one. This enables:
- Graph flow tracking and debugging
- Understanding execution order in graph workflows
- Better activity correlation between consecutive nodes
Example:
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing',
nodeId: 'extract-emails-node',
prevNodeId: 'validate-input-node', // Previous node identifier
});
Use Cases:
- Graph execution flow tracking
- Understanding node execution sequence
- Debugging graph workflow issues
coreSkillId (optional)
Type: string
Description: Alternative node identifier for graph execution
Used as an alternative to nodeId when executing tasks within a graph. This field is automatically mapped to identity.nodeId in activity tracking, with priority over nodeId but below skillId (from the underlying skill request).
Example:
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing',
coreSkillId: 'q0', // Alternative node identifier
});
Use Cases:
- Alternative node identification (common in worex-graphs integration)
- Node-level tracking when using coreSkillId naming convention
Field Mapping
The client automatically passes graph/node fields through @exellix/ai-skills to the underlying @x12i/ai-gateway, which maps them to the activity identity structure. The gateway uses the following priority rules:
Graph ID Mapping
masterSkillId(primary) →identity.graphIdgraphId(fallback) →identity.graphId
Node ID Mapping
skillId(primary, from underlying skill request) →identity.nodeIdcoreSkillId(alternative) →identity.nodeIdnodeId(fallback) →identity.nodeId
Previous Node ID Mapping
prevNodeId→identity.prevNodeId(automatically mapped for graph flow tracking)
Preserved Fields
masterSkillId→identity.masterSkillId(preserved for backward compatibility)masterSkillActivityId→identity.masterSkillActivityId(preserved for skill hierarchies)
This enables:
- Smarter Identity: Activity records include graph and node context automatically
- Better Grouping: Filter and aggregate activities by graph or node
- Enhanced Debugging: Trace activities back to specific graph nodes
- Improved Analytics: Analyze graph execution patterns and node performance
- Flexible Integration: Support for multiple field naming conventions
Complete Field Mapping Guide
| @exellix/ai-tasks Field | @exellix/ai-skills Field | @x12i/ai-gateway Field | Identity Field | Mapping Priority |
|---|---|---|---|---|
masterSkillId |
masterSkillId |
masterSkillId (AIRequest) |
identity.graphId |
Priority 1 (primary) |
graphId |
graphId |
graphId |
identity.graphId |
Priority 2 (fallback) |
skillId (via skills) |
skillId (AIRequest) |
skillId (AIRequest) |
identity.nodeId |
Priority 1 (primary) |
coreSkillId |
coreSkillId |
coreSkillId |
identity.nodeId |
Priority 2 (alternative) |
nodeId |
nodeId |
nodeId |
identity.nodeId |
Priority 3 (fallback) |
skillKey |
skillKey |
instructions |
- | Direct mapping |
jobId |
jobId |
jobId |
identity.jobId |
Direct mapping |
agentId |
agentId |
agentId |
identity.agentId |
Direct mapping |
masterSkillActivityId |
masterSkillActivityId |
masterSkillActivityId (AIRequest) |
identity.masterSkillActivityId |
Preserved |
Note: masterSkillId is a legacy field that is still supported for backward compatibility. The new graphId and nodeId fields provide a cleaner API for graph-based workflows.
Usage Examples
Basic Graph Node Execution
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
const tasks = new WorexClientTasks(skills, executor);
// Execute a task within a graph
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing',
nodeId: 'extract-emails-node',
});
Graph Execution with Legacy Fields
// Using legacy masterSkillId (still supported)
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
masterSkillId: 'workflow-data-processing', // Maps to identity.graphId
coreSkillId: 'extract-emails-node', // Maps to identity.nodeId
});
Graph Execution with coreSkillId
// Using coreSkillId (common in worex-graphs)
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing',
coreSkillId: 'q0', // Alternative node identifier
});
Graph Execution Without Node ID
// Graph-level execution (no specific node)
const result = await tasks.runTask({
skillKey: 'tasks/process-data',
executionType: ExecutionType.DIRECT,
input: { data: '...' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing',
// nodeId not provided - still works
});
Non-Graph Execution (Backward Compatible)
// Standard execution (no graph context)
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
// graphId and nodeId not provided - works as before
});
Using TaskRequestBuilder with Graph Context
import { TaskRequestBuilder } from "@exellix/ai-tasks";
const builder = new TaskRequestBuilder();
const request = builder
.withSkillKey('tasks/security-risk-summary')
.withInput({ assetId: 'a-123' })
.withGraphId('workflow-data-processing')
.withNodeId('extract-emails-node')
.withJobId('job-123')
.withAiSkillsCorrelation('agent-1', 'job-type-id', 'task-type-id')
.build();
const result = await tasks.runTask(request);
Using TaskRequestBuilder with Memory Management
import { TaskRequestBuilder } from "@exellix/ai-tasks";
import type { JobHistory, TaskHistory, ExecutionHistory } from "@exellix/ai-tasks";
const jobMemory: JobHistory = { /* previous task results */ };
const taskMemory: TaskHistory = { /* previous skill executions */ };
const executionMemory: ExecutionHistory = { /* execution context */ };
const builder = new TaskRequestBuilder();
const request = builder
.withSkillKey('tasks/my-task')
.withInput({ data: 'process' })
.withJobMemory(jobMemory)
.withTaskMemory(taskMemory)
.withExecutionMemory(executionMemory)
.withJobId('job-123')
.withAiSkillsCorrelation('agent-1', 'job-type-id', 'task-type-id')
.build();
const result = await tasks.runTask(request);
Using Convenience Methods with Graph Context
import { runTask, ExecutionType } from "@exellix/ai-tasks";
// Convenience method automatically initializes SDK (ERC mode)
const result = await runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'workflow-data-processing',
nodeId: 'extract-emails-node',
});
Integration with Graph Runtimes
When integrating with graph execution runtimes (e.g., worex-graphs), you can pass graph/node fields directly - the client automatically passes them through to the underlying skills client and gateway.
Automatic Mapping (Recommended)
The client automatically passes fields from graph runtimes to the gateway. You can pass fields directly without manual mapping. Production calls must also include jobTypeId, taskTypeId, and executionStrategies (often []); the snippets below omit them for brevity.
// worex-graphs sends fields directly - client handles passing through automatically
const requestFromGraph = {
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
jobId: 'job-123',
agentId: 'agent-1',
// jobTypeId, taskTypeId, executionStrategies: [] — required in real requests
masterSkillId: 'graph-question-breakdown', // Automatically mapped to identity.graphId
coreSkillId: 'q0', // Automatically mapped to identity.nodeId
masterSkillActivityId: 'job-123:q0', // Preserved in identity
input: { assetId: 'a-123' },
// ... other fields
};
// Pass fields directly - client passes through automatically
const result = await tasks.runTask({
skillKey: requestFromGraph.skillKey,
executionType: ExecutionType.DIRECT,
input: requestFromGraph.input,
jobId: requestFromGraph.jobId,
agentId: requestFromGraph.agentId,
masterSkillId: requestFromGraph.masterSkillId, // ✅ Automatically passed to gateway
coreSkillId: requestFromGraph.coreSkillId, // ✅ Automatically passed to gateway
masterSkillActivityId: requestFromGraph.masterSkillActivityId, // ✅ Automatically passed to gateway
// ... other fields
});
Using New Graph Fields (Alternative)
Alternatively, you can map to the new direct fields (graphId/nodeId):
// Map to new graph fields
const result = await tasks.runTask({
skillKey: requestFromGraph.skillKey,
executionType: ExecutionType.DIRECT,
input: requestFromGraph.input,
jobId: requestFromGraph.jobId,
agentId: requestFromGraph.agentId,
graphId: requestFromGraph.masterSkillId, // Direct graph identifier
nodeId: requestFromGraph.coreSkillId, // Direct node identifier
// ... other fields
});
Xynthesized memory and smartInput from graph-engine
Graph runtimes can pass xynthesized (job, task, and execution buckets) and smartInput (Rendrix paths or legacy paths: string[]) on the same runTask request. ai-tasks normalizes legacy string paths, forwards smartInput / smartInputRenderOptions to local ctx, PRE synthesis, MAIN runSkill, and merged template variables; it does not apply graph output mapping or merge patches into a global store—that is the engine’s job after reading response.xynthesizedPatch (job, task, and/or execution slices) and any response.smartInputRenderResult from downstream. See Xynthesized memory and smart input.
Activity Record Structure
When graph context is provided, activity records include the mapped fields in the identity structure:
{
_id: ObjectId('...'),
jobId: 'job-123',
agentId: 'agent-1',
taskId: 'task-456',
taskTypeId: 'abc123...',
identity: {
jobId: 'job-123',
agentId: 'agent-1',
taskId: 'task-456',
taskTypeId: 'abc123...',
graphId: 'workflow-data-processing', // ← Automatically mapped from masterSkillId or graphId
nodeId: 'extract-emails-node', // ← Automatically mapped from skillId, coreSkillId, or nodeId
masterSkillId: 'workflow-data-processing', // ← Preserved if provided (backward compatibility)
masterSkillActivityId: 'job-123:node-456' // ← Preserved if provided (skill hierarchies)
},
request: { /* ... */ },
config: { /* ... */ },
// ... other fields
}
Benefits
- Enhanced Activity Tracking: Activities include graph and node context for better traceability
- Smarter Identity: Activity identity is enriched with graph/node information
- Better Debugging: Easier to correlate activities with graph structure
- Improved Analytics: Analyze execution patterns at graph and node levels
- Flexible Integration: Optional fields don't break existing integrations
- Backward Compatible: Legacy
masterSkillIdfield still works
Migration Guide
For Existing Integrations
These fields are optional, so existing integrations continue to work without changes:
// ✅ Existing code continues to work
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
// graphId and nodeId not required
});
Adding Graph Support
To add graph execution support, simply include the optional fields:
// ✅ Add graph context for better tracking
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
jobId: 'job-123',
agentId: 'agent-1',
graphId: 'my-graph-id', // Add graph context
nodeId: 'my-node-id', // Add node context (optional)
});
Migrating from Legacy Fields
If you're using masterSkillId, you can optionally migrate to the new graphId field for clarity:
// Before (still works)
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
masterSkillId: 'graph-id',
coreSkillId: 'node-id',
});
// After (cleaner API)
const result = await tasks.runTask({
skillKey: 'tasks/security-risk-summary',
executionType: ExecutionType.DIRECT,
input: { assetId: 'a-123' },
graphId: 'graph-id', // More explicit
nodeId: 'node-id', // More explicit
});
TypeScript Types
All graph execution fields are properly typed in the TypeScript definitions:
interface RunTaskRequest {
skillKey: string;
agentId: string; // Required by @exellix/ai-skills
jobTypeId: string;
taskTypeId: string;
input: Record<string, any> | string;
executionType: ExecutionType;
// ... other fields ...
graphId?: string; // Graph execution context
nodeId?: string; // Node identifier
coreSkillId?: string; // Alternative node identifier
masterSkillId?: string; // Legacy graph identifier (still supported)
// ... other fields ...
}
Related Documentation
- @exellix/ai-skills — UPGRADING_AI_GATEWAY_10.md — gateway 10.x migration (billing, required fields, Activix persistence)
- @exellix/ai-skills Graph Execution Support - Skills-level documentation
- @x12i/ai-gateway Graph Execution Support - Gateway-level documentation
- @x12i/activix-tracking - Activity tracking specification
Notes
agentId/jobTypeId/taskTypeIdare required on everyrunTask(seeRUNTASK_REQUEST.md).- Graph/node fields (
graphId,nodeId, …) remain optional for non-graph runs — provide them when executing tasks in graphs. - Fields are automatically passed through to the skills client and gateway, then mapped to activity identity.
- Multiple field naming conventions supported:
- New fields:
graphId/nodeId/coreSkillId(recommended for new code) - Legacy fields:
masterSkillId(still supported for backward compatibility)
- New fields:
- Mapping priority ensures consistent identity structure regardless of field naming.
- Best practice: Include graph/node context fields for graph task execution for maximum traceability.
API Reference
WorexClientTasks
Main class for task execution.
Constructor
constructor(
skillsClient: WorexClientSkills,
executor: {
execute<TParsed = any>(
input: any,
onNotFound: (key: string) => Promise<void>
): Promise<RunTaskResponse<TParsed>>;
}
)
Methods
runTask<TParsed>(input: RunTaskRequest): Promise<RunTaskResponse<TParsed>>
Executes a task with task-level enrichment and context generation.
const result = await tasks.runTask({
skillKey: "tasks/security-risk-summary",
agentId: "my-agent",
jobTypeId: "security-risk-job",
taskTypeId: "security-risk-task",
// executionType is optional, defaults to ExecutionType.DIRECT
input: { assetId: "a-123" },
variables: { orgName: "Acme" },
jobMemory: previousJobMemory,
taskMemory: previousTaskMemory,
executionMemory: previousExecutionMemory,
jobId: "job-123"
});
Local task registry
registerLocalTask(skillKey: string, handler: LocalTaskHandler): void— Register a local task handler. WhenrunTaskis called with thisskillKey, the handler runs instead of the normal enrichment/executor path.getLocalTask(skillKey: string): LocalTaskHandler | undefined— Return the registered handler forskillKey, orundefined.registerBuiltInLocalTasks(): void— Register built-in handlers (skills/node.callExport,skills/node.callExportBatch,skills/skill.local:validateInput,skills/skill.local:normalizeNarrixResult). The Narrix skillskills/skill.local:narrixRun(record/text/docs/chat) is registered via a side-effect import and is available when the package is loaded.LocalTaskContext— Type:skillKey,jobMemory,taskMemory,executionMemory,variables,xynthesized,smartInput,jobId,taskId,agentId,graphId,nodeId,prevNodeId,coreSkillId,masterSkillId,masterSkillActivityId.LocalTaskHandler— Type:(args: { input: any; ctx: LocalTaskContext }) => Promise<any>.
TaskRequestBuilder
Builder class for constructing task requests with a fluent API.
Methods
withSkillKey(skillKey: string): this- Set the skill keywithInput(input: Record<string, any> | string): this- Set input datawithExecutionStrategies(invocations: ExecutionStrategyInvocation[]): this— MAIN strategy invocations (required on the built request; defaults to[]inbuild()when unset)withExecutionPipeline(steps: ExecutionStep[]): this- Set execution pipeline (pre/main/post steps)withSynthesizedContextPreStep(modelOrConfig?: string | SynthesisConfig): this- Add synthesized-context PRE step and setincludeContextInPrompt: true. Pass aSynthesisConfigobject to setcontextSourcePolicy,webEvidence,memoryPaths, etc.withAuditPostStep(config: AuditConfig): this- Add audit POST step (quality-gate loop)withPolishPostStep(config: PolishConfig): this- Add polish POST step (refinement checklist)withVariables(variables: Record<string, any>): this- Set contextual variableswithContext(context: Record<string, any> | string): this- Set contextwithKnowledge(knowledge: Record<string, any>): this- Set knowledge datawithJobMemory(jobMemory: JobHistory): this- Set job memorywithTaskMemory(taskMemory: TaskHistory): this- Set task memorywithExecutionMemory(executionMemory: ExecutionHistory): this- Set execution memorywithJobId(jobId: string): this- Set job IDwithAgentId(agentId: string): this- Set agent IDwithJobTypeId(jobTypeId: string): this- Set job type id (required beforebuild())withTaskTypeId(taskTypeId: string): this- Set task type id (required beforebuild())withAiSkillsCorrelation(agentId: string, jobTypeId: string, taskTypeId: string): this- Set all three correlation ids (recommended)withGraphId(graphId: string): this- Set graph identifierwithNodeId(nodeId: string): this- Set node identifierwithPrevNodeId(prevNodeId: string): this- Set previous node identifierwithModelConfig(modelConfig: RunTaskModelConfig): this- Set the required 3-slot model triplet + tuningwithTemplateRenderOptions(templateRenderOptions: TemplateRenderOptions): this- Per-task gateway / parser template overrides (v4)withTemplateTokens(templateTokens: GatewayTemplateTokens): this- Per-tasktemplateTokensoverlaywithTimeout(timeoutMs: number): this- Set timeoutwithXynthesized(memory: XynthesizedMemory): this— optionalxynthesizedbucket (job/task/executionrecords)withSmartInput(config: RunTaskSmartInput): this— passthroughsmartInput(Rendrixpathsor legacystring[])withSmartInputPaths(paths: string[]): this— buildssmartInput: { paths: paths.map(p => ({ title: p, path: p })) }withSmartInputRenderOptions(opts: SmartInputRenderOptions): this— setssmartInputRenderOptionson the request (gateway/Rendrix merge)withXynthesizedJob(key: string, value: unknown): this/withXynthesizedTask(key: string, value: unknown): this/withXynthesizedExecution(key: string, value: unknown): this— merge a single key intoxynthesized.job/xynthesized.task/xynthesized.executionbuild(): RunTaskRequest- Build the final request (executionStrategiesdefaults to[]when unset)
Example
import { TaskRequestBuilder } from "@exellix/ai-tasks";
const request = new TaskRequestBuilder()
.withSkillKey("tasks/my-task")
.withInput({ data: "process" })
.withAiSkillsCorrelation("my-agent", "my-job-type", "my-task-type")
.withExecutionStrategies([]) // [] = plain MAIN; omit only if you rely on build() defaulting strategies to []
.withModelConfig({
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "gpt-5",
temperature: 0.7,
})
.withJobMemory(jobMemory)
.withTaskMemory(taskMemory)
.withExecutionMemory(executionMemory)
.withJobId("job-123")
.build();
const result = await tasks.runTask(request);
Types
RunTaskRequest
interface RunTaskRequest {
skillKey: string; // "tasks/security-risk-summary"
agentId: string; // Required — @exellix/ai-skills correlation
jobTypeId: string; // Required — catalog / telemetry job classification
taskTypeId: string; // Required — catalog / telemetry task classification
/** Required: MAIN strategy invocations; use [] for plain gateway MAIN (see BREAKING-CHANGES / RUNTASK_REQUEST). */
executionStrategies: ExecutionStrategyInvocation[];
/** Optional guarded metadata rows from catalogId `execution-strategy`; code validation remains authoritative. */
executionStrategyCatalogItems?: TaskStrategyItemData[];
input: Record<string, any> | string; // Task payload (canonical user/host data)
executionType?: ExecutionType | string; // Optional; DIRECT default when using plain MAIN; narrix-then-direct when configured
narrixInput?: NarrixRunInput | { $path: string }; // Required when narrixMode infers handler without preprocessor narrix
narrixScope?: NarrixScope; // When set with narrix handler path, filters which signals/stories go to task memory
/** Task-level NARRIX pre-processor: datasetId, attachToField (CNI attachment only; see [NARRIX task-level pre-processor](#narrix-task-level-pre-processor)). */
narrix?: NarrixPreProcessorConfig;
includeContextInPrompt?: boolean; // Opt-in: when true, task-scoped context is added to the prompt; default is false (no context)
/** Optional: pre / main / post steps. When set (non-empty), runs PRE (e.g. synthesized-context), one MAIN, then POST. See [Execution pipeline](#execution-pipeline-and-synthesized-context-pre-step). */
executionPipeline?: ExecutionStep[];
/** executionMode: "trace" — ordered debugTrace + richer LLM observability (see [Trace mode](#trace-mode-authoritative-ordered-debug-trace)). */
executionMode?: "default" | "trace";
/** Merged into template variables before explicit variables (MAIN path). See mergeSkillTemplateVariables. */
jobContext?: Record<string, unknown>;
/** Job-, task-, and execution-scoped synthesized buckets for graph execution (distinct from raw memories). */
xynthesized?: XynthesizedMemory;
/** Rendrix smart-input paths or legacy `paths: string[]` (normalized before MAIN); root allows only `paths`. */
smartInput?: RunTaskSmartInput;
/** Optional Rendrix options for the `{{smartInput}}` macro (merged on gateway like `RunSkillRequest`). */
smartInputRenderOptions?: SmartInputRenderOptions;
// Optional fields
variables?: Record<string, any>; // Contextual variables
context?: Record<string, any> | string;
knowledge?: Record<string, any>;
jobMemory?: JobHistory; // History of previous task results
taskMemory?: TaskHistory; // History of skills executed
executionMemory?: ExecutionHistory; // History of execution context (execution-level memory)
modelConfig: RunTaskModelConfig; // Required 3-slot triplet + optional tuning
jobId?: string;
// Graph execution context (optional)
graphId?: string; // Graph identifier for activity tracking
nodeId?: string; // Node identifier for activity tracking
coreSkillId?: string; // Alternative node identifier (maps to identity.nodeId)
// Legacy fields (still supported for backward compatibility)
masterSkillId?: string; // Legacy graph identifier (maps to identity.graphId)
masterSkillActivityId?: string; // Preserved for skill hierarchies
timeoutMs?: number;
/** Per-invoke template parser options (merged on gateway defaults). See [Gateway template rendering (v4)](#gateway-template-rendering-v4). */
templateRenderOptions?: TemplateRenderOptions;
/** Highest-priority template token overlay for this invoke. */
templateTokens?: GatewayTemplateTokens;
}
NarrixPreProcessorConfig is exported from this package. It configures the CNI attachment only and carries no web-scope fields — web scoping is a standalone webScope PRE unit.
The closed JSON schema for hosts and codegen is documenations/schemas/v1/run-task-request.json (includes xynthesized and smartInput). Narrative contract: RUNTASK_REQUEST.md. For smart input, the TypeScript types exported from this package (RunTaskSmartInput, SmartInputConfig) are authoritative; the JSON schema / RUNTASK narrative may still emphasize string paths only until updated to Rendrix { title, path } entries.
TemplateRenderOptions and GatewayTemplateTokens are re-exported from this package (see Gateway template rendering (v4)).
RunTaskResponse<TParsed>
ai-tasks extends the skills response with optional identity, xynthesizedPatch (job, task, and/or execution writes from PRE synthesis when configured), intermediateSteps, debugTrace (when executionMode: "trace"), and observability-related metadata extensions. Underlying shape:
type RunTaskResponse<TParsed = any> = RunSkillResponse<TParsed> & {
/** When downstream returns smart-input diagnostics markdown + resolved paths. */
smartInputRenderResult?: SmartInputRenderResult;
identity?: Record<string, unknown>;
xynthesizedPatch?: XynthesizedMemory;
intermediateSteps?: IntermediateStep[];
debugTrace?: { tasks: DebugTraceTask[] };
aiTasksObservability?: AiTasksObservabilityDigest; // usually attached by orchestrators, not by default
};
interface RunSkillResponse<TParsed = any> {
skillKey: string;
rawText: string;
flexMd: {
frame: string;
payloads: Record<string, string>;
};
parsed: TParsed;
metadata: {
instructionVersion?: string;
activityId?: string;
durationMs?: number; // Present for local tasks (execution tracing)
localSkillKey?: string; // Present for local tasks (e.g. "skills/node.callExportBatch")
narrix?: { // Present when narrix-then-direct ran and Narrix succeeded (or failed; see parsed.phase)
entity: { entityKind: string; entityKey: string };
signals: unknown[];
stories: unknown[];
meta: Record<string, unknown>;
durationMs?: number;
};
};
}
Task responses may include an optional top-level intermediateSteps (array of IntermediateStep) for combined/multi-step tasks; see Intermediate steps (multi-step tasks).
IntermediateStep, IntermediateSteps, RunTaskResponseWithSteps
For multi-step tasks, use the extended response type and step shape:
import type { IntermediateStep, IntermediateSteps, RunTaskResponseWithSteps } from "@exellix/ai-tasks";
interface IntermediateStep {
step: number; // 1-based order
id: string; // e.g. "to-cni", "engine-enrich"
ok: boolean;
summary?: string;
error?: string;
inputSummary?: Record<string, unknown> | string;
outputExcerpt?: Record<string, unknown>;
}
type IntermediateSteps = IntermediateStep[];
interface RunTaskResponseWithSteps<TParsed = any> extends RunSkillResponse<TParsed> {
intermediateSteps?: IntermediateSteps;
}
See documenations/intermediate-steps.md for full documentation and examples.
ExecutionPhase, ExecutionStep, SynthesisConfig, ContextSourcePolicy, WebEvidenceConfig, structured synthesis types
Used with executionPipeline and the synthesized-context PRE step. Import from @exellix/ai-tasks (and use SYNTHESIZED_CONTEXT for the PRE step type string):
import type {
ExecutionPhase,
ExecutionStep,
ExecutionStrategyInvocation,
SynthesisConfig,
ContextSourcePolicy,
WebEvidenceConfig,
SynthesisInput,
SynthesizedPromptPayload,
SynthesizedContext,
SynthesizedItem,
ContextSynthesizer,
XynthesizedMemory,
SmartInputConfig,
SmartInputRenderOptions,
SmartInputRenderResult,
RunTaskSmartInput,
} from "@exellix/ai-tasks";
import { SYNTHESIZED_CONTEXT } from "@exellix/ai-tasks";
// ExecutionPhase = "pre" | "main" | "post"
// ExecutionStep: { phase, type: SYNTHESIZED_CONTEXT | "direct" | "audit" | "polish", config? }
// SynthesisConfig: … synthesisOutputFormat?: "markdown" | "structured", questionPath?, getQuestion?, structuredMaxItemsPerSide?, …
// ContextSourcePolicy: "auto" | "narrix-web" | "narrix-web-memory" | "memory-web" | "memory-only" | "narrix-only" | "narrix+memory"
// WebEvidenceConfig: preferCleanContent?, maxSources?, dedupeByUrl?, maxTotalChars?
Semantics and the full policy table: Execution pipeline and synthesized-context.
ExecutionType
enum ExecutionType {
DIRECT = "direct"
}
The package also exports the constant NARRIX_THEN_DIRECT = "narrix-then-direct" for use with executionType when running Narrix first, then the executor. See Narrix then execute.
NarrixScope
Optional filter for which signals and stories from Narrix are written to task memory (used with executionType: "narrix-then-direct"):
interface NarrixScope {
includeSignals?: string[]; // Signal codes to keep (allowlist)
excludeSignals?: string[]; // Signal codes to drop (blocklist; ignored when includeSignals is set)
includeStories?: string[]; // narrativeTypeIds to keep (allowlist)
excludeStories?: string[]; // narrativeTypeIds to drop (blocklist; ignored when includeStories is set)
}
NarrixPreProcessorConfig
Exported type for RunTaskRequest.narrix. Includes datasetId, optional attachToField, and reserved fields. It carries no web-scope fields. See NARRIX task-level pre-processor and documenations/web-scoping-in-ai-tasks.md.
import type { NarrixPreProcessorConfig } from "@exellix/ai-tasks";
RunTaskModelConfig
Required on every runTask() — three slots plus optional tuning (no maxTokens, no legacy model / xynthesisModel):
import type { RunTaskModelConfig } from "@exellix/ai-tasks";
interface RunTaskModelConfig {
preActionModel: string; // ai-profiles alias (xynthesis PRE)
skillModel: string; // alias or concrete id (ai-skills MAIN)
postActionModel: string; // ai-profiles alias (xynthesis POST)
temperature?: number;
topP?: number;
reasoningEffort?: OptimixerReasoningEffort;
frequencyPenalty?: number;
presencePenalty?: number;
}
Example:
const modelConfig: RunTaskModelConfig = {
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "cyber",
temperature: 0.7,
topP: 0.9,
reasoningEffort: "medium",
};
const result = await runTask({
skillKey: "tasks/analysis",
executionType: ExecutionType.DIRECT,
input: { data: "analyze" },
modelConfig
});
Note: Root RunTaskRequest.modelConfig uses canonical slots (RunTaskModelConfig: preActionModel, skillModel, postActionModel). Profile/choice keys are validated at runTask entry and resolved at each xynthesis / ai-skills call boundary via @x12i/ai-profiles v2. See Model profile aliases and MODEL-CONFIGURATION.md.
JobHistory, TaskHistory, and ExecutionHistory
History objects for managing execution context across workflows:
import type { JobHistory, TaskHistory, ExecutionHistory } from "@exellix/ai-tasks";
// JobHistory: Contains history of all previous task results in the job
const jobMemory: JobHistory = {
// Previous task results
};
// TaskHistory: Contains history of skills executed up to this task
const taskMemory: TaskHistory = {
// Previous skill executions
};
// ExecutionHistory: Contains history of execution context (execution-level memory)
const executionMemory: ExecutionHistory = {
// Execution state, intermediate results, execution-level variables
previousAttempts: 2,
lastError: "timeout",
intermediateResults: { step1: "completed" }
};
JobHistoryandTaskHistoryare re-exported from@x12i/execution-memory-managerfor convenienceExecutionHistoryis defined in this package asRecord<string, any>for execution-level memory
See HISTORY-OBJECTS.md for detailed documentation on history objects and HISTORY-OBJECTS-DOWNSTREAM.md for information on how they flow downstream.
Content Registry Structure (v2.0.2+)
To ensure robust resolution with @x12i/ai-gateway v7.0.0+, it is recommended to provide both .instructions and .prompt files for each skill in your .metadata/skills directory.
{skillId}.instructions: Contains the system instructions (persona, constraints, output format).{skillId}.prompt: Contains the user prompt template (e.g.,{{input}}). With gateway template v4,{{input}}is supplied from merged working memory (see Gateway template rendering (v4)); usetemplateRenderOptions.subPathSearchwhen templates need paths like{{execution.summary}}backed by nested task input.
See SKILL-CONTENT-GUIDE.md for a detailed migration and implementation guide.
Environment Variables
See .env.example for complete configuration. Key variables:
Used by ai-tasks:
MONGO_LOGS_DB- [OPTIONAL] Default database for activity logs (defaults tologs-db), passed asbindingDefaultsDbto the skills client.
POST step (shared):
POST_STEP_MODEL- [OPTIONAL] Fallback model for all POST step LLM calls when no step-specific env is set.POST_STEP_TIMEOUT_MS- [OPTIONAL] Fallback timeout in ms for POST step LLM calls (default30000).
Audit POST step:
AUDIT_MODEL- [OPTIONAL] Model for audit evaluator (→POST_STEP_MODEL→ gateway default).AUDIT_SYNTHESIS_MODEL- [OPTIONAL] Model for synthesis merge whenselectionStrategy: "synthesis"(→POST_STEP_MODEL→ MAIN step model).AUDIT_TIMEOUT_MS- [OPTIONAL] Timeout per audit LLM call (→POST_STEP_TIMEOUT_MS→30000).AUDIT_TEMPLATES_PATH- [OPTIONAL] Base path for audit templates (default<project>/templates/post-steps/audit).
Polish POST step:
POLISH_MODEL- [OPTIONAL] Model for polish LLM (→POST_STEP_MODEL→ gateway default).POLISH_TIMEOUT_MS- [OPTIONAL] Timeout per polish call (→POST_STEP_TIMEOUT_MS→30000).POLISH_TEMPLATES_PATH- [OPTIONAL] Base path for polish templates (default<project>/templates/post-steps/polish).
Required for content and providers (validated by ai-skills):
GITHUB_TOKEN- [REQUIRED] GitHub token for content registry accessGITHUB_REPO_URL- [REQUIRED] Content registry repository URLOPENAI_API_KEYorGROK_API_KEY- [REQUIRED] AI provider key (orOPEN_ROUTER_KEY/OPENROUTER_API_KEYif ai-skills accepts OpenRouter-only)
Downstream (used by ai-skills → ai-gateway → ai-provider-router, ai-activities-tracking): ai-tasks does not read these; they affect LLM and activity tracking when using this package.
OPEN_ROUTER_KEYorOPENROUTER_API_KEY- [OPTIONAL] Use OpenRouter for LLM calls; no provider registration needed when set.MONGO_LOGS_COLLECTION- [OPTIONAL] Activities collection (default in ai-activities-tracking:cognitive-activities).MONGO_BAD_REQUESTS_COLLECTION- [OPTIONAL] Bad-requests collection (default:ai-bad-requests).
Synthesis (synthesized-context PRE step):
SYNTHESIS_MODEL- [OPTIONAL] Default model for synthesis (e.g.gpt-5-nano).SYNTHESIS_TIMEOUT_MS- [OPTIONAL] Timeout for synthesis call (default 30000).SYNTHESIS_MAX_OUTPUT_LENGTH- [OPTIONAL] Max length of synthesis output (characters).SYNTHESIS_TEMPLATES_PATH- [OPTIONAL] Base path fortemplates/synthesis/system.mdanduser.txt(default: project root).
Other:
MONGODB_URI- [OPTIONAL] MongoDB for context enrichmentCONTENT_REGISTRY_LOCAL_PATH- [OPTIONAL] Local registry pathUSE_NARRIX_INGEST- [OPTIONAL] Set to"1"to enable the Narrix pipeline forskills/skill.local:narrixRun(record/text/docs/chat). When unset, the handler returnsNARRIX_DISABLED.NARRIX_DEBUG- [OPTIONAL] Set to"1"to log datasetId, medium, packId, entityKind, signal codes, and narrativeTypeIds when the Narrix handler runs.TAVILY_API_KEY- [OPTIONAL] Required when usingwebScopePRE units (via@exellix/web-scope→@x12i/web-queries+@x12i/search-adapter/ Tavily). When missing or invalid,executionMemory.webContextreflectsok: false; the task still completes.MONGO_URI,EXELLIX_NAME,MEMORIX_KNOWLEDGE_DB- [OPTIONAL] Memorix TTL caching and synthesized-knowledge persistence for web scope (via@x12i/web-scoper-memorix).WEB_CONTEXT_MARKDOWN_MAX_CHARS- [OPTIONAL] Positive integer: max Unicode length of the Web sources markdown block appended to task context / synthesis source material (tail truncated with a marker). Omit for no downstream cap (upstreammaxTotalWebContextChars/maxSnippetCharsPerSourcestill apply in the scoper).
For a concise breakdown of what ai-tasks uses vs downstream, see documenations/downstream-environment.md.
Examples
Basic Usage
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);
const result = await tasks.runTask({
skillKey: "tasks/security-risk-summary",
executionType: ExecutionType.DIRECT,
input: { assetId: "a-123" },
variables: { orgName: "MyOrganization" },
modelConfig: {
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "openai/gpt-5",
temperature: 0.7
}
});
console.log(result.flexMd.payloads);
With Memory Management
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
import type { JobHistory, TaskHistory, ExecutionHistory } from "@exellix/ai-tasks";
const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);
const jobMemory: JobHistory = {
// Previous task results
};
const taskMemory: TaskHistory = {
// Previous skill executions
};
const executionMemory: ExecutionHistory = {
previousAttempts: 1,
intermediateResults: { analysis: "in-progress" }
};
const result = await tasks.runTask({
skillKey: "tasks/professional-decision",
executionType: ExecutionType.DIRECT,
input: { scenario: "Should we migrate to cloud?" },
variables: { orgName: "EnterpriseCorp" },
jobMemory,
taskMemory,
executionMemory,
jobId: "job-456"
});
With Model Configuration
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);
// Configure model per-request (@exellix/ai-skills correlation ids required)
const result = await tasks.runTask({
skillKey: "tasks/analysis",
agentId: "agent-1",
jobTypeId: "analysis-job",
taskTypeId: "analysis-task",
executionStrategies: [],
executionType: ExecutionType.DIRECT,
input: { data: "analyze this" },
modelConfig: {
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "gpt-5",
temperature: 0.7,
topP: 0.9,
},
});
// Or using the builder
import { TaskRequestBuilder } from "@exellix/ai-tasks";
const request = new TaskRequestBuilder()
.withSkillKey("tasks/analysis")
.withInput({ data: "analyze this" })
.withAiSkillsCorrelation("agent-1", "analysis-job", "analysis-task")
.withExecutionStrategies([])
.withModelConfig({
preActionModel: "cheap/default",
postActionModel: "cheap/default",
skillModel: "gpt-5",
temperature: 0.5,
})
.build();
const result2 = await tasks.runTask(request);
With Graph Execution Context
import { WorexClientSkills } from "@exellix/ai-skills";
import { WorexClientTasks, ExecutionType } from "@exellix/ai-tasks";
const skills = new WorexClientSkills();
const executor = { /* get from skills client */ };
const tasks = new WorexClientTasks(skills, executor);
// Execute a task within a graph workflow
const result = await tasks.runTask({
skillKey: "tasks/security-risk-summary",
executionType: ExecutionType.DIRECT,
input: { assetId: "a-123" },
variables: { orgName: "TechCorp" },
modelConfig: {
model: "gpt-5",
temperature: 0.7
},
jobId: "job-123",
agentId: "agent-1",
graphId: "workflow-data-processing", // Graph identifier for activity tracking
nodeId: "extract-emails-node" // Node identifier for activity tracking
});
console.log(result.flexMd.payloads);
// Graph and node context are automatically passed through to activity tracking
Activix — task activity tracking
@x12i/activix (8.4+) provides multi-phase activity tracking: each runTask() invocation emits several phase records (local, narrix, pipeline_pre, direct, audit, polish, narrix_then_direct). All phase records share the same correlationId (runtime taskId), so you can group them into one logical task run.
Operational checklist: documenations/activix.md · Full reference: docs/activix.md
How wiring works (Activix 8.4+)
ai-tasks always calls Activix lifecycle APIs on AI activity phases when Mongo is configured. Activix decides whether rows are persisted — do not branch in application code on a local write toggle.
runTask() phase → startRecord / completeRecord / failRecord
↓
activix-collections legend state
track → Mongo write | off → same API, skip write
| Layer | Owns |
|---|---|
| ai-tasks | Construct Activix when MONGO_URI or MONGO_LOGS_URI is set; call lifecycle APIs per phase; pass runContext, outer, domain metadata |
| Activix | recordId, status, startTime, endTime, duration; read legend state and skip or perform Mongo writes |
| Ops | Toggle persistence via activix-collections (state: track | off) or setCollectionTrackingState() — no redeploy required |
Lifecycle fields (status, timestamps, primary key) are never set by ai-tasks; Activix manages them on every write.
Enable / disable
Client construction (whether phase APIs are called):
- On by default when
MONGO_URIorMONGO_LOGS_URIis set. - Opt out entirely:
ACTIVIX_ENABLED=false— no Activix client, no API calls (zero overhead for local dev/CI). ACTIVIX_ENABLED=trueis no longer required to turn tracking on.
Mongo persistence (whether rows reach the database):
- Controlled by Activix collection legend
stateinactivix-collections(defaulttrack). - Set
offto skip writes while keeping the same API surface:await ax.setCollectionTrackingState('task-activities', 'off'). - Legend state is cached (default TTL 600 s); override with
ACTIVIX_COLLECTION_TRACKING_STATE_TTL_SEC.
All Activix failures are non-fatal — a MongoDB outage never surfaces to the runTask() caller.
Configuration
| Variable | Default | Description |
|---|---|---|
MONGO_URI / MONGO_LOGS_URI |
— | Required to construct Activix (first set wins) |
ACTIVIX_ENABLED |
(unset) | Set to false to opt out of client construction |
ACTIVIX_COLLECTION |
task-activities |
Dev/CI override only; package hard-codes task-activities in source |
ACTIVIX_STALE_TTL_MS |
300000 |
TTL (ms) before a stuck started record is marked timeout |
ACTIVIX_COLLECTION_TRACKING_STATE_TTL_SEC |
600 |
Legend state cache TTL (Activix 8.4+) |
ACTIVIX_LOGS_LEVEL |
(see below) | Injected Activix logger threshold (@x12i/logxer, prefix ACTIVIX). Falls back to ACTIVIX_LOG_LEVEL, then warn. Values: verbose | debug | info | warn | error | off / silent / none. |
ACTIVIX_LOG_LEVEL |
warn |
Legacy alias when ACTIVIX_LOGS_LEVEL is unset |
AI_TASKS_LOGS_LEVEL |
warn |
ai-tasks diagnostics threshold (warnCode + .metadata/log-diagnostics.json). Legacy alias: AI_TASKS_LOG_LEVEL. |
Debug tooling (logxer 4.4.2): getAiTasksJobLogs({ jobId, level, limit }) queries in-process structured log envelopes captured during runTask(). Correlation ids are set via runAiTasksWithLogContext / patchRunTaskLogContext.
Query API: getTaskActivities(taskId) returns all phase records for one run (by correlationId).
What is recorded
Each record in the task-activities collection captures:
correlationId,phase— join key for grouping phase records into onerunTask()invocationrunContext— correlation envelope:sessionIdmatchescorrelationId;taskId,skillId, graph/node ids live here (not a top-levelidentityblob)outer— required I/O tier (input/output/metadata); minimal placeholders until completion metadata is mergedskillKey,executionType,jobId,agentId,graphId,nodeId— from the requesthasPipeline,hasNarrix,hasNarrixInput— execution path flagsstatus,startTime,endTime,duration— auto-managed by ActivixinstructionVersion,durationMs,stepCount— from the response on success (phase-dependent)error/errorMessage,errorCode— on failure
Stale record cleanup
Crashed or hung tasks leave records stuck in started. Call ax.markStaleRecords() from a periodic job (e.g. every 5 minutes) in the host process to flip them to timeout.
Development Notes
- This is a private package; we optimize for reuse and speed.
@exellix/ai-skillsis treated as the shared helper + execution layer.- Naming leakage is explicitly acceptable (private monorepo + private packages).
- Task-level enrichment always uses
level: 'task'(not'skill'). - The executor must be a "no-enrichment execute primitive" to avoid double-enrichment.
Troubleshooting
"Content not found" Error
- Verify skill key format:
"tasks/security-risk-summary"(not"security-risk-summary") - Check content registry contains the task
- Verify
GITHUB_TOKENandGITHUB_REPO_URLare set correctly
"Repository not accessible"
- Verify
GITHUB_TOKENhasreposcope for private repos - Check
GITHUB_REPO_URLpoints to an existing repository
"Environment validation failed"
- Ensure at least one AI provider key is set (
OPENAI_API_KEYorGROK_API_KEY) - Verify
GITHUB_TOKENandGITHUB_REPO_URLare set - Check
.envfile is loaded (if usingdotenv)
Development
# Install dependencies (requires @exellix scope registry/auth for narrix packages)
npm install
# Build
npm run build
# Run tests (all narrix tests under test/narrix/; requires build first)
npm test
# Run full Narrix integration tests (record, text, docs, chat + 14 record scenarios)
npm run test:with-narrix-ingest
# Bash/macOS/Linux alternative: USE_NARRIX_INGEST=1 npm test
# PowerShell: $env:USE_NARRIX_INGEST='1'; npm test
# Run real E2E for intermediateSteps (real executor + real skill test-intermediate-steps; requires LLM + gateway env like other E2Es)
# Also requires Catalox/Firebase: FIREBASE_PROJECT_ID and GOOGLE_SERVICE_ACCOUNT_BASE64 (tests load repo-root `.env` first; skip gate requires both to be set).
npm run test:e2e:intermediateSteps
# Bash alternative: RUN_INTERMEDIATE_STEPS_E2E=1 npm test
# Run real E2E for synthesized-context (requires a provider API key in .env)
# Keys recognized by the gate: OPENROUTER_API_KEY, OPENAI_API_KEY, OPENAI_KEY, OPEN_ROUTER_KEY, GROK_API_KEY.
# Optional: SYNTHESIS_MODEL, LLM_MODEL_STRONG, LLM_MODEL_NORMAL, LLM_MODEL, POST_STEP_MODEL (else gateway default).
# Runs three opt-in tests in test/synthesis/e2e-synthesis-real.test.ts:
# (1) default markdown synthesis PRE step + main skill,
# (2) runStructuredSynthesisGatewayCall (real gateway JSON → parse → buildSynthesizedContextMarkdown),
# (3) synthesisOutputFormat: "structured" PRE + main skill (memory-only source + input.question).
# The synthesis model must follow JSON instructions for (2) and (3). Uses ~3 synthesis + ~2 main LLM calls.
RUN_SYNTHESIS_E2E=1 npm test
# Or: npm run test:e2e:synthesis
#
# Xynthesis + Mongo / Activix + sidekick hardening: documenations/xynthesis-upstream-fixes-checklist.md
# Development mode
npm run dev
Narrix tests: When USE_NARRIX_INGEST is not set, only the "feature flag off" test runs; the rest are skipped. Use npm run test:with-narrix-ingest (or set the env var per shell above) to run contract and integration tests; live catalog tests also need .archive/packages/… (see BREAKING-CHANGES-NARRIX-V5.md). Optional: NARRIX_DEBUG=1 for verbose handler logging.
NARRIX task-level pre-processor tests: test/narrix/preProcessorAdapter.integration.test.ts (adapter produces record from jobMemory/executionMemory/input shapes), test/narrix/buildNarrixAttachment.test.ts (attachment from runner result with/without passes), and test/narrix/preProcessorE2E.test.ts (throw when narrix set but no record; e2e handler receives ctx.executionMemory._narrix when ingest is enabled and the narrix seed archive is present).
License
ISC
Repository
Related Packages
@exellix/ai-skills— MAIN skill execution (runSkill, Catalox catalog, gateway invoke; ≥ 6.4.0 / gateway ≥ 10.1.0)@exellix/xynthesis— PRE/POST LLM hops (executeXynthesisAction, sidekick actions; ≥ 4.5.0)@x12i/ai-gateway— LLM gateway for MAIN skill (transitive via ai-skills ≥6.4, ≥ 10.1.0)@x12i/execution-memory-manager— Memory management utilities
Related Documentation
- @exellix/ai-skills — UPGRADING_AI_GATEWAY_10.md — gateway 10.x migration (billing ownership, required
model+maxTokens, removed defaults, Activix persistence,error.metadata) - @exellix/ai-skills — MODEL_INVOKE_CONTRACT.md — MAIN wire model, Optimixer caps, gateway billing ownership
- @exellix/ai-skills — AI_GATEWAY_INVOKE_EXECUTION_METADATA.md — gateway invoke metadata (
costUsd, rejectionmetadata, routing) - @exellix/ai-skills — SKILLS_MANAGER_CLIENT_GUIDE.md — studio vs runtime boundaries
- documenations/intermediate-steps.md - Structured intermediate results for combined/multi-step tasks (
intermediateStepsresponse field) - HISTORY-OBJECTS.md - Comprehensive guide to
JobHistory,TaskHistory, andExecutionHistoryobjects - HISTORY-OBJECTS-DOWNSTREAM.md - How history objects flow downstream through the execution pipeline
- GRAPH-EXECUTION-SUPPORT.md - Graph execution context and activity tracking
- MODEL-CONFIGURATION.md - Complete guide to model configuration in
runTask() - NARRIX-FOUR-OBJECTS-AND-USE-CASES.md - The four Narrix input objects (record, text, docs, chat), payload shapes, and use cases (local narrixRun, narrix-then-direct, normalize, validate)
- NARRIX-STATUS-REPORT.md - Narrix pipeline status: unified handler (record/text/docs/chat), resolver, runners, and tests
- documenations/web-scoping-in-ai-tasks.md - Web scoping in the task SDK
- documenations/run-task-execution-flow.md - Step-by-step current
runTask()behavior and flow diagram - documenations/schemas/README.md - JSON Schema / OpenAPI 3.1 fragments for
outputValidationand PRE synthesisexecutionMemoryfields - documenations/core-runtime-tokens-and-strategies.md - Core token handling and strategy behavior overview
- documenations/synthesized-context-guide.md - User-facing guide for
synthesized-contextPRE-step behavior and controls - documenations/task-core-and-core-aware-synthesis.md - Template core token model and structured synthesis behavior
- documenations/synthesis-invocation-notes.md - Notes on synthesis call behavior, cost, and operational implications
- documenations/activix.md - Activix integration: lifecycle tracking, collection legend
state(Activix 8.4+), record schema, env config, execution path mapping, stale cleanup - documenations/downstream-test-runtime-teardown-cleanup.md - Downstream
ai-skillslifecycle cleanup note for open handles/process-exit hygiene in tests