@exellix/xynthesis
v4.9.14
Published
Standalone pre-actions and post-actions LLM library for Exellix workflows
Readme
@exellix/xynthesis
What it does
- Recommended for hosts (orchestrators):
runXynthesisAiAction— one call withactionId,actionPhase(pre|post),gateway,identity(jobId/taskIdplus run context),config, andtemplateContext(data fields only). RequiresXYNTHESIS_CATALOX_ACTIONS=1; templates load only from Catalox (no host-supplied system/user prompt text). Seedocs/catalox-public-ai-action.md. Consumer migration checklist:docs/migration-ai-tasks-xynthesis-ai-action.md. - Sidekick (lower-level):
runSidekickGatewayCallaccepts a fully builtSidekickInputplusRunSidekickExecConfig. Templates: Catalox-only whenXYNTHESIS_CATALOX_ACTIONS=1; otherwisetemplates/{actionType}/on disk. Same actions (synthesis, audit, fix, pick-best, craft-final) and typed JSON parsers. Model tier (cheapest/balanced/best) resolves per action viaACTION_MODEL_MATRIXandresolveModelForAction. - Terminal graph finalization:
runXynthesisFinalizeexposes a stable utility keyedxynthesis/finalizefor producing a graph’s canonical final output (structured JSON with optional schema validation, or explicit text mode). - Builds the synthesis system prompt from rendered downstream instructions, rendered prompt, and source material (markdown or structured).
executeXynthesisAction— low-level single hop withExecuteXynthesisActionRequest(gateway,prompts,workScopewith requiredjobId/taskId). Used by the sidekick gateway, finalize, and callers that already materialize prompts. PreferrunXynthesisAiActionwhen Catalox should supply templates.- Calls the LLM via
@x12i/funcxdefaultFuncxInvoker(Client.askto OpenRouter; structured paths setresponseFormat: json_objectand client-side JSON normalization). OptionalAIGatewayInvoker(@x12i/ai-gateway) viasetSynthesisInvokerwhen you need the gateway request shape. - In structured synthesis: parses validated JSON, caps items/content, and converts to markdown.
- Provides discoverable template core tokens (
{{core:analysis}},{{core:question}}, etc.) from raw templates via@x12i/rendrix. - Exposes a swappable
SynthesisInvoker— inject a custom backend or wrapFuncxInvoker/AIGatewayInvoker(AIFunctionsInvokeris a deprecated alias for the gateway invoker). This repo’s integration tests use the real API only.
v3 observability: jobId and taskId are required on executeXynthesisAction.workScope, RunSidekickExecConfig, RunStructuredSynthesisParams, and runXynthesisFinalize input. They drive Activix runContext and outer.metadata, @x12i/logxer correlation, and runtimeObjects.lastJobId. Blank values throw on executeXynthesisAction and gateway entry points.
Gateway wire context: every LLM hop requires SynthesisCallGatewayContext on ExecuteXynthesisActionRequest.gateway, RunSidekickExecConfig.gateway, RunStructuredSynthesisParams.gateway, and runXynthesisFinalize input.gateway — aiRequestId, agentId, sessionId, temperature. temperature lives on gateway (canonical); xynthesis does not invent defaults. Optional topP and reasoningEffort sit on the hop request / exec config and are forwarded to the invoker and Optimixer respectively. The default FuncxInvoker maps gateway ids into funcx attribution.tags (and trace id).
Naming note: legacy APIs use question for the task directive (SynthesisInput, resolveSynthesisQuestion, questionPath). Sidekick inputs use task for the same idea.
AI actions subset (@exellix/xynthesis/ai-actions)
Narrow barrel for orchestrators: prefer runXynthesisAiAction + buildSidekickInputFromTemplateContext, runStructuredSynthesisGatewayCall, runXynthesisFinalize, setSynthesisInvoker / flattenExecuteXynthesisActionRequest, executeXynthesisAction, sizing (resolveEffectiveMaxTokens, resolveOutputExpectation, …), markdown / structured synthesis helpers, errors, and related types. getSynthesisInvoker is only on the root package.
import {
runXynthesisAiAction,
runStructuredSynthesisGatewayCall,
resolveEffectiveMaxTokens,
} from "@exellix/xynthesis/ai-actions";That entry omits runSidekickGatewayCall, sidekick pipelines (runSidekickAuditThenFix, …), most Catalox/Activix/runtime exports, and the full @x12i/logxer surface. import "@exellix/xynthesis" is the full public API.
Stack alignment (4.5+)
| Package | Role | Minimum (this repo) |
|---------|------|---------------------|
| @x12i/ai-profiles | Profile/choice → wire model (catalogLane on resolve) | 3.0.0 |
| @x12i/funcx | Default PRE/POST invoker (Client.ask, sidekick built-ins) | ≥ 4.9.19 |
| @x12i/activix | Activity tracking / telemetry when enabled | ≥ 9.0.0 |
| @exellix/ai-skills | MAIN skill execution (orchestrator sibling) | ≥ 7.0.1 / gateway ≥ 10.4.4 |
Billing: USD cost on invokeSummary is authoritative from @x12i/funcx (or gateway when using AIGatewayInvoker) — xynthesis is pass-through only; do not post-price with @x12i/ai-tools. Gateway 10.x rules for MAIN: UPGRADING_AI_GATEWAY_10.md in @exellix/ai-skills.
Orchestrator pin: @exellix/ai-tasks ≥ 10.10.0 depends on @exellix/xynthesis ^4.9.2, @exellix/ai-skills ^7.0.1, @x12i/funcx ^4.9.19, and optional @exellix/fact-guard-* ^1.0.0 (monorepo: file:../fact-guard/packages/...; npm publish order in ../fact-guard/RELEASE.md).
Fact-guard (optional)
When hosts pass factGuard + sourceMaterial on executeXynthesisAction or runXynthesisAiAction, output is checked against approved source atoms after the LLM hop. Supports observe, fallback, retry (with violation feedback appended to the user prompt), and fail. Disabled by default; set FACT_GUARD_DISABLED=1 to bypass all guards.
await executeXynthesisAction({
// … gateway, prompts, workScope, outputExpectation …
factGuard: {
enabled: true,
policyId: "network-rca",
onViolation: "retry",
maxRetries: 2,
},
sourceMaterial: "Host 203.0.113.0/24",
stage: "post-polish",
});Tests: npm run test:fact-guard. See ../fact-guard/README.md.
Install
npm install @exellix/xynthesisPublished publicly on npm. No custom registry or auth token is required to install.
To publish from this repo (maintainers only), configure npm for the @exellix scope and set NPM_TOKEN — see .npmrc.example.
Environment
Logging
This package uses @x12i/logxer (≥ 4.5.0 stack support).
| Identity field | Value | Purpose |
|---|---|---|
| packageName | Xynthesis | package column in logs, shadow, Mongo, getJobLogs({ package }) |
| envPrefix | XYNTHESIS | Env vars and stack keys (XYNTHESIS_LOGS_LEVEL, LOGXER_PACKAGE_LEVELS) |
| debugNamespace | xynthesis | DEBUG=xynthesis:* filter |
Exported as XYNTHESIS_LOGXER_PACKAGE (object) and XYNTHESIS_LOG_ENV_PREFIX. Distinct from the scoped npm name @exellix/xynthesis.
| Variable | Values | Default | Behavior |
|---|---|---|---|
| XYNTHESIS_LOGS_LEVEL | off, error, warn, info, debug, verbose | warn | Log threshold for this package |
| XYNTHESIS_LOG_LEVEL | same | — | Legacy alias when XYNTHESIS_LOGS_LEVEL is unset |
| XYNTHESIS_VERBOSE | true / 1 / yes | off | Attach full sanitized LLM request/response at logxer verbose (still requires threshold verbose) |
| LOGXER_PACKAGE_LEVELS | XYNTHESIS:debug,OTHER:warn | — | Bulk stack config (host startup) |
Console app: is the host app (from cwd package.json); package: is Xynthesis from this library.
Programmatic stack (forward on public APIs via logging):
import { type StackLoggingOptions } from "@x12i/logxer";
import { runXynthesisAiAction, XYNTHESIS_LOG_ENV_PREFIX } from "@exellix/xynthesis";
const logging: StackLoggingOptions = {
packageLevels: { [XYNTHESIS_LOG_ENV_PREFIX]: "debug" },
};
await runXynthesisAiAction({
actionId: "audit",
actionPhase: "post",
gateway: { /* … */ },
identity: { jobId: "j1", taskId: "t1" },
templateContext: { /* … */ },
logging,
config: { verbose: true },
});Coverage by path (when XYNTHESIS_LOGS_LEVEL=info you get one completion summary per hop):
| Path | info | debug | verbose + XYNTHESIS_VERBOSE |
|---|---|---|---|
| executeXynthesisAction | completion summary (duration, model, usage) | invoke start, routing/cost detail | sanitized prompts + response |
| runSidekickGatewayCall | gateway completion (action, duration, usage) | template load, legacy maxTokens | via nested invoke |
| runXynthesisFinalize | finalize completion (template, mode, repair flag) | template load | via nested invoke |
The package exports xynthesisLogger, createXynthesisLogger, buildWorkScopeLogMeta / createWorkScopedLogger (work-scope jobId / taskId + runtimeIdentity), xynthesisLogxerClient, and optional helpers (resolvePackageLogsLevel, isXynthesisVerbose, applyPackageLogLevelsFromEnv, …).
Catalox (optional)
Catalox 6 / Mongo: xynthesis in this repo uses
@x12i/catalox@^6.0.0and Mongo bootstrap.@exellix/[email protected]is prepared locally but not published until@x12i/[email protected]ships. Seedocumenations/catalox-6-rollout.md.
Live Catalox integration uses @x12i/catalox with the Mongo bootstrap from @x12i/catalox/mongo (createCataloxFromMongoEnv). Typical env:
| Variable | Purpose |
|---|---|
| XYNTHESIS_CATALOX_ACTIONS=1 | On: sidekick + finalize template bodies load only from Catalox (no filesystem fallback for those paths). Required for runXynthesisAiAction. Off (default): disk templates under templates/ for dev/tests. |
| XYNTHESIS_CATALOX_APP_ID or CATALOX_APP_ID | Catalox app id (CLI and library agree on CATALOX_APP_ID). |
| XYNTHESIS_CATALOX_MODEL_CATALOG_ID | Overrides the native catalog id for strategy rows (default xynthesis, same as app metadata.cataloxId). Used by warmCataloxModelRoutingFromEnv() and validation. |
| XYNTHESIS_CATALOX_SKIP_MODEL_WARM=1 | Skip warmCataloxModelRoutingFromEnv() even when an app id is configured. |
| XYNTHESIS_CATALOX_APP_NAME | Display name written when seeding the App record (default Xynthesis). |
| MONGO_URI or MONGODB_URI (required), optional MONGO_DB_NAME / MONGODB_DB | Mongo persistence for xynthesis Catalox seed / validate / createCataloxFromMongoEnv. assertXynthesisCataloxMongoConfigured runs before bootstrap. |
| npm run test:catalox-mongo | Uses testMongoConnectionFromEnv from @x12i/catalox/mongo plus xynthesis Mongo assert — does not write catalogs. |
Injecting your own catalox + mongoStore via CataloxBootstrapInjected bypasses this env check (you supply credentials however you want).
See Model selection → Catalox below for validateCataloxXynthesis, initCataloxXynthesis, and npm scripts. Integration status vs Catalox (presets, probe, native-map): docs/catalox-upstream-requests.md. Full list in .env.example.
Activix / Mongo telemetry (synthesis action records)
When MONGO_LOGS_URI or MONGO_URI is set, xynthesis initializes @x12i/activix in database mode and writes telemetry for LLM calls (collection default xynthesis-actions, overridable via XYNTHESIS_ACTIVIX_COLLECTION). outer.metadata.actionType is pre-action or post-action; jobId / taskId are on runContext and metadata. Query guide: docs/activix-activity-queries.md.
v3.3.3+: The Activix constructor always receives diagnostics.owner (default @exellix/xynthesis) so the built-in activix-collections registry can initialize. Older builds without this field failed init() with errors about diagnostics.owner / collectionRegistry.owner — that was a xynthesis bug, not something hosts should paper over in tests.
| Variable | Purpose |
|---|---|
| XYNTHESIS_ACTIVIX=FALSE (or typo XYNTESIS_ACTIVIX) | Do not construct Activix (no client, no Mongo init, no Optimixer). Use in tests / dev without Mongo — not the same as collection state: 'off' (see Activity Tracking). |
| XYNTHESIS_ACTIVIX_DIAGNOSTICS_OWNER | Optional override for diagnostics.owner (npm package id) on Activix legend / diagnostics; use your app id if multiple services share one Mongo deployment and you want registry rows attributed to the host. |
@x12i/funcx / FuncxInvoker reads OPENROUTER_API_KEY for OpenRouter.
Quick start
Catalox-backed sidekick hop (hosts)
import { runXynthesisAiAction } from "@exellix/xynthesis";
// process.env.XYNTHESIS_CATALOX_ACTIONS = "1" (+ Mongo URI + app id — see Catalox section)
const payload = await runXynthesisAiAction({
actionId: "audit",
actionPhase: "post",
gateway: {
aiRequestId: "req-audit-1",
agentId: "my-app-agent",
sessionId: "sess-1",
temperature: 0.2,
},
identity: { jobId: "job-1", taskId: "task-1", sessionId: "sess-1" },
config: { model: "cheapest", timeoutMs: 45_000 },
templateContext: {
templateCores: ["evaluation"],
task: "Is the output grounded?",
aiOutput: "The issue is rated Critical (CVSS 9.8).",
usedInstructions: "Be factual.",
usedPrompt: "Summarize severity.",
},
});Use buildSidekickInputFromTemplateContext (also exported) if you need to validate templateContext before calling runSidekickGatewayCall directly.
Markdown mode
import {
loadSynthesisTemplates,
buildSynthesisSystemPrompt,
executeXynthesisAction,
} from "@exellix/xynthesis";
const { system, user } = await loadSynthesisTemplates();
const systemPrompt = buildSynthesisSystemPrompt(
renderedInstructions, // downstream task instructions (fully rendered)
renderedPrompt, // downstream task user message (fully rendered)
sourceMaterial, // pre-built local+supporting material string
system
);
const gateway = {
aiRequestId: "req-example-md-1",
agentId: "my-app-agent",
sessionId: "sess-example-md",
temperature: 0.2,
};
const contextMarkdown = await executeXynthesisAction({
gateway,
prompts: { systemPrompt, userPrompt: user },
workScope: { jobId: "job-example-md", taskId: "task-example-md" },
model: "openai/gpt-5-nano",
timeoutMs: 30_000,
maxOutputLength: 8000,
});Structured mode
import {
runStructuredSynthesisGatewayCall,
buildSynthesizedContextMarkdown,
discoverTemplateCores,
} from "@exellix/xynthesis";
const { templateCores } = discoverTemplateCores({
templates: [
{ name: "instructions", content: rawInstructions },
{ name: "prompt", content: rawPrompt },
],
});
const payload = await runStructuredSynthesisGatewayCall({
templateCores,
question,
localMarkdown, // caller-built: Narrix + memory markdown
supportingMarkdown, // caller-built: web evidence markdown
renderedInstructions,
renderedPrompt,
gateway: {
aiRequestId: "req-example-structured-1",
agentId: "my-app-agent",
sessionId: "sess-example-structured",
temperature: 0.2,
},
model: "x-ai/grok-4.1-fast",
timeoutMs: 45_000,
jobId: "job-example-structured",
taskId: "task-example-structured",
});
const contextMarkdown = buildSynthesizedContextMarkdown(payload);Sidekick gateway (audit, fix, pick-best, craft-final, or synthesis)
Prefer runXynthesisAiAction when Catalox is enabled (see above). Use runSidekickGatewayCall(input, execConfig) when you already have a discriminated SidekickInput. Template source: Catalox if XYNTHESIS_CATALOX_ACTIONS=1, else templates/{actionType}/ on disk (same layout as structured synthesis for synthesis).
import {
runSidekickGatewayCall,
resolveModelForAction,
ModelPick,
} from "@exellix/xynthesis";
// Optional: inspect per-action resolution
const auditCheap = resolveModelForAction("audit", ModelPick.Cheapest);
const payload = await runSidekickGatewayCall(
{
actionType: "audit",
templateCores: ["evaluation"],
task: "Is the output grounded in the stated facts?",
aiOutput: "The issue is rated Critical (CVSS 9.8).",
usedInstructions: "Be factual.",
usedPrompt: "Summarize severity.",
},
{
model: "cheapest",
timeoutMs: 45_000,
maxTokens: 4096,
jobId: "job-example-audit",
taskId: "task-example-audit",
gateway: {
aiRequestId: "req-example-audit-1",
agentId: "my-app-agent",
sessionId: "sess-example-audit",
temperature: 0.2,
},
}
);
if (payload.actionType === "audit") {
console.log(payload.verdict, payload.findings.length);
}The example above sets maxTokens without outputExpectation, which selects legacy passthrough (exactly 4096 tokens — automation off). To use automatic budgeting instead, omit maxTokens or pair maxTokens with outputExpectation (see Max tokens: automation, visibility, and tuning).
Set templateMode: "markdown" in execConfig to load templates/{action}/system.md and user.md instead of *-structured.md (same rendrix variables; friendlier prose). Default is "structured".
Identity / xynthesisStrategies: execConfig.model quality picks (cheapest / balanced / best) are resolved to concrete OpenRouter ids before the inner LLM call. The gateway still merges the requested pick into payload.identity.xynthesisStrategies on the way out, so hosts can see which tier was chosen alongside the resolved model in telemetry.
Multi-step flows: runSidekickAuditThenFix runs audit then fix (with priorAudit filled in). runSidekickAuditFixCraft adds craft-final, using fixPayloadToDraftText on the fix result as the craft draft unless you pass craftAiOutputOverride.
createGatewaySidekickComponent(execConfig) returns a GatewaySidekickComponent with run(input) for a fixed exec config.
Terminal graph finalization (xynthesis/finalize)
Use this for a terminal graph node that produces the graph’s canonical output from upstream structured artifacts.
- Structured mode (default): returns
output.parsed(a JSON object). IfoutputContractIdis set, xynthesis validatesparsedagainsttemplates/finalize/contracts/<outputContractId>.schema.jsonusing AJV. - Text mode: returns
output.text(raw text/markdown). Use only when your canonical result is intentionally not structured.
When XYNTHESIS_CATALOX_ACTIONS=1, finalize instructions / prompt / contracts load only from Catalox (finalize-templates catalog). When off, files load from templates/finalize/<templateId>/:
instructions.mdprompt.mdassumptions.md(optional)repair.instructions.md(optional; only used ifenableRepairOnceis set)
Optional env override for disk mode: XYNTHESIS_FINALIZE_TEMPLATES_PATH (root for templates/finalize/...).
import { runXynthesisFinalize, XYNTHESIS_FINALIZE_UTILITY_KEY } from "@exellix/xynthesis";
console.log(XYNTHESIS_FINALIZE_UTILITY_KEY); // "xynthesis/finalize"
const out = await runXynthesisFinalize(
{
templateId: "triage.finalize.v1",
inputs: {
decisions: { shouldUseWeb: true },
evidence: { /* upstream artifacts */ },
},
outputContractId: "triage.finalize.v1",
jobId: "job-123",
taskId: "task-finalize-1",
gateway: {
aiRequestId: "req-finalize-1",
agentId: "my-app-agent",
sessionId: "sess-graph-123",
temperature: 0.2,
},
metadata: { graphId: "graph-123", finalizerNodeId: "finalize-1" },
// outputMode: "structured" (default) | "text"
},
{
model: "best",
timeoutMs: 60_000,
enableRepairOnce: true, // optional; structured-mode only
}
);
// structured mode:
console.log(out.parsed);Custom invoker (telemetry, routing, or tests)
setSynthesisInvoker replaces the default FuncxInvoker. Prefer delegating to FuncxInvoker or AIGatewayInvoker instead of returning fixed text, unless you are in a fully isolated unit test outside this repo’s npm scripts.
import { setSynthesisInvoker, FuncxInvoker } from "@exellix/xynthesis";
const inner = new FuncxInvoker();
setSynthesisInvoker({
async invoke(opts) {
// e.g. metrics, logging, then real call:
return inner.invoke(opts);
},
});
// restore package default:
setSynthesisInvoker(null);Caller responsibilities (not in this package)
| Concern | Where it lives |
|---|---|
| Memory enrichment (enrichMemoriesWithScoping) | Caller orchestrator SDK |
| Narrix/web markdown builders | Caller — pass localMarkdown/supportingMarkdown pre-built |
| Artifact storage (executionMemory.synthesizedContext) | Caller |
| Execution pipeline wiring (PRE step scheduling, fallback rules) | Caller |
Public API
Exports below are from import "@exellix/xynthesis" unless noted. @exellix/xynthesis/ai-actions re-exports the narrower subset in AI actions subset above (no runSidekickGatewayCall, no most Catalox/Activix helpers, no getSynthesisInvoker).
Host LLM entrypoints
| Export | What it does |
|---|---|
| runXynthesisAiAction(request) | Preferred orchestrator entry: actionId + actionPhase, gateway, identity, config, templateContext (data only). Catalox templates only; requires XYNTHESIS_CATALOX_ACTIONS=1. Returns SidekickPayload. |
| buildSidekickInputFromTemplateContext(actionId, templateContext) | Validates templateContext (rejects instructional keys) and builds a SidekickInput for use with runSidekickGatewayCall if you are not using runXynthesisAiAction. |
| runSidekickGatewayCall(input, execConfig) | Runs one sidekick action from a full SidekickInput + RunSidekickExecConfig (includes gateway, jobId, taskId). |
| createGatewaySidekickComponent(execConfig) | Returns { run(input) } with fixed execConfig. |
| runStructuredSynthesisGatewayCall(params) | Structured synthesis only: builds sidekick synthesis input, calls gateway, returns SynthesizedPromptPayload. |
| executeXynthesisAction(request) | Single LLM hop with ExecuteXynthesisActionRequest; returns ExecuteXynthesisActionResult (text + optional invokeSummary). |
| flattenExecuteXynthesisActionRequest(request) | Flatten nested request to invoker fields (advanced callers/tests). |
| sidekickActionToActivixActionType, buildActivixActivityMetadata | Activix metadata helpers (pre-action / post-action). |
| setSynthesisInvoker / getSynthesisInvoker | Replace or read the package SynthesisInvoker (default FuncxInvoker). getSynthesisInvoker is root only (not on ai-actions). |
Sidekick pipelines & parsers
| Export | What it does |
|---|---|
| runSidekickAuditThenFix, runSidekickAuditFixCraft, fixPayloadToDraftText | Multi-step audit → fix → optional craft-final helpers. |
| parseAuditPayload, parseFixPayload, parsePickBestPayload, parseCraftFinalPayload, parseSidekickNonSynthesisPayload | Parse model JSON/text into typed payloads. |
| invokeFuncxRun, resolveSidekickFuncxFunctionId, sidekickFuncxCatalogMap | FuncX run() for all sidekick / execution-strategy hops; prompts from @x12i/funcx/content-seed. |
Markdown / structured synthesis (non–sidekick-gateway)
| Export | What it does |
|---|---|
| loadSynthesisTemplates, buildSynthesisSystemPrompt, buildSynthesisUserPrompt | Markdown-mode template load + prompt assembly. |
| loadStructuredSynthesisTemplates, buildStructuredSynthesisSystemPrompt, buildStructuredSynthesisUserPrompt | Structured-mode template load + prompt assembly. |
| discoverTemplateCores, getRenderedTemplates, renderTemplate | Template core discovery, skill rendering, minimal {{path}} renderer. |
| buildSynthesizedContextMarkdown, parseAndValidateSynthesizedPromptPayload, normalizeAndValidateSynthesizedPayload | Markdown from structured payload; parse/validate JSON payloads. |
| resolveSynthesisQuestion | Resolve task question string from request/config. |
| setContextSynthesizer, getContextSynthesizer, createGatewayStructuredContextSynthesizer | Pluggable ContextSynthesizer; factory backed by runStructuredSynthesisGatewayCall. |
Finalize
| Export | What it does |
|---|---|
| runXynthesisFinalize(input, exec?), XYNTHESIS_FINALIZE_UTILITY_KEY | Terminal graph finalize LLM utility; structured or text outputMode. |
Models & token budgeting
| Export | What it does |
|---|---|
| resolveXynthesisModel, ModelPick, isModelPick | Model alias resolution via @x12i/ai-profiles. |
| getModelRoutingJsonPath, loadModelRoutingFile, ACTION_OUTPUT_DEFAULTS | Load models/model-routing.json per-action output expectations (prompt hints + Optimixer input). |
| resolveEffectiveMaxTokens, completeOptimixerMaxTokensPrediction, getOptimixer, getOptimixerAcceptableRisk | Required @x12i/optimixer max token predict for every LLM hop. |
| measureInputWords, estimateTokens, DEFAULT_TOKEN_WORDS_RATIO, renderOutputConstraintsBlock, resolveOutputExpectation, resolveOutputWordsForBudget, UNKNOWN_ACTION_OUTPUT_EXPECTATION | Prompt <output_constraints> helpers (word targets — separate from max tokens). |
| runSynthesizedContextPreStep, SYNTHESIS_FALLBACK_TO_DIRECT_WARN_CODE | PRE synthesized-context helper with optional fallbackToDirect. |
Errors & invoker types
| Export | What it does |
|---|---|
| XynthesisInvokeError, XynthesisResponseParseError, buildInvokeAttemptSummary | Typed failures with optional invokeSummary. |
| FuncxInvoker, AIGatewayInvoker, AIFunctionsInvoker | Concrete SynthesisInvoker implementations (gateway alias deprecated). |
| Types SynthesisInvoker, InvokeAttemptSummary, ExecuteXynthesisActionResult, ExecuteXynthesisActionRequest, etc. | Wire shapes for invoker and results. |
Logging
| Export | What it does |
|---|---|
| xynthesisLogger, createXynthesisLogger, createWorkScopedLogger, buildWorkScopeLogMeta, XYNTHESIS_LOGXER_PACKAGE, XYNTHESIS_LOG_ENV_PREFIX, isXynthesisVerbose, xynthesisLogxerClient | @x12i/logxer integration, work-scoped envelopes, and job log query (package: "Xynthesis"). |
| resolvePackageLogsLevel, parsePackageLogsLevelString, packageLogsLevelEnvKey, legacyPackageLogLevelEnvKey | Package log level helpers from @x12i/logxer. |
Catalox (catalogs & seed)
| Export | What it does |
|---|---|
| isCataloxActionsEnabled, warmXynthesisActionsFromCatalox, clearXynthesisActionStore, getActionDefinition, getFinalizeDefinition, getSurfaceDefinition, findFinalizeDefByContractId, xynthesisActionStoreSnapshot | Action/finalize/surface cache backed by Catalox when enabled. |
| warmCataloxModelRoutingFromEnv, clearCataloxModelOverlay, getCataloxOverlayForAction, getCataloxOverlayGlobalPick | Strategy rows overlay for resolveModel. |
| initCataloxXynthesis (initCatalox), validateCataloxXynthesis, getCataloxAppIdFromEnv, assertXynthesisCataloxMongoConfigured, buildXynthesisSeedPayloads, countXynthesisSeedPayloads | Bootstrap, validation, credentials assert, seed payload builders. |
| cataloxGlobalStrategyItemId, cataloxActionStrategyItemId, cataloxSurfaceItemId, listExpectedCataloxStrategyItemIds, listExpectedCataloxSurfaceItemIds | Stable Catalox itemId helpers and lists. |
| Constants XYNTHESIS_CATALOX_METADATA_ID, XYNTHESIS_PRE_ACTIONS_CATALOG_ID, XYNTHESIS_POST_ACTIONS_CATALOG_ID, XYNTHESIS_FINALIZE_TEMPLATES_CATALOG_ID, XYNTHESIS_SURFACES_CATALOG_ID, XYNTHESIS_CATALOX_MODEL_CATALOG_ID_DEFAULT, getXynthesisCataloxModelCatalogIdFromEnv, XYNTHESIS_CATALOX_MONGO_BOOTSTRAP_OPTIONS | Catalog ids and env-driven model catalog id. |
| Types CataloxValidationReport, XynthesisActionItem, FinalizeItem, … | Catalox payload shapes. |
Activix & runtime
| Export | What it does |
|---|---|
| getActivix, closeActivix, getActivixWithInitBudget, getActivixInitWaitMs, getActivixTelemetryBudgetMs, buildActivixOptions | Mongo-backed activity telemetry client and constructor helpers. |
| runtimeObjects | Package-level observability handles (lastJobId, optional query clients). |
Constants & enums
| Export | What it does |
|---|---|
| TASK_CORES, SIDEKICK_ACTION_TYPES | Allowed template cores and sidekick action type literals. |
Types (selection)
Contract and config types (SynthesisInput, SidekickInput, RunSidekickExecConfig, RunStructuredSynthesisParams, RunXynthesisAiActionRequest, ActivityRunContext, SynthesisCallGatewayContext, OutputExpectation, CataloxSeedPayloads, …) are exported from the root entry; see dist/index.d.ts or source src/index.ts for the full list.
Activity Tracking
Every synthesis call can be tracked as an activity via @x12i/activix (v8+). The package pins @x12i/activix ^8.4 (see package.json). Storage is configured once in the Activix constructor (mongoUri, storageMode: "database", collection, …). Per call, correlation uses runContext (not end-user identity) and the canonical document-root activity I/O: outer (required) and optional inner, each with input / output (or request / response) and metadata — not a nested structure wrapper.
Lifecycle ownership: Activix sets activityId, status, startTime, endTime, duration, and related fields. Xynthesis supplies runContext, outer, and metadata only — never lifecycle-owned fields.
Collection tracking state (8.4+): Each activix-collections legend row may set state: 'track' | 'off'. When off, Activix skips Mongo writes but xynthesis still calls startRecord / completeRecord / failRecord unchanged. Do not branch on state in xynthesis code; ops use setCollectionTrackingState or edit the legend and refreshCollectionTrackingStates(). This is separate from XYNTHESIS_ACTIVIX=FALSE, which prevents constructing Activix at all.
Runtime queries: After init, runtimeObjects.activixClient exposes getJobActivities({ jobId, … }) (same instance as getActivix()). See docs/runtime-objects-observability.md.
Timeouts vs telemetry: executeXynthesisAction timeoutMs applies only to the LLM invoker (plus local trim / maxOutputLength). Activix init() and startRecord / completeRecord / failRecord run after the model returns, on separate budgets: XYNTHESIS_ACTIVIX_INIT_TIMEOUT_MS (default 5000) caps how long telemetry waits on Mongo init; XYNTHESIS_ACTIVIX_TELEMETRY_BUDGET_MS (default 15000) caps the whole post-call Activix write path. Slow or blackholed Mongo must not surface as Synthesis call timeout.
Activix metadata: outer.metadata.actionType is pre-action (synthesis) or post-action (audit, fix, finalize, …). outer.metadata.jobId and outer.metadata.taskId duplicate runContext for Mongo queries. Optional outer.metadata.sidekickAction holds the Catalox item id (audit, fix, …). See docs/activix-activity-queries.md.
Catalogs (Catalox) vs activity (Activix): published strategy and surface-type metadata live in @x12i/catalox (see listExpectedCataloxStrategyItemIds, warmCataloxModelRoutingFromEnv, validateCataloxXynthesis, initCataloxXynthesis, and .env.example). Sidekick phase enums can be stored as native catalogs pre-actions (synthesis) and post-actions (audit, fix, pick-best, craft-final). Activix owns durable run rows for synthesis; the synthesis hot path does not use legacy nx-mongo directly.
Operational integration checklist: docs/activix-integration-best-practices-checklist.md
Note: Activity tracking is enabled by default when
MONGO_LOGS_URI(orMONGO_URI) is set. To skip Activix entirely (tests, no Mongo), setXYNTHESIS_ACTIVIX=FALSE. To pause Mongo writes in a shared deployment while keeping the client alive, use ActivixsetCollectionTrackingState('xynthesis-actions', 'off')instead.
- Primary key:
activityId(prefer over legacy aliasrecordId). - Prefix:
act-(e.g.,act-eb47a60e…). - Collection:
XYNTHESIS_ACTIVIX_COLLECTIONoverrides; otherwise defaultxynthesis-actions.
Run context (work hierarchy) — not user identity
runContext describes where this AI call sits in your work tree (job, task, session, optional leaf ids like aiRequestId, executor instance). It is not Activix configuration, not end-user or tenant identity (put tenant/user in outer.metadata if you need them stored on the row), and not set in the Activix constructor.
In xynthesis, pass extra envelope fields on identity (historical name) or use the type alias ActivityRunContext. jobId and taskId must be supplied on the call (executeXynthesisAction, RunSidekickExecConfig, RunStructuredSynthesisParams, runXynthesisFinalize) so Activix and @x12i/logxer stay correlated; they are merged into runContext and outer.metadata, and echoed on the result identity. Optional top-level agentId on those calls is also merged. Activix may warn if sessionId is missing; supply it on identity for correlation.
import type { ActivityRunContext } from "@exellix/xynthesis";
const runContext: ActivityRunContext = {
sessionId: "job-20240315-001",
jobId: "job-20240315-001",
taskId: "task-cust-001",
linkedActivity: {
activityId: "act-parent-123",
collection: "upstream-activities",
},
};
await executeXynthesisAction({
gateway: {
aiRequestId: exec.aiRequestId,
agentId: exec.agentId,
sessionId: runContext.sessionId ?? exec.jobId,
temperature: 0.2,
},
prompts: { systemPrompt: "…", userPrompt: "…" },
workScope: { jobId: exec.jobId, taskId: exec.taskId, identity: runContext },
});See @x12i/activix docs for the full run-context model (job → task → skill → AI activity), findRecordsByRunContext, and getJobActivities.
Max tokens (Optimixer) and output constraints (prompt hints)
Every LLM hop resolves completion maxTokens via required @x12i/optimixer (resolveEffectiveMaxTokens → predictAiMaxTokens). There is no local heuristic, no fixed default (8192), and no bypass path. Problems with cold-start, ceilings, init, or predict failures → fix in @x12i/optimixer (see docs/specs/optimixer-max-tokens-contract.md).
Output constraints (word/density hints in the system prompt) are separate from max tokens. Sidekick gateways always resolve outputExpectation from models/model-routing.json actionOutputDefaults (or caller override) and append <output_constraints>.
Single path
| Step | Behavior |
|---|---|
| 1 | Resolve outputExpectation (sidekick: per-action JSON default; finalize: craft-final default; direct invoke: caller must supply). |
| 2 | Call Optimixer predictAiMaxTokens with prompt size, model profile, structured flags, and optional callerMaxTokens ceiling. |
| 3 | Pass resolved maxTokens to the LLM invoker. |
| 4 | After invoke, call completeAiMaxTokensPrediction with provider finishReason and predictionContext. When Optimixer returns retryPrediction (output hit the token limit), xynthesis re-invokes once with the bumped cap (still subject to optional caller ceiling). |
Automatic max-token retry: Up to one retry (OPTIMIXER_MAX_TOKEN_RETRY_MAX_ATTEMPTS = 2 → initial + bump). Requires provider finishReason (e.g. length); if missing, xynthesis logs OPTIMIXER_FINISH_REASON_MISSING and does not retry (no local heuristics). Observability: invokeSummary.executionMetadata.optimixerRetryCount, previousRecommendedMaxTokens, bumpedMaxTokens, log code OPTIMIXER_MAX_TOKENS_RETRY.
await runSidekickGatewayCall(input, {
...execBase,
maxTokens: hostComputedCeiling, // optional — caps Optimixer, does NOT bypass it
jobId,
taskId,
});Knobs
| Knob | Role |
|---|---|
| outputExpectation | Required on every hop. Overrides per-action word/density defaults for prompt hints and Optimixer predict input. Not a token count — Optimixer resolves the numeric cap internally. |
| reasoningEffort | Optional Optimixer reasoning tier (high, medium, low, none, not-applicable). Sidekick hops also read Catalox catalog defaults when set. |
| gateway.temperature | Required sampling temperature on every hop (canonical location). Echoed in invokeSummary.executionMetadata.llmInvokeParams. |
| topP | Optional sampling top-p on hop request / exec config. Forwarded to the invoker when set. |
| maxTokens | Optional ceiling on Optimixer output (callerMaxTokens). Orchestrators should omit unless product needs a hard cost guardrail. |
| structuredModeMultiplier | Forwarded to Optimixer predict for structured JSON hops (default 1.2). |
| XYNTHESIS_OPTIMIXER_ACCEPTABLE_RISK | Risk tuning for Optimixer predict only. |
| models/model-routing.json | Per-action actionOutputDefaults (not model caps). Validate with npm run validate:models. |
Do not import resolveMaxTokens from @exellix/xynthesis/ai-actions — it was removed in 4.2. Pass outputExpectation and call executeXynthesisAction (or resolveEffectiveMaxTokens for advanced use).
Visibility
| Signal | What it shows |
|---|---|
| invokeSummary | maxTokensFromCaller, maxTokensEffective, model fields, executionMetadata.llmInvokeParams (temperature, topP), executionMetadata.optimixerRetryCount / bumpedMaxTokens when a max-token retry ran, tokenResolution metadata (reason: "optimixer", reasoningEffort, Optimixer diagnostics). |
| XYNTHESIS_LOGS_LEVEL=debug | Work-scoped maxTokens resolved logs. |
| Errors | XynthesisInvokeError / XynthesisResponseParseError carry invokeSummary. |
PRE synthesis: fallbackToDirect
| Setting | Where | Default |
|---|---|---|
| fallbackToDirect | PRE node SynthesisConfig (call config) | false — not an env var |
When true, synthesis failure returns empty contextMarkdown and MAIN continues. Empty context is not backfilled from Narrix/web. xynthesis emits SYNTHESIS_FALLBACK_TO_DIRECT via logxer when fallback is used.
Use runSynthesizedContextPreStep({ config, run }) or replicate the same warn contract in your orchestrator.
Model selection and routing
- Models: callers pass
@x12i/ai-profilesprofile/choice(e.g.cheap/default,pro/default) or shortcuts (cheapest,best); xynthesis resolves to a concrete wire id for funcx. Required on every hop — no package default model; do not pass provider model ids intoexecuteXynthesisAction. models/model-routing.json(v3):actionOutputDefaultsonly (output expectations per sidekick action). Model caps live in Optimixer / ai-profiles.
Model routing (OpenRouter vs direct)
Controlled by preferOpenRouterPolicy / resolveXynthesisModel:
| Condition | Routing |
|---|---|
| PREFER_OPENROUTER=true (default) + API key present | OpenRouter |
| PREFER_OPENROUTER=true + key absent | Vendor-direct + warning in policy snapshot |
| PREFER_OPENROUTER=false | Always vendor-direct |
Env: PREFER_OPENROUTER. Key: OPENROUTER_API_KEY.
Secondary model on LLM failure
xynthesis does not retry with an alternate model. When @x12i/ai-profiles adds secondary-model support, xynthesis passes through resolution/routing only.
Usage
import { ModelPick, resolveXynthesisModel, executeXynthesisAction, ACTION_OUTPUT_DEFAULTS } from "@exellix/xynthesis";
const resolved = await resolveXynthesisModel(ModelPick.Balanced);
await executeXynthesisAction({
gateway: { aiRequestId: "req-1", agentId: "my-app", sessionId: "sess-1", temperature: 0.2 },
prompts: { systemPrompt: "…", userPrompt: "…" },
workScope: { jobId: "job-1", taskId: "task-1" },
model: ModelPick.Balanced,
outputExpectation: ACTION_OUTPUT_DEFAULTS.synthesis,
});Optional Catalox overlay
When an app id is set, warmCataloxModelRoutingFromEnv() can overlay strategy rows. See initCataloxXynthesis / validateCataloxXynthesis for bootstrap and validation.
Template core values
Valid {{core:VALUE}} values: question, action, plan, objective, decision, comparison, classification, evaluation, analysis, summary, generation, extraction.
Custom synthesis prompt templates
Place files at (base path: SYNTHESIS_TEMPLATES_PATH env or process.cwd()):
templates/synthesis/system.md # markdown mode system prompt
templates/synthesis/user.txt # markdown mode user message
templates/synthesis/system-structured.md # structured mode system prompt
templates/synthesis/user-structured.txt # structured mode user messageMissing files fall back to built-in defaults.
Tests in this repo
Default (npm test): validate:models, validate:catalox-catalog, test:token-budget, test:synthesized-context-prestep, test:finalize, test:runtime-objects, test:activix-isolation, test:activix-diagnostics-owner, test:sidekick-gateway, test:build-sidekick-input, test:ai-actions-export — no live OpenRouter calls.
OpenRouter + live integration: npm run test:with-openrouter runs npm run test:integration, which chains npm test, npm run test:live (test/live.ts: markdown + structured + sidekick + custom invoker + optional Activix/Mongo check), npm run test:client-envelope, and npm run test:finalize:live. Requires OPENROUTER_API_KEY where those scripts call the API. Run individual pieces (e.g. npm run test:live only) when iterating.
Client envelope (test/example)
Maps test/example/synthesis-as-input.json through test/example/clientEnvelopeToStructured.ts into runStructuredSynthesisGatewayCall (callers must pass explicit gateway into clientEnvelopeToStructuredParams). There is no mock LLM: the script uses FuncxInvoker (@x12i/funcx, OpenRouter).
Required: OPENROUTER_API_KEY. If the key is missing, the script exits with an error (it does not fall back to fake output).
npm test
npm run test:with-openrouter # full integration chain (live.ts + client-envelope + finalize.live)
# or individual live scripts:
npm run test:live
npm run test:client-envelope
npm run test:finalize:livenpm run test:client-envelope:live is an alias for the client-envelope script (kept for older docs/scripts).
Optional: override the synthesis wait budget (milliseconds) without editing the JSON:
XYNTHESIS_TEST_SYNTHESIS_TIMEOUT_MS=180000 npm run test:client-envelopeTimeouts: executeXynthesisAction applies timeoutMs only to the LLM invoker (not Activix Mongo init()). Structured synthesis with large prompts can exceed short budgets; the sample envelope sets timeoutMs in the pre-step config (mapped to RunStructuredSynthesisParams.timeoutMs). If you see Synthesis call timeout, raise that value or set XYNTHESIS_TEST_SYNTHESIS_TIMEOUT_MS. If Mongo is slow or down, tune XYNTHESIS_ACTIVIX_INIT_TIMEOUT_MS / XYNTHESIS_ACTIVIX_TELEMETRY_BUDGET_MS or set XYNTHESIS_ACTIVIX=FALSE for tests.
Network: If you see ConnectTimeoutError / fetch failed to openrouter.ai, outbound HTTPS to OpenRouter is blocked or unstable (firewall, VPN, proxy). Fix connectivity; there is no offline substitute in this test.
CI: Pipelines can run npm test without OpenRouter. Use npm run test:with-openrouter (or inject OPENROUTER_API_KEY) only when you intend to hit the live API. See docs/reports/testing-requires-openrouter.md if that doc still references the old single-step npm test.
Activity logging: With MONGO_LOGS_URI or MONGO_URI, xynthesis initializes Activix and writes start/complete/fail records. Set XYNTHESIS_ACTIVIX=FALSE to skip Mongo activity writes. The client-envelope test calls closeActivix() in a finally block so the process can exit cleanly.
Broader live suite (@x12i/env CLI)
npx x12i-env run --env-file .env -- npx tsx test/live.tsRuns additional live integration checks: markdown synthesis, core discovery, question resolution, structured round-trip, custom invoker delegating to @x12i/funcx, sidekick audit round-trip, and resolveModelForAction. All LLM-backed steps use the real API (requires a key like the client-envelope test).
Background
This package is @exellix/xynthesis (src/ here). Orchestrators should prefer runXynthesisAiAction from @exellix/xynthesis or @exellix/xynthesis/ai-actions. Smaller ai-actions barrel omits sidekick gateway and most infra exports — see AI actions subset and Public API above. See docs/ for design notes and extraction rationale:
01-current-behavior.md— reference PRE-step behaviour (orchestrator integration)02-downstream-request-contract.md— original ai-gateway contract03-how-to-extract-into-package.md— extraction recipe04-dependencies-and-boundaries.md— boundary decisions
