@exellix/xynthesis
v3.7.0
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. The default FuncxInvoker maps these into funcx attribution.tags (and trace id); it does not invent missing ids.
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 (resolveMaxTokens, resolveOutputExpectation, …), markdown / structured synthesis helpers, errors, and related types. getSynthesisInvoker is only on the root package.
import {
runXynthesisAiAction,
runStructuredSynthesisGatewayCall,
resolveMaxTokens,
} 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.
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
Per-package verbosity uses XYNTHESIS_LOGS_LEVEL (canonical). Legacy XYNTHESIS_LOG_LEVEL is still read if XYNTHESIS_LOGS_LEVEL is not set. If both are unset, the default is warn (warn and error only). To silence this package’s diagnostics, set XYNTHESIS_LOGS_LEVEL=off. To raise verbosity: e.g. XYNTHESIS_LOGS_LEVEL=info or debug. Console/file/format and other host-level options are configured via @x12i/logxer in the consuming app; see logxer package docs for ERC-style package logging.
The package exports xynthesisLogger (a @x12i/logxer instance), buildWorkScopeLogMeta / createWorkScopedLogger (work-scope jobId / taskId + runtimeIdentity for log envelopes), and optional helpers (resolvePackageLogsLevel, packageLogsLevelEnvKey, etc.) for the same rules outside the default logger.
Catalox (optional)
Live Catalox integration uses @x12i/catalox with the Firebase Admin bootstrap from @x12i/catalox/firebase (createCataloxFromEnv). 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). |
| GOOGLE_SERVICE_ACCOUNT_BASE64 (required), FIREBASE_PROJECT_ID, FIRESTORE_DATABASE_ID | Only supported credential path for xynthesis Catalox seed / validate / createCataloxFromEnv (base64-encoded Firebase service account JSON). assertXynthesisCataloxFirebaseBase64Configured runs before bootstrap; FIREBASE_SERVICE_ACCOUNT_PATH and ADC are not used there. |
| npm run test:catalox-firestore | Uses testFirestoreConnectionFromEnv from @x12i/catalox/firebase (probe collection cataloxConnectivityProbe) plus xynthesis base64-only assert — does not write catalogs. Equivalent probe CLI: npx catalox firestore probe (no base64 assert). |
@x12i/catalox ≥ 4.0.3 also ships declarative catalox seed apply (--file or --preset, e.g. builtin:native-map-v1) and applyCataloxSeedPreset on @x12i/catalox/embedder. Xynthesis npm run seed:catalox-action-catalogs still uses initCataloxXynthesis (rich templates from disk) until this package publishes a catalox.seedPreset manifest.
Injecting your own catalox + firestore via CataloxBootstrapInjected bypasses this env check (you supply credentials however you want).
Credential resolution details live in @x12i/catalox (firebase/bootstrap: base64 env first, then explicit path option, then ADC). Xynthesis still requires valid GOOGLE_SERVICE_ACCOUNT_BASE64 for non-injected seed / validate / shared client paths.
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) | Disable Mongo telemetry entirely (no Activix client). |
| 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 (or OPEN_ROUTER_KEY inside funcx) for OpenRouter.
If your env uses OPEN_ROUTER_KEY, bridge it before calling:
if (!process.env.OPENROUTER_API_KEY && process.env.OPEN_ROUTER_KEY) {
process.env.OPENROUTER_API_KEY = process.env.OPEN_ROUTER_KEY;
}Quick start
Catalox-backed sidekick hop (hosts)
import { runXynthesisAiAction } from "@exellix/xynthesis";
// process.env.XYNTHESIS_CATALOX_ACTIONS = "1" (+ Firebase app id env — 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. |
| loadSidekickTemplates, buildSidekickSystemPrompt, buildSidekickUserVars, buildSidekickUserPrompt | Load Catalox or disk templates; build system/user strings and rendrix vars. |
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 |
|---|---|
| resolveModel, resolveModelForAction, isModelPick, OpenRouterModel, ModelPick, MODEL_BY_PICK, ACTION_MODEL_MATRIX | Model id resolution from picks or strings; per-action matrix. |
| getModelRoutingJsonPath, loadModelRoutingFile, MODEL_CAPABILITIES, ACTION_OUTPUT_DEFAULTS, getModelCapabilities | Load models/model-routing.json and derived caps/defaults. |
| resolveMaxTokens, measureInputWords, estimateTokens, DEFAULT_TOKEN_WORDS_RATIO, headroomMultiplier, renderOutputConstraintsBlock, resolveOutputExpectation, resolveOutputWordsForBudget, PACKAGE_FALLBACK_OUTPUT_EXPECTATION | Output sizing and <output_constraints> helpers. |
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, createWorkScopedLogger, buildWorkScopeLogMeta | @x12i/logxer integration and work-scoped envelopes. |
| 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, assertXynthesisCataloxFirebaseBase64Configured, 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_FIREBASE_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 (v7+). The package pins @x12i/activix ^7 (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.
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 explicitly disable writes, setXYNTHESIS_ACTIVIX=FALSE.
- 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) and findRecordsByRunContext.
Max tokens: automation, visibility, and tuning
Automatic completion budgeting matters for cost, latency, and avoiding truncated JSON on structured actions. This package treats maxTokens differently depending on which entrypoint you use.
Where automation runs
| Entrypoint | Automatic maxTokens? | Default when unset |
|---|---|---|
| runSidekickGatewayCall, runStructuredSynthesisGatewayCall | Yes — see §Decision rules below | Resolver output (no fixed 8192) |
| executeXynthesisAction (direct), runXynthesisFinalize | No — effective cap computed in xynthesis then passed as required maxTokens on the wire | 8192 when unset on the request (see executeXynthesisAction.ts) |
The resolver reads models/model-routing.json: per-action actionOutputDefaults (word targets / density), per-model models rows (contextWindow, maxOutputTokens, completionTokenCost), and the measured prompt. It also appends an <output_constraints> block to the system prompt so the model sees human-readable length hints.
Implementation anchors: resolveMaxTokens (src/resolveMaxTokens.ts), runSidekickGatewayCall (src/sidekickGateway.ts), outputExpectation / ACTION_OUTPUT_DEFAULTS (src/outputExpectation.ts, src/modelRoutingLoader.ts).
Decision rules (sidekick gateways only)
Let exec be RunSidekickExecConfig (or the equivalent fields on RunStructuredSynthesisParams).
Legacy passthrough (automation OFF) —
exec.maxTokensis set andexec.outputExpectationisundefined.- Exactly
exec.maxTokensis sent to the LLM. - Metadata records
tokenResolution.mode: "legacy-maxTokens-passthrough".
- Exactly
Automatic resolution (automation ON) — any other case, including:
maxTokensomitted, oroutputExpectationprovided (with or withoutmaxTokens).- The package computes a budget from prompt size +
resolveOutputExpectation(actionType, exec.outputExpectation)(caller override → JSON default → package fallback). - If
exec.maxTokensis set while automation is on, it is a hard ceiling:finalMaxTokens = min(computed, exec.maxTokens)(seeresolveMaxTokenscallerMaxTokens).
Structured synthesis extras — when
actionType === "synthesis"andtemplateModedefaults tostructured, the resolver applies a structured JSON multiplier (default 1.2) and a floor so small budgets do not starve schema-heavy outputs. OverridestructuredModeMultiplieronexecper call if needed.
Critical pattern — dynamic ceiling without disabling automation:
If you compute maxTokens per request but want automation to stay on, you must not use the legacy combination above. Pass an explicit outputExpectation (even one copied from defaults for that action):
import { ACTION_OUTPUT_DEFAULTS } from "@exellix/xynthesis";
await runSidekickGatewayCall(
{
actionType: "audit",
templateCores: ["evaluation"],
task: "Check grounding.",
aiOutput: modelDraft,
},
{
...execBase,
outputExpectation: ACTION_OUTPUT_DEFAULTS.audit,
maxTokens: computeBudgetForThisTask(), // ceiling only; resolver still runs
jobId,
taskId,
}
);For another action, substitute ACTION_OUTPUT_DEFAULTS.synthesis, .fix, etc. You can also pass a fully custom outputExpectation (size relative/absolute + density) for per-task sizing without turning off the resolver.
Knobs reference (sidekick)
| Knob | Role |
|---|---|
| outputExpectation | Overrides per-action default sizing expectations used by the resolver and the <output_constraints> block. |
| maxTokens | Legacy: exact cap if outputExpectation omitted. Otherwise: ceiling on the computed budget. |
| structuredModeMultiplier | Scales estimated output tokens for structured synthesis JSON (optional). |
| model / routing | MODEL_CAPABILITIES for the resolved OpenRouter id set ceilings and headroom. |
| models/model-routing.json | Change actionOutputDefaults or models when you want package-wide policy shifts (validate with npm run validate:models). |
Visibility — how to see what was chosen
| Signal | What it shows |
|---|---|
| RunSynthesisResult.invokeSummary | maxTokensFromCaller, maxTokensEffective (wire cap passed to the LLM invoker — default FuncxInvoker / OpenRouter; AIGatewayInvoker maps to ai-gateway config.maxTokens), model fields, optional usage / routing / costUsd when the invoker exposes diagnostics. executionMetadata includes sidekick tokenResolution (resolver reason, estimated token diagnostics). |
| XYNTHESIS_LOGS_LEVEL=debug | Work-scoped log maxTokens resolved with reason and diagnostics (runSidekickGatewayCall). |
| Activix (when enabled) | outer.metadata merges tokenResolution / resolver diagnostics from the gateway path. |
| Errors | XynthesisInvokeError / XynthesisResponseParseError carry invokeSummary for failed invokes or post-LLM parse failures (includes preview + summary). |
Exports: InvokeAttemptSummary, RunSynthesisResult, XynthesisInvokeError, XynthesisResponseParseError, buildInvokeAttemptSummary, resolveMaxTokens, ACTION_OUTPUT_DEFAULTS, PACKAGE_FALLBACK_OUTPUT_EXPECTATION.
Direct executeXynthesisAction / finalize
There is no resolveMaxTokens hook on these paths. Pass maxTokens explicitly when you care about the cap; otherwise the invoker uses 8192. Use maxOutputLength to truncate returned text after the model responds (separate from token limits).
Output sizing and maxTokens (quick recap)
- Sidekick gateways: automatic budgeting unless you intentionally choose legacy passthrough (
maxTokensset andoutputExpectationomitted). maxTokenswith automation ON is always a ceiling, never the sole source of the budget.- Direct
executeXynthesisAction/runXynthesisFinalize: 8192 default via the invoker path; no resolver.
npm run validate:models re-validates model-routing.json (also run as part of npm test).
Model Selection
- Single JSON source of truth:
models/model-routing.jsonat the package root (also published in the npm tarball undermodels/). It definesglobalPicks,actionMatrix, per-modelmodels(completion token cost,contextWindow,maxOutputTokens), andactionOutputDefaults(output expectations per sidekick action). At startup,MODEL_BY_PICKandACTION_MODEL_MATRIXare built from that file;MODEL_CAPABILITIESandACTION_OUTPUT_DEFAULTSmirror the new sections. UsegetModelRoutingJsonPath()if you need the absolute path in tooling or tests. - Future split: validation + pure routing logic are specified for a new
@x12i/openrouter-model-routingpackage;@x12i/ai-gatewayshould own live config reload—seedocs/specs/package-openrouter-model-routing.md. - Optional Catalox overlay: when an app id (
XYNTHESIS_CATALOX_APP_IDorCATALOX_APP_ID) is set, callwarmCataloxModelRoutingFromEnv()during process startup to load native items from the cataloggetXynthesisCataloxModelCatalogIdFromEnv()(defaultxynthesis; override withXYNTHESIS_CATALOX_MODEL_CATALOG_ID). Item ids come fromlistExpectedCataloxStrategyItemIds();data.model(ordata.openRouterModel) overridesresolveModel/resolveModelForActionfor matching picks. SetXYNTHESIS_CATALOX_SKIP_MODEL_WARM=1to disable. If warm fails or items are missing, JSON routing remains authoritative.
Catalox bootstrap and validation
npm run test:catalox-firestore— smoke-test Firebase Admin → Firestore (GOOGLE_SERVICE_ACCOUNT_BASE64only, same as seed); does not write catalogs.npm run seed:catalox-action-catalogs— operator CLI: ensures the App record (metadata.cataloxId=xynthesis) and native catalogspre-actions/post-actionswith one item perSIDEKICK_ACTION_TYPESpartition (requires Firebase credentials + app id).npm run validate:catalox-setup— prints aCataloxValidationReportJSON (exit 1 ifokis false). By default also checks every id fromlistExpectedCataloxStrategyItemIds()in the resolved model catalog (defaultxynthesis). SetXYNTHESIS_CATALOX_VALIDATE_SKIP_MODEL=1to skip strategy-item checks only.npm run validate:catalox-catalog— prints expected strategy/surface itemId strings; withXYNTHESIS_CATALOX_VALIDATE_LIVE=1plus model-catalog env, spot-checkswarmCataloxModelRoutingFromEnv()(part ofnpm test).
Programmatically:
import { initCataloxXynthesis, validateCataloxXynthesis } from "@exellix/xynthesis";
await initCataloxXynthesis(); // idempotent create/fix
const report = await validateCataloxXynthesis();
if (!report.ok) {
console.error(report.issues);
}- Global defaults (
resolveModel+MODEL_BY_PICK): mapcheapest/balanced/bestwhen there is no sidekick action context. - Sidekick actions (
resolveModelForAction+ACTION_MODEL_MATRIX): the same pick strings resolve to different models peractionType(e.g. synthesis vs audit vs pick-best).
Illustrative global picks (see model-routing.json for current IDs):
- Best — strong workhorse default.
- Balanced — production compromise.
- Cheapest — spend-first filtering / lite tasks.
Usage
import { ModelPick, OpenRouterModel, executeXynthesisAction, resolveModelForAction } from "@exellix/xynthesis";
// Sidekick: per-action pick resolution
const modelId = resolveModelForAction("synthesis", ModelPick.Balanced);
const g = (id: string) => ({
aiRequestId: `req-${id}`,
agentId: "my-app",
sessionId: `sess-${id}`,
temperature: 0.2,
});
// 1. Using a pick (global resolution via resolveModel inside executeXynthesisAction)
await executeXynthesisAction({
gateway: g("pick"),
prompts: { systemPrompt: "…", userPrompt: "…" },
workScope: { jobId: "job-model-pick", taskId: "task-model-pick" },
model: ModelPick.Best,
});
// 2. Using the enum
await executeXynthesisAction({
gateway: g("enum"),
prompts: { systemPrompt: "…", userPrompt: "…" },
workScope: { jobId: "job-model-enum", taskId: "task-model-enum" },
model: OpenRouterModel.GPT4oMini,
});
// 3. Using a raw string
await executeXynthesisAction({
gateway: g("string"),
prompts: { systemPrompt: "…", userPrompt: "…" },
workScope: { jobId: "job-model-string", taskId: "task-model-string" },
model: "anthropic/claude-3-haiku",
});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 (prints canonical Catalox itemId lists; optional live warm only when XYNTHESIS_CATALOX_VALIDATE_LIVE=1), test:token-budget, 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. For a full Firestore alignment check (app + pre-actions / post-actions + optional strategy catalog), run npm run validate:catalox-setup with working Firebase credentials (not part of default CI).
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 or OPEN_ROUTER_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 or OPEN_ROUTER_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
