# @exellix/xynthesis

> Standalone pre-actions and post-actions LLM library for Exellix workflows

Latest version **4.9.14** (published 2026-07-12) · 0 weekly downloads

## Install

```sh
npm install @exellix/xynthesis
pnpm add @exellix/xynthesis
yarn add @exellix/xynthesis
bun add @exellix/xynthesis
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 4.9.14 |
| Published | 2026-07-12 |
| First published | 2026-05-26 |
| Weekly downloads | 0 |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 15 |
| Unpacked size | 894.5 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | exellix |

## Links

- npm: https://www.npmjs.com/package/@exellix/xynthesis
- Repository: https://github.com/exellix/ai-tasks-mono-repo
- Homepage: https://github.com/exellix/ai-tasks-mono-repo#readme
- Issues: https://github.com/exellix/ai-tasks-mono-repo/issues
- npm.io page: https://npm.io/package/@exellix/xynthesis

## Dependencies (15)

- [ajv](https://npm.io/package/ajv.md) ^8.17.1
- [flex-md](https://npm.io/package/flex-md.md) ^4.7.4
- [ts-node](https://npm.io/package/ts-node.md) ^10.9.2
- [@x12i/env](https://npm.io/package/@x12i/env.md) ^4.0.3
- [@x12i/funcx](https://npm.io/package/@x12i/funcx.md) ^4.10.4
- [@x12i/logxer](https://npm.io/package/@x12i/logxer.md) ^5.1.0
- [@x12i/activix](https://npm.io/package/@x12i/activix.md) ^9.0.6
- [@x12i/catalox](https://npm.io/package/@x12i/catalox.md) ^6.0.0
- [@x12i/rendrix](https://npm.io/package/@x12i/rendrix.md) ^4.3.0
- [@x12i/optimixer](https://npm.io/package/@x12i/optimixer.md) ^3.6.0
- [@x12i/ai-profiles](https://npm.io/package/@x12i/ai-profiles.md) ^4.4.0
- [@exellix/fact-guard-types](https://npm.io/package/@exellix/fact-guard-types.md) ^1.0.1
- [@exellix/fact-guard-adapters](https://npm.io/package/@exellix/fact-guard-adapters.md) ^1.0.1
- [@exellix/fact-guard-policies](https://npm.io/package/@exellix/fact-guard-policies.md) ^1.0.1
- [@exellix/fact-guard-validation](https://npm.io/package/@exellix/fact-guard-validation.md) ^1.0.1

## Recent versions

- 4.9.14 (latest) — 2026-07-12
- 4.9.13 — 2026-07-11
- 4.9.12 — 2026-07-10
- 4.9.11 — 2026-07-10
- 4.9.10 — 2026-07-09
- 4.9.9 — 2026-07-08
- 4.9.7 — 2026-07-07
- 4.9.5 — 2026-06-30
- 4.9.4 — 2026-06-29
- 4.9.3 — 2026-06-29
- 4.9.2 — 2026-06-29
- 4.9.1 — 2026-06-27
- 4.9.0 — 2026-06-26
- 4.8.4 — 2026-06-18
- 4.8.3 — 2026-06-18
- … 40 more at https://npm.io/package/@exellix/xynthesis/versions

## README

# `@exellix/xynthesis`

## What it does

- **Recommended for hosts (orchestrators):** **`runXynthesisAiAction`** — one call with **`actionId`**, **`actionPhase`** (`pre` | `post`), **`gateway`**, **`identity`** (**`jobId`** / **`taskId`** plus run context), **`config`**, and **`templateContext`** (data fields only). **Requires `XYNTHESIS_CATALOX_ACTIONS=1`**; templates load **only** from Catalox (no host-supplied system/user prompt text). See [**`docs/catalox-public-ai-action.md`**](docs/catalox-public-ai-action.md). Consumer migration checklist: [**`docs/migration-ai-tasks-xynthesis-ai-action.md`**](docs/migration-ai-tasks-xynthesis-ai-action.md).
- **Sidekick (lower-level):** **`runSidekickGatewayCall`** accepts a fully built **`SidekickInput`** plus **`RunSidekickExecConfig`**. Templates: **Catalox-only** when **`XYNTHESIS_CATALOX_ACTIONS=1`**; otherwise **`templates/{actionType}/`** on disk. Same actions (**synthesis**, **audit**, **fix**, **pick-best**, **craft-final**) and typed JSON parsers. Model tier (`cheapest` / `balanced` / `best`) resolves **per action** via **`ACTION_MODEL_MATRIX`** and **`resolveModelForAction`**.
- **Terminal graph finalization:** `runXynthesisFinalize` exposes a stable utility keyed **`xynthesis/finalize`** for 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 with **`ExecuteXynthesisActionRequest`** (`gateway`, **`prompts`**, **`workScope`** with required **`jobId`** / **`taskId`**). Used by the sidekick gateway, finalize, and callers that already materialize prompts. Prefer **`runXynthesisAiAction`** when Catalox should supply templates.
- Calls the LLM via **`@x12i/funcx`** default **`FuncxInvoker`** (`Client.ask` to OpenRouter; structured paths set **`responseFormat: json_object`** and client-side JSON normalization). Optional **`AIGatewayInvoker`** (`@x12i/ai-gateway`) via **`setSynthesisInvoker`** when 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 wrap **`FuncxInvoker`** / **`AIGatewayInvoker`** (`AIFunctionsInvoker` is 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.

```ts
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](../ai-skills/docs/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/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.

```ts
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`](../fact-guard/README.md).

---

## Install

```bash
npm install @exellix/xynthesis
```

Published publicly on [npm](https://www.npmjs.com/package/@exellix/xynthesis). 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`](.npmrc.example).

### Environment

#### Logging

This package uses [**`@x12i/logxer`**](https://www.npmjs.com/package/@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`):

```ts
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.0`** and Mongo bootstrap. **`@exellix/xynthesis@4.9.13`** is prepared locally but **not published** until **`@x12i/funcx@4.10.0`** ships. See [**`documenations/catalox-6-rollout.md`**](../documenations/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`**](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`**](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)

```ts
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

```ts
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

```ts
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`**).

```ts
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). If `outputContractId` is set, xynthesis validates `parsed` against `templates/finalize/contracts/<outputContractId>.schema.json` using 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.md`
- `prompt.md`
- `assumptions.md` (optional)
- `repair.instructions.md` (optional; only used if `enableRepairOnce` is set)

Optional env override for disk mode: **`XYNTHESIS_FINALIZE_TEMPLATES_PATH`** (root for `templates/finalize/...`).

```ts
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.

```ts
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`**](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`**](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`](docs/activix-integration-best-practices-checklist.md)

> **Note:** Activity tracking is **enabled by default** when `MONGO_LOGS_URI` (or `MONGO_URI`) is set. To skip Activix entirely (tests, no Mongo), set **`XYNTHESIS_ACTIVIX=FALSE`**. To pause Mongo writes in a shared deployment while keeping the client alive, use Activix **`setCollectionTrackingState('xynthesis-actions', 'off')`** instead.

- **Primary key**: `activityId` (prefer over legacy alias `recordId`).
- **Prefix**: `act-` (e.g., `act-eb47a60e…`).
- **Collection**: `XYNTHESIS_ACTIVIX_COLLECTION` overrides; otherwise default **`xynthesis-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.

```ts
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`](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`**.

```ts
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-profiles`** **`profile/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 into `executeXynthesisAction`.
- **`models/model-routing.json` (v3):** **`actionOutputDefaults`** only (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

```ts
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 message
```

Missing 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`](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).

```bash
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:live
```

`npm 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:

```bash
XYNTHESIS_TEST_SYNTHESIS_TIMEOUT_MS=180000 npm run test:client-envelope
```

**Timeouts:** `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`](../archived/xynthesis/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)

```bash
npx x12i-env run --env-file .env -- npx tsx test/live.ts
```

Runs 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`](../archived/xynthesis/docs/legacy/01-current-behavior.md) — reference PRE-step behaviour (orchestrator integration)
- [`02-downstream-request-contract.md`](../archived/xynthesis/docs/legacy/02-downstream-request-contract.md) — original ai-gateway contract
- [`03-how-to-extract-into-package.md`](../archived/xynthesis/docs/legacy/03-how-to-extract-into-package.md) — extraction recipe
- [`04-dependencies-and-boundaries.md`](../archived/xynthesis/docs/legacy/04-dependencies-and-boundaries.md) — boundary decisions

---
_Source: https://npm.io/package/@exellix/xynthesis · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
