# @x12i/funcx

> FuncX: a super-easy typed AI reasoning-functions library for product code

Latest version **4.12.0** (published 2026-08-02) · MIT license · 0 weekly downloads

## Install

```sh
npm install @x12i/funcx
pnpm add @x12i/funcx
yarn add @x12i/funcx
bun add @x12i/funcx
```

Provides the commands `funcx`, `funcx-mcp`.

## 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; large bundle.

## Facts

| | |
|---|---|
| Version | 4.12.0 |
| Published | 2026-08-02 |
| First published | 2026-04-24 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=20 |
| Dependencies | 15 |
| Unpacked size | 35.3 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | x12i |
| Keywords | funcx, catalox, openrouter, llm, llama.cpp, gguf, transformers.js |

## Links

- npm: https://www.npmjs.com/package/@x12i/funcx
- Repository: https://github.com/x12i/funcx
- Homepage: https://github.com/x12i/funcx#readme
- Issues: https://github.com/x12i/funcx/issues
- npm.io page: https://npm.io/package/@x12i/funcx

## Dependencies (15)

- [ajv](https://npm.io/package/ajv.md) ^8.17.0
- [zod](https://npm.io/package/zod.md) ^3.25.76
- [jsonata](https://npm.io/package/jsonata.md) ^2.0.5
- [@x12i/env](https://npm.io/package/@x12i/env.md) ^4.0.1
- [simple-git](https://npm.io/package/simple-git.md) ^3.36.0
- [@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
- [json-logic-js](https://npm.io/package/json-logic-js.md) ^2.0.5
- [@x12i/ai-profiles](https://npm.io/package/@x12i/ai-profiles.md) ^4.4.0
- [secure-json-parse](https://npm.io/package/secure-json-parse.md) ^4.1.0
- [@x12i/core-service](https://npm.io/package/@x12i/core-service.md) ^1.0.0
- [@x12i/ai-router-runtime](https://npm.io/package/@x12i/ai-router-runtime.md) ^1.2.0
- [@modelcontextprotocol/sdk](https://npm.io/package/@modelcontextprotocol/sdk.md) ^1.29.0

## Alternatives

- [mobx-react](https://npm.io/package/mobx-react.md) — 2.8M weekly downloads
- [rc-tree](https://npm.io/package/rc-tree.md) — 2.6M weekly downloads
- [@react-oauth/google](https://npm.io/package/@react-oauth/google.md) — 1.3M weekly downloads
- [@wagmi/connectors](https://npm.io/package/@wagmi/connectors.md) — 877.0K weekly downloads
- [vee-validate](https://npm.io/package/vee-validate.md) — 836.4K weekly downloads

## Recent versions

- 4.12.0 (latest) — 2026-08-02
- 4.11.0 — 2026-07-23
- 4.10.4 — 2026-07-12
- 4.10.3 — 2026-07-11
- 4.10.2 — 2026-07-11
- 4.10.1 — 2026-07-11
- 4.10.0 — 2026-07-11
- 4.9.24 — 2026-07-10
- 4.9.23 — 2026-07-07
- 4.9.22 — 2026-06-30
- 4.9.21 — 2026-06-29
- 4.9.20 — 2026-06-29
- 4.9.19 — 2026-06-29
- 4.9.18 — 2026-06-29
- 4.9.17 — 2026-06-29
- … 58 more at https://npm.io/package/@x12i/funcx/versions

## README

# FuncX (`@x12i/funcx`)

**Turn prompts into real functions** — typed I/O, JSON-safe execution, evaluation, and instruction optimization.

- Use it as an **npm library** (you're in full control; no proxy required).
- Or run the included **stateless REST server** to expose functions over HTTP and manage the full function lifecycle.

### Terminology

- **Function**: canonical public term for an ability in this library.
- **Catalox**: runtime content store — catalog id **`ai-functions`** (app **`funcx`**). Each function is **one native row** keyed by `indexed.functionId`, with structured fields in `data` (`title`, `description`, `instructionsText`, `rules`, `io`, `testCases`, `meta`). **CRUD goes through FuncX, not Catalox directly** — see [.docs/AI-FUNCTIONS-CATALOG-CATALOX.md](.docs/AI-FUNCTIONS-CATALOG-CATALOX.md). Model/provider metadata comes from **`@x12i/ai-profiles`**, not Catalox (see [.docs/CATALOX-MODEL-CATALOG-VERIFICATION.md](.docs/CATALOX-MODEL-CATALOG-VERIFICATION.md)).

### FuncX v4 breaking changes

| v3 | v4 |
|----|-----|
| `executeSkill` | `executeFuncx` |
| `runWithContent` | `executeFuncxFromContent` |
| `getSkillNames()` | `getFuncxNames()` |
| `getSkillsResolver()` | `getFuncxResolver()` |
| `runFunctionWithCatalox` | `runFuncxWithCatalox` |
| Catalox `data.instructions` duplicate | **`data.body` only** |
| Git `content:migrate:catalox` | **`content:primitives:sync`** from `content-seed/functions/*.seed.json` |
| `/skills/*` HTTP | **410** — use `/functions/*` and `POST /run` |

**Workflow:** edit seeds → `npm run content:emit-code-bound-seed` (when code-bound ids change) → `npm run content:primitives:sync` → `npm run content:emit-library` → commit seeds + `functions/generated/`.

**Execution models** (see `functions/funcxRegistry.ts` and each function’s `meta.execution`):

| `meta.execution` | Runtime | Catalox |
|------------------|---------|---------|
| **`code`** | TypeScript handler in the router | Readonly meta + IO schemas (`requiresInternalCode`, `implementation`, `internalCodeReason`) |
| **`content`** | Catalox instructions + prompt via `executeFuncxFromContent` | Editable prompts, rules, tests |

Verify bundled metadata: `npm run content:verify-execution-metadata`. Catalog readiness audit (all 151 ids): `npm run audit:catalog` or `npm run test:readiness`. Code-bound seed emit: `npm run content:emit-code-bound-seed`.

**Sidekick `run()` path (v4.9.10)** — ten gateway-aligned ids use **content** execution with `envelopeKind` and `envelopeToTemplateVariables`. Import helpers from `@x12i/funcx/functions`; golden envelopes and prompt hashes ship in `@x12i/funcx/fixtures`.

| Built-in | Default output |
|----------|----------------|
| `execution-plan`, `research-plan-questions`, `execution-evaluate-result`, `pre-synthesize`, `post-audit`, `post-fix`, `post-pick-best`, `post-craft-final` | JSON object |
| `post-audit-checklist` | **Markdown string** (`args.outputMode: "json"` for structured `{ checks, overallFeedback }`) |
| `post-audit-merge` | **Plain text string** (merged result only) |

```ts
import { run, getFuncxOutputSchema } from "@x12i/funcx/functions";
import { loadSidekickEnvelope, loadSidekickFixtureManifest } from "@x12i/funcx/fixtures";

const checklist = await run("post-audit-checklist", loadSidekickEnvelope("post-audit-checklist"));
// → markdown string with ### Checks / ### Overall feedback

const schemas = getFuncxOutputSchema("post-audit-checklist"); // string (markdown default)
const jsonSchema = getFuncxOutputSchema("post-audit-checklist", { outputMode: "json" });
```

Regenerate golden prompt hashes after seed edits: `npm run content:emit:sidekick-fixtures`.

**Typed imports:** `import { semanticCompare } from "@x12i/funcx/functions"` (codegen). **Dynamic:** `run("semanticCompare", input, { resolver })` unchanged.

### FuncX v5 — normalized `ai-functions` catalog

| v4 (legacy blobs) | v5 (current) |
|-------------------|----------------|
| ~6 Catalox rows per function (`fx/<id>/instructions`, `rules`, `meta.json`, …) | **One catalog item per function** |
| Opaque `data.body` string blobs | Structured `data.instructionsText`, `data.rules`, `data.io`, … |
| Identity via `data.funcxRef` / `indexed.skillId` | Identity via **`indexed.functionId`** |
| Library index in `fx/index.v1.json` + `fx/index/v1/*.json` | Index **derived from catalog list**; enrichment writes normalized items |

**Upgrade an existing Catalox env** (once, before deploying v5 code):

```bash
npm run content:migrate:ai-functions:dry    # review report
npm run content:migrate:ai-functions:apply  # collapse rows + delete legacy blobs
npm run content:primitives:sync             # optional: refresh from seeds
```

Fresh installs: skip migration — `content:primitives:sync` writes the normalized shape directly.

Full CRUD, data shape, and operator workflows: [.docs/AI-FUNCTIONS-CATALOG-CATALOX.md](.docs/AI-FUNCTIONS-CATALOG-CATALOX.md).

Metadata tier API: `aiFunctionsCatalogService` (TS) and `GET|POST|PUT|PATCH|DELETE /functions/catalog/*` (HTTP).

### Local-first `.funcx` dual catalog

Alongside Catalox, FuncX loads functions from a local folder tree (default **`.funcx/`**, override with `FUNCX_LOCAL_ROOT`):

```text
.funcx/
  .functions.json
  <id>/
    .function.json
    instructions.md
    knowledge/
    input.schema.json
    output.schema.json
    prompt.md          # optional
    rules.json         # optional
    test-cases.json    # optional
```

**Get-by-id:** if the id exists locally, return it and **do not** call Catalox for that id.  
**List/query:** union of local + Catalox; **local wins** on id collision; items include `source: "local" | "catalox"`.

---

## Why this exists

LLM "functions" start easy and get messy:
- output drifts
- JSON breaks
- edge cases show up
- you add retries, parsing, validation, model picking… and end up with glue code

**FuncX** (`@x12i/funcx` on npm) standardizes that work:
- **Typed contracts** (input/output schemas)
- **JSON-only execution** with retries + repair
- **Evaluation** (judge/rules)
- **Optimization loop** (run → judge → fix → repeat)
- **Skill packs** stored as files (weak/strong/ultra), so prompts aren't buried in code
- **Functions lifecycle** (create → validate → release) with score-gated promotion

---

## Install

**Node 20+** required (see `engines` in package.json).

```bash
npm i @x12i/funcx
```

### Optional local CPU backends

Local inference is provided by **`@x12i/ai-router-runtime`**. Install its optional deps in your app when you use those providers:

```bash
npm i node-llama-cpp          # GGUF via llama.cpp (provider: llama-cpp)
npm i @huggingface/transformers  # Transformers.js (provider: transformersjs)
```

---

## FuncX MCP

Expose the FuncX catalog as an MCP server so assistants can call your functions as tools, read catalog resources, and use bundled prompts.

- **Library:** `import { createFuncXMcpServer, ... } from "@x12i/funcx/mcp"`
- **CLI:** `funcx-mcp` (installed with the package: `npx funcx-mcp`)

Run **`npm run build`** before local tests; unit tests load the compiled `dist/` entry points like the rest of this repo.

### Transports

**stdio (default)** — wire Cursor, Claude Desktop, or other MCP clients to a subprocess:

```json
{
  "mcpServers": {
    "funcx": {
      "command": "npx",
      "args": ["funcx-mcp", "--transport", "stdio"]
    }
  }
}
```

**Streamable HTTP** — same process flags or env; defaults bind `127.0.0.1:4901` at `/mcp`:

```bash
npx funcx-mcp --transport http --host 127.0.0.1 --port 4901 --endpoint /mcp
```

Point the client at `http://127.0.0.1:4901/mcp`. When **`FUNCX_MCP_REQUIRE_AUTH`** is true or you bind a non-loopback host without opting out, set **`FUNCX_MCP_API_KEY`** and send `Authorization: Bearer <key>` or `x-api-key: <key>`.

Browser-like requests send an `Origin` header; only localhost-style origins and same-host origins are allowed unless you extend that logic in your integration.

### Exposure filters

CLI flags (`--allowed-prefix`, `--blocked-prefix`, `--allowed-function-id`, `--blocked-function-id`) and env vars (`FUNCX_MCP_ALLOWED_PREFIXES`, etc., comma-separated) match the `exposure` options on `CreateFuncXMcpServerOptions`. Draft content skills are hidden when **`FUNCX_MCP_ACTIVE_ONLY`** is true (default) unless **`FUNCX_MCP_INCLUDE_DRAFT`** or `--include-draft` is set.

### Programmatic use

```ts
import { createFuncXMcpServer } from "@x12i/funcx/mcp";

const server = await createFuncXMcpServer({
  transport: "stdio",
  exposure: { exposeCatalogResources: true, exposePrompts: true },
});
await server.start();
```

Use **`createFuncXMcpStdioServer`** / **`createFuncXMcpHttpServer`** when you need a specific transport. **`refreshCatalog()`** reloads the catalog and updates registrations for stdio / new HTTP sessions.

See **`.env.example`** for `FUNCX_MCP_*` names.

---

## Quick start

### 1) Use prebuilt functions

```ts
import { classify, summarize, match, matchLists } from "@x12i/funcx/functions";

const { categories } = await classify({
  text: "I was charged twice this month.",
  categories: ["Billing", "Auth", "Support"],
});

const { summary } = await summarize({ text: longDoc, length: "brief" });

// One query → best matching candidates from a labelled list
const result = await match({
  query: "CVSS base score",
  candidates: [
    { id: "cvss_base",    label: "CVSS base score | numeric severity 0–10" },
    { id: "cvss_vector",  label: "CVSS vector string | attack surface encoding" },
    { id: "patch_status", label: "Patch status | applied / missing / n/a" },
  ],
  maxResults: 1,
});
// result.noMatch → false; result.matches[0].id → "cvss_base"

const r = await matchLists({
  list1: sourceFields,
  list2: targetFields,
  guidance: "Match by semantic meaning.",
});
```

### `match()` — one query, many candidates

Finds the best-matching candidates from an arbitrary list for a single query string. Returns stable IDs, confidence scores, and optional reasons. Never throws on model parse failures — returns `{ noMatch: true }` instead.

```ts
import { match } from "@x12i/funcx/functions";

const result = await match({
  query: "affected product / version",
  candidates: fieldList,   // MatchCandidate[]  { id, label }
  maxResults: 3,           // default 3
  minScore: 0.4,           // optional threshold
  allowNoMatch: true,      // default true  → noMatch result instead of throw
  returnReasons: true,     // default true  → reason string per match
  maxCandidates: 80,       // default 100   → cap sent to model; excess pre-ranked by trigram overlap
  model: "deepseek/deepseek-v4-flash",
});

if (result.noMatch) {
  console.log("no match found");
} else {
  console.log(result.matches[0].id, result.matches[0].score, result.matches[0].reason);
}
```

**`MatchInput` fields:**

| Field | Type | Default | Description |
|---|---|---|---|
| `query` | `string` | required | Free-text search query |
| `candidates` | `MatchCandidate[]` | required | `{ id: string\|number; label: string }` |
| `maxResults` | `number` | `3` | Maximum matches returned |
| `minScore` | `number` | — | Drop matches below this confidence (0–1) |
| `allowNoMatch` | `boolean` | `true` | Return `{ noMatch: true }` instead of throwing when no match / model error |
| `returnReasons` | `boolean` | `true` | Include a short reason per match |
| `maxCandidates` | `number` | `100` | Hard cap on candidates sent to the model; excess are pre-ranked by character-trigram overlap |
| `guidance` | `string` | — | Extra matching guidance appended to the prompt |
| `additionalInstructions` | `string` | — | Appended verbatim to all instruction tiers |
| `model`, `mode`, `temperature`, `maxTokens` (`number` or `"auto"`), `timeoutMs`, `vendor`, `client` | — | — | Standard LLM options |

**`MatchOutput`:**

```ts
{
  query: string;
  noMatch: boolean;
  matches: Array<{ id: string | number; score: number; reason?: string }>;
}
```

**Large candidate sets:** pass `maxCandidates` to guard against context-window overflow. The library pre-selects the most relevant candidates using trigram overlap before the model call, so quality stays high even when the full list is large. Default cap is 100; tune down to ~40–60 for lightweight profile choices (`cheap/default`) or local llama.

### 2) Run any function by name

```ts
import { run } from "@x12i/funcx/functions";

const result = await run("extract-invoice-lines", { text: invoiceText });
// → { lines: [{ description: "...", amount: 4500 }, ...] }
```

**Success shape:** In-process `run()` returns the skill value directly (`Promise<unknown>`). JSON skills normally return a plain object; `validateOutput: true` yields `{ result, validation }`. The optional HTTP server wraps payloads as `{ result, usage, requestId, ... }` — use **`getRunJsonResult`** from `@x12i/funcx/functions` so adapters see the same inner JSON whether you call `run()` locally or parse HTTP bodies.

**Input validation (v4.9.14+):** Content-backed functions validate the request against catalog `io.inputSchema` **before** any LLM call. Invalid input throws `FuncxInputValidationError` (`code: INPUT_VALIDATION`; HTTP `400` on `POST /run`). Run-control fields (`client`, `model` when not in schema, etc.) are stripped before validation.

**Generic execution envelope:** Orchestrators can standardize on capability ids such as **`execution/plan`**, **`execution/evaluate-result`**, and **`research/plan-questions`** (slash forms normalize to hyphen keys). Import canonical string constants **`FUNCX_EXECUTION_PLAN_FUNCTION_ID`**, **`FUNCX_EXECUTION_EVALUATE_RESULT_FUNCTION_ID`**, and **`FUNCX_RESEARCH_PLAN_QUESTIONS_FUNCTION_ID`** from `@x12i/funcx/functions` to avoid drift with other packages. Payloads share **`GenericExecutionEnvelope`** (`goal`, optional `input`, `context`, `result`, `args`, `attribution`); TypeScript types and a draft **`genericExecutionEnvelopeJsonSchema`** export from `@x12i/funcx/functions`. Forward nested telemetry with **`buildAskAttribution`** on custom builtins (same merge rules as the generic built-ins).

Migration table (legacy ai-tasks envelopes vs generic ids), Catalox seed checklist, and upgrade notes: [docs/migration-generic-execution.md](docs/migration-generic-execution.md).

**Full list (manual mode):** All library functions are available for programmatic use with rules and optimization. See [Library functions — manual mode](.docs/LIBRARY-FUNCTIONS-MANUAL-MODE.md) for the complete list and how to run them by name with a content provider or resolver.

---

## Runx authoring sets (built-in FuncX)

Six specialized **authoring** families are registered as built-ins (callable via `run()` / the REST API). Each uses **strict JSON I/O schemas**, `askJson`-style generation, and optional **Catalox seed** content under `content-seed/functions/`. They model **FuncX → structured draft → Runx validates and stores** in Catalox; execution of produced scripts or UI is a separate concern.

| Set | ID prefix | Deliverables |
| --- | --- | --- |
| **Generic JavaScript** | `runx.*` | Small reusable JS capabilities (normalize, plan reuse, generate code/tests, review, repair, build draft). Optional `authoringHints` / `reuseHints` for evolution-driven parameterization. |
| **API adapter** | `runx.api.*` | HTTP adapters using `context.http`, contracts, tests, Catalox-ready **api-adapter** drafts |
| **UI authoring** | `runx.ui.*` | OpenUI Lang, React, HTML, UI tests, review/repair, Catalox-ready **ui** drafts |
| **Script / ops** | `runx.script.*` | PowerShell, bash/sh, Windows CMD, network CLI, tests, review/repair, Catalox-ready **script** drafts |
| **Condition** | `runx.condition.*` | Executable guard predicates (JS / JSONLogic / JSONata / filter-predicate) with smoke tests and auto-repair |
| **Evolution** | `runx.evolution.*` | Health assessment, repair, gap classification (`parameterize-candidate`), composition discovery, test-case generation, parameterization hints for normalize/plan |

Example:

```ts
import { run } from "@x12i/funcx/functions";

const draft = await run("runx.script.normalizeScriptRequest", {
  requestText: "Read-only bash snippet to print df -h.",
  preferredTarget: "bash",
});
```

Runners and schema exports live on **`@x12i/funcx/functions`** (for example `runxScriptNormalizeScriptRequest`, `runxUiPlanUiDelivery`, `runxApiPlanApiAdapter`).

**Refresh seed JSON after editing TypeScript IO schemas** (`functions/catalog/runx/**/ioSchemas.ts`):

```bash
npm run build
npx tsx scripts/emitRunxSeedSchemas.ts && npx tsx scripts/emitRunxSeedBundle.ts
npx tsx scripts/emitRunxApiSeedSchemas.ts && npx tsx scripts/emitRunxApiSeedBundle.ts
npx tsx scripts/emitRunxUiSeedSchemas.ts && npx tsx scripts/emitRunxUiSeedBundle.ts
npx tsx scripts/emitRunxScriptSeedSchemas.ts && npx tsx scripts/emitRunxScriptSeedBundle.ts
npx tsx scripts/emitRunxConditionSeedSchemas.ts && npx tsx scripts/emitRunxConditionSeedBundle.ts
npx tsx scripts/emitRunxEvolutionSeedSchemas.ts && npx tsx scripts/emitRunxEvolutionSeedBundle.ts
```

Then push merged seeds to Catalox (requires your Mongo / FuncX Catalox setup):

```bash
npm run content:primitives:sync
```

---

## Core concepts

### Function packs (file-based prompts)

Functions live as files in a content store (git repo or local folder). Canonical paths use the **`functions/`** prefix:

```
functions/<id>/weak       # local/cheap instructions
functions/<id>/strong     # cloud/high-quality instructions (mode "normal" uses this)
functions/<id>/ultra      # optional highest-tier instructions
functions/<id>/rules      # optional judge rules (JSON)
functions/<id>/meta.json  # status: draft | released, version, scoreGate
functions/<id>/test-cases.json   # stored test cases for validate/optimize
functions/<id>/race-config.json  # race defaults + winner profiles (best/cheapest/fastest/balanced)
functions/<id>/races.json        # race history (append-only, capped)
```

Content can be loaded from a **git-backed resolver** (e.g. `.content` repo) or via a **content provider** (shared-store or inline). The library exports `createFunctionContentProvider` and `ResolverBackedContentProvider` for use with `run(skillName, request, { contentProvider, scopeId?, model?, validateOutput? })`. Use `options.profile` on HTTP `/run` only for **race winner keys** (`best` | `cheapest` | `fastest` | `balanced`), not ai-profiles tiers.

**Docs:** [COLLECTIONS_MAPPING.md](docs/COLLECTIONS_MAPPING.md) and [DATA_MAPPING.md](docs/DATA_MAPPING.md) (when present in `docs/`) describe collections, schema, and data; see also [.docs/](.docs/) for in-repo design docs.

Prompts are:
- reviewable in PRs
- shareable across projects
- versionable like code

### Modes

| Mode                | Typical backend | Use                      |
| ------------------- | --------------- | ------------------------ |
| `weak`              | local (CPU)     | dev/offline/cheap        |
| `normal` / `strong` | cloud           | production default       |
| `ultra`             | cloud           | strictest / highest-tier |

---

## The safety layer (JSON + validation + retries)

For any JSON-producing call, the library applies:

- **extract-first JSON** (prefers ` ```json ` fences, then first balanced object/array)
- **safe parsing** (guards against prototype poisoning)
- **optional schema validation** (Ajv)
- **deterministic retries** (normal → JSON-only guard → fix-to-schema with errors)

```ts
import { runJsonCompletion } from "@x12i/funcx/functions";

const { parsed, text, usage } = await runJsonCompletion({
  instruction: "Extract line items from this invoice. Return JSON only.",
  options: { model: "deepseek/deepseek-v4-flash", maxTokens: 800 },
});
```

### Example generation (schemas, Markdown, plain content)

Use these built-ins to produce **one** representative sample from a contract plus guidance—useful for smoke inputs, docs, and UI previews. They use the same JSON pipeline and LLM options (`model`, `mode`, `temperature`, `maxTokens`, `timeoutMs`, `vendor`, `client`) as other functions.

**Fake data / secrets:** models are instructed to use obviously fictional placeholders (e.g. `example.com`, `proj_test_01`) and **not** to emit realistic API keys, tokens, passwords, or private identifiers. Always review generated samples before shipping or persisting them.

```ts
import {
  generateJsonExample,
  generateMdExample,
  generateContentExample,
  run,
} from "@x12i/funcx/functions";

// JSON example that should match your JSON Schema (validated when the schema compiles in Ajv)
const jsonOut = await generateJsonExample({
  jsonSchema: {
    type: "object",
    additionalProperties: false,
    properties: { userId: { type: "string" }, count: { type: "integer", minimum: 0 } },
    required: ["userId"],
  },
  guidance: [
    "Use realistic but fake values.",
    "Prefer a minimal happy-path payload.",
  ],
  examples: [{ userId: "usr_sample_1", count: 0 }],
  model: "deepseek/deepseek-v4-flash",
});

// Markdown example
const mdOut = await generateMdExample({
  guidance: ["Write a concise operator-facing runbook example."],
  examples: ["# Existing example\n\n..."],
});

// Plain text / CSV / email body / etc.
const textOut = await generateContentExample({
  contentType: "text/plain",
  guidance: ["Generate a realistic email body for this workflow."],
});

// Same functions via run() by name
const same = await run("generate-json-example", {
  jsonSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] },
  guidance: ["Single smoke-test object."],
  client: myOpenRouterClient,
});
```

---

## Evaluation & optimization

### Judge a response

```ts
import { judge } from "@x12i/funcx/functions";

const verdict = await judge({
  instructions: "...",
  response: "...",
  rules: [
    { rule: "Must output valid JSON only", weight: 3 },
    { rule: "Field names must match the schema exactly", weight: 2 },
  ],
  threshold: 0.8,
  mode: "strong",
});
// verdict.pass, verdict.scoreNormalized, verdict.ruleResults
```

### Generate rules from instructions

```ts
import { generateJudgeRules } from "@x12i/funcx/functions";

const { rules } = await generateJudgeRules({ instructions: myPrompt });
```

**Methodology:** When providing good/bad examples (e.g. `POST /optimize/rules` or `optimizeJudgeRules`), include a brief rationale (why it's good or bad) when possible; it improves rule quality.

### Generate / improve instructions until they pass

```ts
import { generateInstructions } from "@x12i/funcx/functions";

const best = await generateInstructions({
  seedInstructions: myPrompt,
  testCases: [{ id: "t1", inputMd: "Invoice #1234\nConsulting: $4,500" }],
  call: "ask",
  targetModel: { model: "deepseek/deepseek-v4-flash", class: "normal" },
  judgeThreshold: 0.8,
  targetAverageThreshold: 0.85,
  loop: { maxCycles: 5 },
  optimizer: { mode: "strong" },
});
// best.achieved, best.best.instructions, best.best.avgScoreNormalized
```

### Fix instructions from judge feedback

```ts
import { fixInstructions } from "@x12i/funcx/functions";

const { fixedInstructions, changes } = await fixInstructions({
  instructions: myPrompt,
  judgeFeedback: verdict,
});
```

### Compare two instruction versions

```ts
import { compare } from "@x12i/funcx/functions";

const result = await compare({
  instructions: baseInstructions,
  responses: [
    { id: "v1", text: responseFromVersionA },
    { id: "v2", text: responseFromVersionB },
  ],
  threshold: 0.8,
});
// result.bestId, result.ranking
```

### Benchmark models

```ts
import { raceModels } from "@x12i/funcx/functions";

const ranking = await raceModels({
  taskName: "invoice-lines",
  call: "askJson",
  testCases: [{ id: "t1", inputMd: "..." }],
  threshold: 0.8,
  models: [
    { id: "deepseek", model: "deepseek/deepseek-v4-flash", vendor: "deepseek", class: "strong" },
    { id: "claude", model: "anthropic/claude-3-5-haiku", vendor: "anthropic", class: "strong" },
  ],
});
```

---

## Client (one API across providers)

```ts
import { createClient } from "@x12i/funcx";

const ai = createClient({ backend: "openrouter" });

const res = await ai.ask("Write a product tagline.", {
  model: "deepseek/deepseek-v4-flash",
  maxTokens: 200,
  temperature: 0.7,
});
// res.text, res.usage, res.model, res.routing?, res.cost?, res.timing?
```

FuncX does **not** ship a built-in OpenRouter default. Pass **`model`** on each call as either a **`profile/choice`** key (`cheap/default`) or a catalog slug (`deepseek/deepseek-v4-flash`) — FuncX resolves both via `@x12i/ai-profiles` (see [Profiles + choice](#profiles--choice-x12iai-profiles)). `configureDefaultLlmModel({ model })` is for unit/live test bootstrap only.

### Trace / diagnostics metadata (authoritative when available)

`client.ask()` returns a stable, additive metadata envelope for downstream trace-mode consumers:

- **`res.usage`**: provider token usage when available, plus funcx echoes **`usage.maxTokensRequested`** (the cap you passed in `AskOptions.maxTokens`). For convenience, `usage.tokensPrompt|tokensCompletion|tokensTotal` mirror `prompt_tokens|completion_tokens|total_tokens`.
- **`res.routing`**: stable provider label + request-id bag for correlation (keys are additive; existing keys won’t be renamed).
- **`res.cost`**: `costUsd`, `costStatus` (`priced` | `unpriced`), and optional `costBreakdown` from `resolveInvokeBilling`.
- **`res.timing`**: ISO timestamps and durations when tracked by the backend.

Notes:

- These fields are **optional** and are populated only when the backend can extract them from response bodies/headers. (OpenRouter provides exact cost via `usage.cost` and may provide request IDs via headers.)
- The `raw` response remains **opt-in**; result payloads stay small by default.

### Backends

All LLM execution goes through **`@x12i/ai-router-runtime@^1.2.0`**. FuncX `createClient({ backend })` selects the router provider (`openrouter` | `llama-cpp` | `transformersjs`).

```ts
createClient({ backend: "openrouter", openrouter?: { apiKey?, baseUrl?, appName?, appUrl?, allowFallbacksDefault? } })
createClient({ backend: "llama-cpp", llamaCpp: { modelPath, contextSize?, threads? } })
createClient({ backend: "transformersjs", transformersjs: { modelId?, cacheDir?, device?: "cpu" } })
```

**FuncX function execution** (`run()`, `callAI`, content-backed `/run`) accepts **`model`** as a **`profile/choice`** key (`cheap/default`, `balanced/default`, …) or a catalog slug (`deepseek/deepseek-v4-flash`). Resolution and catalog validation use **`@x12i/ai-profiles`** at invoke time. Local CPU runs require explicit paths: `createClient({ backend: "llama-cpp", llamaCpp: { modelPath } })` or env `LLAMA_CPP_MODEL_PATH`; `transformersjs` likewise with `modelId` / `TRANSFORMERS_JS_MODEL_ID` (no package defaults). Install optional local deps on the app (see [Install](#install)).

Race **evaluation** keys (`best`, `cheapest`, `fastest`, `balanced`) remain FuncX scope labels for stored winners; winners persist **`model` only**.

See [.docs/funcx-llm-call-contracts.md](.docs/funcx-llm-call-contracts.md) for the full contract.

---

## Configuration

`.env` (all optional unless you use that backend):

```env
OPENROUTER_API_KEY=sk-or-...
PREFER_OPENROUTER=true

# Local CPU — set explicitly per deployment (optional):
# LLAMA_CPP_MODEL_PATH=/path/to/model.gguf
# LLAMA_CPP_THREADS=6
# TRANSFORMERS_JS_MODEL_ID=your/huggingface-model-id

# Optional: persist REST server activities to Mongo (npm run serve only)
# FUNCX_ACTIVIX_ENABLED=1
# MONGO_ACTIVIX_URI=mongodb://...   # or MONGO_LOGS_URI / MONGO_URI
```

See [Optional MongoDB activity persistence (Activix)](#optional-mongodb-activity-persistence-activix) for collection names and database selection.

### Related packages (v4.9+)

| Package | Version | Role |
|---|---|---|
| `@x12i/ai-router-runtime` | **^1.2.0** (direct) | **Sole LLM engine** — OpenRouter + local `llama-cpp` / `transformersjs` providers |
| `@x12i/ai-profiles` | **^4.3.0** (direct) | **Single source of truth** — profile registry, catalog, slug resolution, invoke routing, invoke cost |
| `@x12i/catalox` | **^6.0.0** | Mongo-backed function content (R2/S3 optional for record history) |

FuncX resolves **`profile/choice`** keys and catalog slugs through ai-profiles on every `run()` / `callAI` / `client.ask()` / REST `/run`.

#### Profiles + choice (`@x12i/ai-profiles`)

An **AI profile** is capability intent (`cheap`, `balanced`, `deep`, `json`, …). A **choice** is a concrete implementation within that profile (`default`, `google_floor`, `deepseek_cheap`, …). Stored config should use **`profile/choice`** keys — not bare model slugs.

| Layer | Example | Where |
|-------|---------|--------|
| Profile + choice | `balanced/default`, `cheap/deepseek_cheap` | Graph `modelConfig`, job config — **pass directly to FuncX** |
| OpenRouter slug | `deepseek/deepseek-v4-flash`, `openai/gpt-4o-mini` | FuncX `run()`, `client.ask()`, REST `options.model` |

**Recommended** — pass `profile/choice` and let FuncX resolve at invoke time:

```ts
import { run } from "@x12i/funcx/functions";

await run("classify", { text, categories }, { model: "cheap/default" });
await ai.ask("Summarize this.", { model: "balanced/default", maxTokens: 500 });
```

Or pass a catalog slug when you already know it:

```ts
await run("summarize", { text }, { model: "deepseek/deepseek-v4-flash" });
```

**Host-side resolution** (gateways, pre-flight validation) — optional; same package:

```ts
import { resolveAndNormalizeInvokeModel } from "@x12i/ai-profiles";

const { model } = await resolveAndNormalizeInvokeModel("cheap/deepseek_cheap", {
  catalogLane: "text",
});
// model → e.g. "deepseek/deepseek-v4-flash"
```

**`PREFER_OPENROUTER`** (default `true`, from `@x12i/ai-profiles`) controls OpenRouter vs vendor-direct routing. FuncX applies it when resolving `model`.

REST **`options.profile`** on `/run` is **not** ai-profiles — it selects a **race evaluation winner** (`best` | `cheapest` | `fastest` | `balanced`) for scope-specific model picks.

#### OpenRouter integration (v4.9)

| Area | Status | Notes |
|------|--------|-------|
| ai-profiles as single SOT | **Done** | `@x12i/ai-tools` removed; catalog + cost via `@x12i/ai-profiles` **^4.3.0** |
| `profile/choice` on `model` | **Done** | `cheap/default` resolves inside FuncX (`resolveProfileKeys`) |
| Explicit `model` per call | **By design** | No tier env defaults; pass `profile/choice` or slug |
| Invoke cost | **Done** | `calculateInvokeCostFromRecord` from ai-profiles |
| `PREFER_OPENROUTER` precedence | **Done** | `PREFER_OPENROUTER` wins over legacy `USE_OPENROUTER` when both set |
| BYOK | **Supported** | `x-openrouter-key` header on REST; `OPENROUTER_API_KEY` env fallback |
| REST `options.profile` | **Race keys only** | `best` \| `cheapest` \| `fastest` \| `balanced` — not ai-profiles tiers |
| `RunOptions.temperature` / `topP` | **Done (4.9.9)** | Forwarded on `run()` content path |
| Transport `error.cause` | **Done (4.9.9)** | OpenRouter fetch failures attach underlying cause on `NxAiApiError` |
| Sidekick fixtures export | **Done (4.9.10)** | `@x12i/funcx/fixtures` — golden envelopes + prompt hashes |
| `post-audit-checklist` markdown default | **Done (4.9.10)** | JSON only when `args.outputMode = "json"` |
| Embedder transport retry policy | **Done (4.9.12)** | See **Retry policy for embedders** below |
| `RunOptions.transportRetries` | **Done (4.9.13)** | Built-in exponential backoff on retriable transport failures |
| OpenRouter server tools | **Done (4.9.21)** | All OpenRouter calls route through `@x12i/ai-router-runtime`; `options.serverTools`, `options.openrouter`, REST `runtime`, catalog `meta.runtime.serverTools` |
| Runtime `tools` / `funcxTools` | **Done (4.9.22)** | `ask()` / `run()` / `callAI` forward OpenRouter function tools and FuncX functions-as-tools via the router |
| Streaming removal | **Done (4.9.24)** | Non-streaming only; REST stream requests → `STREAMING_UNSUPPORTED` |
| Sole LLM package | **Done (4.9.24)** | `@x12i/ai-router-runtime@^1.2.0` only (no direct `@x12i/openrouter-runtime` / legacy HTTP client) |
| Sidekick output schema CI | **Done (4.9.12)** | `getFuncxOutputSchema` validated against golden outputs for all 10 ids |

#### Retry policy for embedders (CR-FUNCX-5)

When wrapping `run()` in your pipeline:

| Error class | Examples | Retry? |
|-------------|----------|--------|
| **Transport** | `NxAiApiError` with `code: "OPENROUTER_HTTP_ERROR"` and `cause` (`ECONNRESET`, `ETIMEDOUT`, `fetch failed`) | **Yes** — idempotent `run()` hops only, with backoff |
| **HTTP rate limit** | `OPENROUTER_HTTP_ERROR` with `status: 429` | **Yes** — respect `Retry-After` when present |
| **Schema / validation** | `ERR_SCHEMA_INVALID`, malformed model JSON | **No** — fix envelope, prompt, or schema |
| **Unknown function** | Catalog / resolver misconfiguration | **No** |

Inspect `err.cause` on transport failures to distinguish flaky network from model output issues. Do not blindly retry non-idempotent side effects outside FuncX `run()`.

Pass `transportRetries: 2` on `run()` to retry transient OpenRouter failures automatically (up to 3 total attempts with backoff). Default `0` leaves retry policy to the embedder.

```ts
await run("pre-synthesize", envelope, { client, transportRetries: 2 });
```

**Consumer prompt parity:** render your disk templates with `@x12i/funcx/fixtures` golden envelopes, normalize via `normalizePromptForParity` + `hashSidekickPrompt`, and compare to `loadSidekickFixtureManifest().sidekicks[].promptHash`. FuncX CI enforces the same hashes on seed changes.

#### OpenRouter server tools

FuncX routes **all** OpenRouter execution through **`@x12i/ai-router-runtime`** (`createAiRouterRuntime` → `router.run()`). Server tools are **opt-in** per call, per REST request (`runtime.serverTools`), or per function catalog metadata (`meta.runtime.serverTools`).

**Function tools / FuncX tools**

```ts
await ai.ask("Use tools if needed.", {
  model: "balanced/default",
  tools: [/* OpenAI-compatible function tools with executor */],
  funcxTools: [{ functionId: "summarize", description: "Summarize text" }],
});

await run("research", input, {
  model: "balanced/default",
  tools: [/* … */],
  funcxTools: [{ functionId: "summarize" }],
});
```

**Web search**

```ts
await run("summarize", input, {
  model: "balanced/default",
  responseFormat: "markdown",
  serverTools: {
    webSearch: {
      mode: "required",
      allowedDomains: ["learn.microsoft.com"],
      maxResults: 5,
      maxTotalResults: 15,
    },
  },
});
```

**Search + fetch**

```ts
await ai.ask("Summarize the latest OpenRouter server tools docs.", {
  model: "balanced/default",
  serverTools: {
    webSearch: { mode: "required" },
    webFetch: { mode: "allowed", maxContentTokens: 50000 },
  },
});
```

**Datetime**

```ts
await ai.ask("What is due tomorrow?", {
  model: "cheap/default",
  serverTools: {
    datetime: { mode: "allowed", timezone: "Asia/Jerusalem" },
  },
});
```

**Apply patch** (Responses API; returns proposals only — FuncX never mutates files silently)

```ts
await run("runx.script.repair", input, {
  openrouter: { apiMode: "responses" },
  serverTools: {
    applyPatch: { mode: "required", behavior: "return_only" },
  },
});
```

Supported tool families are `webSearch`, `webFetch`, `datetime`, `imageGeneration`, `applyPatch`, `advisor`, `subagent`, and `fusion`. Each uses `mode: "disabled" | "allowed" | "required"`. Invocation options override catalog runtime defaults; an invocation can explicitly disable a catalog-enabled tool with `{ mode: "disabled" }`.

FuncX owns catalog lookup, input validation, prompt rendering, JSON parsing, output validation, lifecycle/timeout handling, and the SDK/REST shape. OpenRouter and local LLM calls use **`@x12i/ai-router-runtime`** for request compilation, provider routing, OpenRouter server-tool payload construction, provider policy, citations, generated images, patch proposals, and normalized runtime responses. FuncX does not implement web search, fetch, advisor/subagent/fusion, or patch application itself.

Tool usage, citations, generated images, and patch proposals appear additively under `result.metadata` / `result.openrouter` for `client.ask()` and under `usage` for `run()` responses with usage enabled. JSON functions remain JSON-safe: citations and tool telemetry stay in metadata/usage/trace, not inside parsed results unless the output schema explicitly requests them.

`advisor.model`, `subagent.model`, `fusion.analysisModels`, and `fusion.judgeModel` accept concrete model strings. Graphenix-style `modelSelection` fields are rejected with `FUNCX_SERVER_TOOL_MODEL_SELECTION_UNSUPPORTED`; FuncX does not resolve graph execution model-selection objects.

`applyPatch` is proposal-only in FuncX. `behavior: "return_only"` is accepted; auto-apply or callback application modes are rejected with `FUNCX_APPLY_PATCH_AUTO_APPLY_UNSUPPORTED`.

OpenRouter server tools use the existing `OPENROUTER_API_KEY` or per-request BYOK header. There is no separate server-tool key.

Legacy `:online` model suffixes (e.g. `openai/gpt-5.2:online`) normalize to the base slug with implied `webSearch.allowed` and emit `OPENROUTER_ONLINE_VARIANT_DEPRECATED`.

---

## REST API (optional, stateless)

Expose functions and the full lifecycle over HTTP. Request/response shapes are described in this README and in the API contract when present (e.g. [docs/API_CONTRACT.md](docs/API_CONTRACT.md)); server–contract sync: [docs/CONTRACT_SYNC.md](docs/CONTRACT_SYNC.md). If `docs/` is not in your clone, the REST API section below is the endpoint reference.

The HTTP process uses **`@x12i/core-service`** (ports-manager zone **`funcx`**, default API port **6120**): `/health`, `/_live`, `x-correlation-id`, startup banner.

```bash
npm install @x12i/funcx
npx funcx serve
# zone default port 6120; override with FUNCX_PORT or PORT
# from a clone: npm run build && npm run serve
```

### AI-services contract (`/ai-services/v1/*`)

Shared wire contract for typed AI catalogs + invoke. FuncX owns Catalox catalog **`ai-functions`** (app **`funcx`**). Legacy `/functions/*` and `POST /run` remain available.

```
GET  /ai-services/v1/descriptor     → { protocolVersion: "ai-services/1", aiServiceType: "function", packageId, zoneId, title, cataloxCatalogIds }
GET  /ai-services/v1/items          → { items: [{ id, title, summary?, tags?, version?, meta? }], total, limit, offset, nextCursor? }
GET  /ai-services/v1/items/:id      → item detail (+ inputSchema / outputSchema / defaults when present)
POST /ai-services/v1/invoke         → { itemId, input?, execution?, correlationId? } → { ok, itemId, output? | error?, meta? }
```

Prefer request header `x-correlation-id` (echoed on responses); body `correlationId` is a fallback for invoke attribution.

### Authentication

| Header | Purpose |
|---|---|
| `x-api-key` | Authenticates to the server — validated against `FUNCX_API_KEY`, `LIGHT_SKILLS_API_KEY`, or legacy `AIFUNCTIONS_API_KEY` |
| `x-openrouter-key` | BYOK — passed through to OpenRouter so each user can use their own key and billing |

If none of those API key env vars is set, all requests are allowed.

### Run and health

```
GET  /health                  core-service health → { ok, service: "@x12i/funcx", version, uptime, skills, … }
GET  /_live                   live request UI (api-live-view)
GET  /config/modes            server mode→model mapping → { weak, normal, strong, ultra } (each { model, description })
POST /run                     { skill, input, options } → { result, usage, valueEstimate? }
POST /skills/:name/run        { input, options }        → { result, usage }
POST /functions/:id/run       { input, options }        → { result, usage, requestId, draft?, trace?, valueEstimate? }
GET  /skills                  list functions + metadata (legacy alias route)
GET  /skills/:name            function detail (legacy alias route)
GET  /functions               list functions
GET  /functions/:id           function detail with status, version, last validation, currentInstructions, currentRules, currentRulesCount
```

Run request body may include `options.scopeId` and `options.profile` (`best` | `cheapest` | `fastest` | `balanced`) for scope-specific model selection; `options.validateOutput: true` returns `{ result, validation }` against the library index schema; `options.estimateValue: true` with optional `options.humanCostPerHour` / `options.humanCostProfile` adds `valueEstimate` (see [Cost vs value](#cost-vs-value-humanvalue-and-estimatevalue)). OpenRouter runtime options are accepted under `options.serverTools`, `options.openrouter`, and `options.serverToolInstructionMode`; manager/playground APIs use the same fields in their `config` object. Run responses include `requestId` (same as attribution `traceId` when provided). When the function is in draft status, the response includes `draft: true`. Pass `options.trace: true` in the run body to receive a `trace` object with the full prompt(s), model selection, and model used per call (for that request only; not stored). Run endpoints are rate-limited per key (see `RATE_LIMIT_PER_MINUTE`) and return `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers.

```json
{
  "skill": "source-backed-answer",
  "input": { "question": "What changed?" },
  "options": {
    "model": "balanced/default",
    "serverTools": {
      "webSearch": { "mode": "required", "requireCitations": true },
      "webFetch": { "mode": "allowed", "maxContentTokens": 50000 }
    },
    "openrouter": { "apiMode": "auto" },
    "serverToolInstructionMode": "minimal"
  }
}
```

**Human value** (dedicated routes; also callable via `POST /run` with `skill: "humanValue.compute"` etc.):

```
POST /human-value/derive-strategy
POST /human-value/compute
POST /human-value/resolve-for-function
POST /functions/:id/human-value/derive-strategy    # body { apply: true } persists meta.humanValueStrategy
GET  /functions/:id/human-value/strategy
PUT  /functions/:id/human-value/strategy
POST /content/human-value/emit-strategies
GET  /analytics/value?from=&to=&functionId=&projectId=&groupBy=functionId
```

Run `mode` may be `weak`, `normal`, `strong`, `ultra`, or profile modes `best`, `cheapest`, `fastest`, `balanced`. Profile modes require a race to have been run first; otherwise the server returns `422` `NO_RACE_PROFILE`.

### Response formats

Use **`responseFormat`** to choose how the model should answer:

| Format | Use when |
|--------|----------|
| `json` (default) | Structured tools, extraction, automation — output is parsed and schema-validated. |
| `markdown` | Reports, explanations, chat-style prose — returns a markdown string (no JSON wrapper). |
| `text` | Plain-text summaries or labels — returns a raw string. |

**npm library** (`@x12i/funcx/functions`):

```ts
import { run } from "@x12i/funcx/functions";

const report = await run("my.report", { topic: "Q1" }, { responseFormat: "markdown" });

// Optional sampling overrides (v4.9.9+) — apply to content-backed and router LLM paths
await run("execution-plan", envelope, {
  model: "cheap/default",
  temperature: 0.2,
  topP: 0.95,
});
```

FuncX is **non-streaming only**. Every `run()` / `client.ask()` call completes as a single response. REST requests that send `options.stream: true` or `Accept: text/event-stream` receive **400** with code `STREAMING_UNSUPPORTED`. Pass `signal: AbortSignal` to cancel in-flight non-streaming calls.

**Catalog migrations** (one-time, existing Catalox envs):

```bash
# v5: multi-row fx/* blobs → one normalized item per function (required before v5 deploy)
npm run content:migrate:ai-functions:dry
npm run content:migrate:ai-functions:apply

# Optional legacy: add responseContracts + runtime to function meta (pre-v5 meta.json migration)
npm run content:migrate:response-contracts:dry
npm run content:migrate:response-contracts:apply
```

### Agent platform — Memorix, Narrix, Reportix, Content Recipes, FuncX Catalog

| Namespace | Role |
|-----------|------|
| **`memorix`** | Knowledge discovery/extraction, Q&A, prompt matching, entities |
| **`narrix`** | Narrative composition (story + narrix → content) |
| **`reportix`** | Data-backed report pages and data-to-story output |
| **`contentRecipes`** | Content-structure composition (recipe → content) |
| **`funcxCatalog`** | Registry introspection (list/find functions; no LLM on listers) |

**Memorix** (`memorix.knowledge` + `memorix.access`):

- `memorix.exploreKnowledgeTypes` / `memorix.extractKnowledge` — discover types then extract (`single` \| `multiple` \| `all` \| `auto`)
- `memorix.answerQuestionsFromContent`, `memorix.classifyRelevantQuestions`, `memorix.classifyRelevantNarratives`, `memorix.extractPromptEntities`

**Narrix** (composition only — not Q&A or reports): `narrix.composeContent`, `narrix.planComposition`, `narrix.validateComposition`, `narrix.rewriteToNarrix`

**Reportix**: `reportix.classifyDataNarratives` → `reportix.tellDataStoryInNarrative` → `reportix.composeQuestionAnswerReportPage`

**Content Recipes**: `contentRecipes.selectRecipe`, `planContent`, `composeContent`, `validateContent`, `rewriteWithRecipe`, `generateRecipeListItem`, `validateRecipe`, `composeFromNarrix`

**FuncX Catalog** (deterministic listers + AI find):

- `funcxCatalog.listCategories`, `listAllFunctions`, `listFunctionsByCategory` — `mode: brief|detailed`, `format: json|markdown`
- `funcxCatalog.findFunction` — `need` → matching `functionId`
- `funcxCatalog.getFunction` — `functionId` with `detail: brief|full` (default brief)

Seeds: `content-seed/functions/{memorix,narrix,reportix,contentRecipes,funcxCatalog}.*.json`. Emit:

```bash
npm run content:agent-platform:emit-seeds
npm run content:primitives:sync
npm run content:recipes:catalogs:sync   # optional: recipe native catalogs
```

`pagenti.*` has been removed; use `memorix.*` instead.

### Functions lifecycle

Create a function, iterate on it, validate quality, then release it to a stable versioned endpoint.

```
POST /functions               create: { id, seedInstructions, scoreGate?, rules? }
POST /functions/:id:validate  run schema + semantic scoring → { passed, scoreNormalized, cases }
POST /functions/:id:release   promote to released (blocked if score < scoreGate). Body may include `scopeId` for scope-specific release.
POST /functions/:id:rollback  set current instructions/rules to a previous version (body: `{ version: gitRef }`; requires version APIs)
POST /functions/:id:optimize  rewrite instructions in-place
POST /functions/:id:push      push to remote git repo (requires SKILLS_LOCAL_PATH)
GET  /functions/:id/versions  instruction version history (git shas)
POST /functions/:id/versions/:version/run  run at a pinned version (ref = git sha from versions list)
GET  /functions/:id/scopes    query: scopeId required → { functionId, scopeId, releases }
POST /functions/:id/apply    apply evaluation result to scope (body: { scopeId?, evaluationSessionId, appliedBy? }) → { appliedProfileSet, scopeId, functionId }
GET  /functions/:id/test-cases
PUT  /functions/:id/test-cases  { testCases: [{ id, input, expectedOutput? }] }
POST /functions/:id/save-optimization  persist instructions, rules, examples from optimization wizard
POST /functions/generate-examples      { description, count?, mode? } → { examples, usage? }
```

### Race / benchmark

```
POST /race/models              race models, temperatures, or tokens (async job) — body: skillName|prompt, testCases?, candidates|models, functionKey?, applyDefaults?, raceLabel?, notes?, type? (model|temperature|tokens), model?, temperatures?, tokenValues?
GET  /functions/:id/profiles   race winner profiles and defaults → { defaults, profiles: { best, cheapest, fastest, balanced } }
GET  /functions/:id/race-report  race history — query: last, since, raceId → { races }
```

Job result for a race includes `ranked`, `raw`, `winners`, `usage`. Run with `mode: best|cheapest|fastest|balanced` uses the stored profile for that function.

The `cheapest` winner is selected by pricing rate using the bundled pricing table (see below).

### Optimization endpoints

```
POST /optimize/generate       generate instructions from test cases (async job)
POST /optimize/judge          score a response against rules → { pass, score, ruleResults }
POST /optimize/rules          generate rules from labeled examples or instructions
POST /optimize/rules-optimize optimize existing rules from examples with rationale (append/replace)
POST /optimize/fix            fix instructions from judge feedback → { fixedInstructions, changes, summary, usage, optional addedRuleBullets }
POST /optimize/compare        rank 2+ responses by quality → { ranking, bestId, candidates }
POST /optimize/instructions   one-shot instruction rewrite
POST /optimize/skill          rewrite one skill's instructions in-place
POST /optimize/batch          batch rewrite (async job)
```

### Jobs (for async operations)

```
GET /jobs               list recent jobs
GET /jobs/:id           status, progress, result
GET /jobs/:id/logs      streaming log lines
```

### Content workflows

```
POST /content/sync         sync instructions to content store (and optional push)
POST /content/index        enrich discovery metadata on normalized catalog items → { indexed, skills, errors }
POST /content/index/full   build and return full embedded library snapshot (body: prefix?, staticOnly?, writeDocsFallback?) → { fullSnapshot, ... }; writes .docs/library-index.full.fallback.json by default
POST /content/fixtures      validate examples vs io.output schemas
POST /content/layout-lint  enforce folder-based layout under functions/
```

### Cost estimation (`resolveInvokeBilling`)

All LLM paths resolve **token usage** and **USD cost** through one orchestrator before responses are returned or persisted to Activix. Library calls expose `CallAIResult.cost` / `usage.costUsd`; HTTP and `run()` responses include a structured `usage` object (unless opted out).

Resolution order (first match wins; provider/router cost is never overwritten by catalog):

| Step | Condition | Result |
|------|-----------|--------|
| A | Provider/router finite `costUsd` | `costStatus: "priced"` |
| A′ | Explicit `costStatus: "priced"` | priced (cost optional) |
| A″ | Explicit `costStatus: "unpriced"` | unpriced — catalog not called |
| B | Tokens + catalog authoritative (`calculateInvokeCostFromRecord`) | priced + optional breakdown |
| C | Tokens, still no price | `costStatus: "unpriced"` |
| D | Zero tokens | omit cost fields |

HTTP `costEstimate` maps `costStatus`: **priced** → `status: "available"`; **unpriced** → `status: "unavailable"` with `reasonCode: "BACKEND_NO_PRICING"`. Sources are **`provider-response`** or **`catalog`** only.

**Activix (7.2+):** with **`autoCost`** enabled, `outer.cost` is filled on `completeRecord` / `failRecord` from usage in `outer.output`. FuncX invoke billing uses **`@x12i/ai-profiles`** `calculateInvokeCostFromRecord`; Activix may still use its own catalog path transitively.

**Config** (funcx-specific env; legacy `FUNCX_AI_TOOLS_*` / `AI_TOOLS_*` names still honored):

| Flag | Default | Env |
|------|---------|-----|
| `calculateCost` | `true` | `FUNCX_AI_PROFILES_CALCULATE_COST=0` (or `FUNCX_AI_TOOLS_CALCULATE_COST=0`) |
| `bundledOnly` | `false` | `AI_PROFILES_BUNDLED_ONLY=1` (or `AI_TOOLS_BUNDLED_ONLY=1`) |
| `costIncludeBreakdown` | `true` | `configureAiProfilesEngine({ costIncludeBreakdown: false })` |

Model slugs and `profile/choice` keys are resolved via `resolveModelSlug` / `requireCatalogModel` (ai-profiles). Reasoning models use `getModelReasoningProfile` for token-budget headroom.

```ts
import {
  enrichAskResultCost,
  resolveInvokeBilling,
  configureAiProfilesEngine,
  resolveModelSlug,
  getModelReasoningProfile,
  configureActivix,
} from "@x12i/funcx";
```

#### `includeUsage` (default: `true`)

By default, **`run()`** wraps LLM function output as `{ result, usage }` so callers get tokens and cost without extra wiring. Pass **`includeUsage: false`** for a clean function payload only (cost is still computed internally and sent to Activix when enabled).

```ts
import { run } from "@x12i/funcx/functions";

// Default — result + usage envelope
const out = await run("memorix.answerQuestionsFromContent", input);
// out.result — function output
// out.usage — { promptTokens, completionTokens, totalTokens, estimatedCost, costEstimate, ... }

// Clean — function output only
const clean = await run("memorix.answerQuestionsFromContent", input, { includeUsage: false });
```

HTTP run endpoints accept the same flag at **`options.includeUsage`** or top-level **`includeUsage`**:

```json
POST /functions/memorix.answerQuestionsFromContent/run
{
  "input": { "...": "..." },
  "options": { "includeUsage": false }
}
```

When `includeUsage` is omitted or `true`, the response includes `usage` alongside `result`. When `false`, the response is `{ "result": …, "requestId": … }` only.

### Token budget (`tokenBudget.*` and `maxTokens: "auto"`)

FuncX can derive and apply per-function **max-tokens strategies** stored in Catalox meta as `maxTokensStrategy` (versioned JSON: output archetype, padding, reasoning multiplier, ceilings).

| Function | Role |
|----------|------|
| `tokenBudget.deriveStrategy` | LLM (rare): design strategy from instructions / functionId |
| `tokenBudget.adaptForModel` | Pure: adjust strategy to model `maxCompletionTokens` / reasoning |
| `tokenBudget.computeMax` | Pure: strategy + input → `maxTokens` + breakdown |
| `tokenBudget.resolveForFunction` | Orchestrator: meta strategy or archetype → final `maxTokens` |
| `tokenBudget.auto` | Alias for `resolveForFunction` |

**Standalone** (no FuncX function id):

```ts
import { run } from "@x12i/funcx/functions";

await run("tokenBudget.resolveForFunction", {
  instructions: "Summarize the document in 3 bullets.",
  input: { text: "..." },
  model: "deepseek/deepseek-v4-flash",
});
```

**Auto max tokens** on any LLM call:

```ts
await client.ask(prompt, { maxTokens: "auto", model: "deepseek/deepseek-v4-flash", system: instructions });
```

Batch-write strategies to Catalox meta: `npm run content:token-budget:emit-strategies` (optional `--dry-run`, `--function-id <id>`).

Combined token + human value batch: `npm run content:catalog:emit-runtime-strategies`.

### Cost vs value (`humanValue.*` and `estimateValue`)

FuncX separates **AI execution cost** from **estimated human effort equivalent**:

| Concept | Meaning |
|---------|---------|
| **Cost** | How much did the AI execution cost? (USD from usage / pricing) |
| **Value** | How much human effort did this function likely save? (time → estimated human-cost equivalent) |
| **Net value** | Estimated human-cost equivalent minus AI cost |
| **ROI multiple** | Human-cost equivalent divided by AI cost (only when AI cost is known) |

This is an **operational estimate**, not accounting truth. Prefer wording like *estimated human-cost equivalent*; avoid *actual savings*, *guaranteed ROI*, or *revenue generated*.

Strategies live in Catalox meta as `humanValueStrategy` (`human-value-strategy.v1`): a safe **formula DSL** (no arbitrary JavaScript). The LLM runs once via `humanValue.deriveStrategy`; runtime uses `humanValue.compute` (pure, no model).

| Function | Role |
|----------|------|
| `humanValue.deriveStrategy` | LLM (rare): design reusable strategy from function catalog context |
| `humanValue.compute` | Pure: strategy + input/output/usage → minutes, cost equivalent, net value, ROI |
| `humanValue.resolveForFunction` | Load meta strategy (or archetype fallback) and compute |

**Run with value estimate** (HTTP or library):

```ts
import { run } from "@x12i/funcx/functions";

const out = await run("memorix.answerQuestionsFromContent", input, {
  estimateValue: true,
  humanCostPerHour: 80,
});
// out.result — function output
// out.valueEstimate — { humanMinutes, humanCostEquivalent, netValueUsd, roiMultiple, confidence }
```

```http
POST /run
{ "skill": "classify", "input": { ... }, "options": { "estimateValue": true, "humanCostPerHour": 80 } }
```

**Resolve value for a function** (no LLM):

```ts
import { resolveHumanValueForFunction } from "@x12i/funcx/functions";

const value = await resolveHumanValueForFunction({
  functionId: "memorix.answerQuestionsFromContent",
  input,
  output,
  usage: { costUsd: 0.04, promptTokens: 1200, completionTokens: 300 },
  humanCostPerHour: 90,
});
```

**Human cost profiles** (override with `humanCostPerHour` or `humanCostProfile: "analyst"`):

| Profile id | Typical rate |
|------------|----------------|
| `general-knowledge-worker` | $50/hr |
| `analyst` | $80/hr |
| `senior-engineer` | $120/hr |
| `legal-reviewer` | $150/hr |

**REST** (also via generic `POST /run`):

- `POST /human-value/derive-strategy`, `POST /human-value/compute`, `POST /human-value/resolve-for-function`
- `POST /functions/:id/human-value/derive-strategy` (body `{ "apply": true }` persists to meta)
- `GET|PUT /functions/:id/human-value/strategy`
- `GET /analytics/value?from=&to=&functionId=&projectId=&groupBy=functionId`
- `POST /content/human-value/emit-strategies`

Batch: `npm run content:human-value:emit-strategies`, validate: `npm run content:human-value:validate-strategies`.

### ai-skills: gateway invoke analysis (`ai-skills/analyze-gateway-invoke-request`)

**Pre-invoke packet review** before calling ai-gateway or running the target FuncX function. Full guide: [.docs/GATEWAY-INVOKE-ANALYSIS.md](.docs/GATEWAY-INVOKE-ANALYSIS.md).

| FuncX key | Alias (slash form) |
|-----------|-------------------|
| `ai-skills-analyze-gateway-invoke-request` | `ai-skills/analyze-gateway-invoke-request` |

**What runs:** SDK static checks (optional) + FuncX packet consistency (automatic) + LLM simulation review. **What does not run:** the gateway, and the target `functionId` function.

| Layer | Field / mechanism |
|-------|-------------------|
| SDK deterministic | `deterministicFindings` (your static pre-pass) |
| Packet consistency | Auto: `functionId` vs `actionRef` vs `runSkillRequest` |
| LLM simulation | This FuncX function → `verdict`, `findings`, `markdownReport` |

**Input:** real sanitized invoke packet (not mock data). Canonical target id is **`functionId`** (FuncX id). Legacy `skillKey` is accepted with a deprecation warning.

```bash
npm run content:ai-skills:emit-seeds    # → aiSkills.seed.json
npm run content:primitives:sync
```

```ts
import { run } from "@x12i/funcx/functions";

await run("ai-skills/analyze-gateway-invoke-request", {
  functionId: "summarize",
  gatewayPayload: {
    actionType: "skill",
    actionRef: "summarize",
    workingMemory: { /* ... */ },
    smartInput: { /* ... */ },
    modelConfig: { model: "deepseek/deepseek-v4-flash" },
  },
  runSkillRequest: { functionId: "summarize", input: { text: "..." } },
  deterministicFindings: [], // SDK static pre-pass (optional)
});
```

### Catalog judge rules

Catalog **`data.rules`** may be **empty**; runtime omits the rules section when there are no non-empty entries. Rules are appended to system instructions (`callAI`, `askJson`, `buildFunctionPrompt`) and passed through hand-written catalog runners.

---

### Function-level cost & activity tracking

Every LLM call is automatically tagged with the **function that originated it**, so usage data can be attributed precisely — not just at the model or account level.

**How it works:**

1. The server injects `functionId` automatically (e.g. `extract.requirements`, `optimize.judge`).
2. Callers may optionally pass `projectId`, `traceId`, and `tags` in any POST body.
3. The server embeds these as metadata in the outgoing provider request (`user` field for OpenRouter).
4. Every response returns extended attribution fields in the `usage` object.

**Optional request fields (any POST endpoint that calls an LLM):**

| Field | Type | Description |
|---|---|---|
| `projectId` | `string` | Logical project or tenant (e.g. `"cognni-prod"`) |
| `traceId` | `string` | Correlation ID for distributed tracing. Auto-generated UUID if omitted. |
| `tags` | `object` | Free-form key-value metadata (string values) |
| `options.includeUsage` | `boolean` | When `false`, omit `usage` from the response (default `true`) |

**Example request:**

```json
POST /functions/extract.requirements/run
{
  "input": { "text": "..." },
  "projectId": "cognni-prod",
  "traceId": "req-983741",
  "tags": { "workflow": "classification", "environment": "production" }
}
```

**Extended `usage` response:**

```json
{
  "promptTokens": 240,
  "completionTokens": 82,
  "totalTokens": 322,
  "model": "deepseek/deepseek-v4-flash",
  "latencyMs": 1430,
  "estimatedCost": 0.000147,
  "costEstimate": {
    "amountUsd": 0.000147,
    "status": "available",
    "confidence": "high",
    "source": "provider-response"
  },
  "functionId": "extract.requirements",
  "projectId": "cognni-prod",
  "traceId": "req-983741",
  "tags": { "workflow": "classification", "environment": "production" }
}
```

`functionId` is always present. `projectId`, `traceId`, and `tags` appear only when provided.

The library remains **stateless** when used as an npm dependency. The REST server is **in-memory by default** for `GET /activity` (ring buffer only). Optional Mongo persistence is described below.

#### Runtime logging (@x12i/logxer)

FuncX uses [@x12i/logxer](https://www.npmjs.com/package/@x12i/logxer) for structured runtime logs (diagnostic codes, evidence, correlation ids). Logging and the in-memory debug capture (`runTime`, AI activity ring buffer) are **off in `mode=prod`** unless you enable them in code or env.

**Log level (single resolution — code wins over env):**

| Source | Effect |
|--------|--------|
| `configureFuncxLogging({ logLevel: "off" })` | Silence all FUNCX log output |
| `configureFuncxLogging({ logLevel: "debug" })` | Emit `debug` and above (overrides env) |
| `configureFuncxLogging({ logLevel: "warn" })` | Emit `warn` and `error` only |
| `FUNCX_LOGS_LEVEL` | Canonical env threshold when code does not set `logLevel` |
| `FUNCX_LOG_LEVEL` | Legacy env alias (used only when `FUNCX_LOGS_LEVEL` is unset) |
| Default (no code, no env) | `warn` |

Allowed values: `off` (or `none` / `silent`), `error`, `warn`, `info`, `debug`, `verbose`.

**Runtime capture enablement:**

| Source | Effect |
|--------|--------|
| `configureFuncxLogging({ enabled: false })` | Disable runtime logging and in-memory capture (overrides `mode=debug`) |
| `configureFuncxLogging({ enabled: true })` | Enable runtime logging even when `mode=prod` |
| `mode=debug` (env) | Enable when code does not set `enabled` |
| `mode=prod` (default) | Disabled when code does not set `enabled` |

```ts
import { configureFuncxLogging, getFuncxLogger, getRunTime } from "@x12i/funcx";

// Embed FuncX in a host app — silence FuncX logs regardless of env
configureFuncxLogging({ logLevel: "off" });

// Or keep capture but only show warnings/errors
configureFuncxLogging({ logLevel: "warn" });

// Enable structured debug logs in production for a support session
configureFuncxLogging({ enabled: true, logLevel: "debug", logFormat: "json" });

// Direct logger / runtime access (undefined when runtime logging is disabled)
const logger = getFuncxLogger();
logger?.infoCode("FUNCX_SERVER_STARTED", { port: 6120 });

// After configureFuncxLogging, use getRunTime() for the current capture state
configureFuncxLogging({ enabled: false });
// getRunTime() === undefined
```

Call `configureFuncxLogging` **before** importing modules that log (or call it anytime to hot-reload the logger; existing `aiActivities` are preserved). Diagnostic catalog codes live in `metadata/log-diagnostics.json` (shipped with the package).

**Format:** `configureFuncxLogging({ logFormat: "json" })` or env `FUNCX_LOG_FORMAT=json` (default `text`).

**Deep stacks (`@x12i/logxer` ≥ 4.5.0):** When FuncX is embedded alongside other logxer packages, the host can control all package thresholds in one place:

```ts
import {
  applyPackageLogLevelsFromEnv,
  configurePackageLogLevels,
  type StackLoggingOptions,
} from "@x12i/logxer";
import { configureFuncxLogging, createFuncxLogger, FUNCX_LOG_ENV_PREFIX } from "@x12i/funcx";

// At startup (before heavy imports)
applyPackageLogLevelsFromEnv();
configurePackageLogLevels({
  default: "warn",
  levels: { [FUNCX_LOG_ENV_PREFIX]: "info" },
});

// Or pass-through when constructing FuncX surfaces
const logging: StackLoggingOptions = { packageLevels: { FUNCX: "debug" } };
configureFuncxLogging({ logging });

// Standalone logger factory (tests / custom wiring)
const log = createFuncxLogger({ logging });
```

Precedence for FuncX: `configureFuncxLogging({ logLevel })` → `logging` / `stack` → `FUNCX_LOGS_LEVEL` env → `FUNCX_LOG_LEVEL` legacy → host `configurePackageLogLevels` / `LOGXER_PACKAGE_LEVELS` → default **`warn`**.

Bulk env (host should call `applyPackageLogLevelsFromEnv()` at startup):

```dotenv
LOGXER_PACKAGE_LEVELS=FUNCX:debug,OTHER_PKG:off
LOGXER_PACKAGE_LOGS_DEFAULT=warn
```

#### Optional MongoDB activity persistence (Activix)

When Activix is **enabled**, the server initializes [@x12i/activix](https://www.npmjs.com/package/@x12i/activix) **7.2+** in **`storageMode: database`** with **`autoCost`** (default on) so `outer.cost` is derived from usage in `outer.output`. FuncX billing enrichment uses **`@x12i/ai-profiles`**. Two collections are written (Mongo database from env — see `ACTIVIX_DB_NAME`, `MONGO_AI_LOGS_DB`, `MONGO_LOGS_DB`, `MONGO_DB`; default `activitix`):

**Enablement (single resolution — no env/code collision):**

| Source | Effect |
|--------|--------|
| `configureActivix({ enabled: false })` | Always **off** (overrides `FUNCX_ACTIVIX_ENABLED=1`) |
| `configureActivix({ enabled: true })` | **On** (requires Mongo URI from code or env; **`autoCost`** on by default) |
| `configureActivix({ enabled: true, autoCost: false })` | **On** without automatic `outer.cost` |
| `configureActivix({ autoCost: { bundledOnly: true } })` | **On** with offline catalog-only pricing |
| `FUNCX_ACTIVIX_ENABLED=1` | **On** when code config does not set `enabled` |
| Default | **Off** |

```ts
import { configureActivix } from "@x12i/funcx";

// Embed FuncX — disable Mongo persistence regardless of env
configureActivix({ enabled: false });

// Enable with custom URI and auto-cost (default when enabled)
configureActivix({
  enabled: true,
  mongoUri: process.env.MONGO_ACTIVIX_URI,
  autoCost: { bundledOnly: process.env.AI_TOOLS_BUNDLED_ONLY === "1" },
});
```

| Collection | What is logged |
|------------|----------------|
| **`funcx-requests`** | One row per high-level function execution (`GET /activity` mirror). `runContext.sessionId` = request **`traceId`** when present. Top-level **`metadata`**: `functionId`, `projectId`, `traceId`. **`outer.output.usage`** carries tokens/cost for Activix **`autoCost`**. |
| **`ai-actions`** | One row per low-level OpenRouter **`ask` / `chat`**. **`outer.output.response`** is the enriched AskResult; Activix **`autoCost`** reads usage/cost from it. |

Document layout follows Activix v6+: root **`outer`** / optional **`inner`**, no nested `structure`. **`outer.cost`** is set by Activix on terminal writes — not duplicated in **`outer.metadata`** or top-level **`metadata`**.

**Mongo connection string** — first non-empty value wins:

1. `configureActivix({ mongoUri })` code override
2. `MONGO_ACTIVIX_URI`
3. `MONGO_LOGS_URI`
4. `MONGO_URI`

If Activix is enabled but **no** URI is set, or Mongo is unreachable at startup, the process **exits** with an error (fail-fast). Runtime write failures are logged and do **not** fail API requests (fire-and-forget).

For Activix diagnostics only (not required for normal operation), see the `@x12i/activix` README (`ENABLE_ACTIVIX_LOGXER`, `ACTIVIX_LOGS_LEVEL`, etc.).

---

### Analytics APIs

Proxy endpoints that fetch usage and cost data directly from the upstream provider. No data is stored by this server.

```
GET /models/available                 list models available via OpenRouter (x-openrouter-key or OPENROUTER_API_KEY)
GET /activity                         server-side activity log (query: from, to, functionId, projectId, model, limit) → { activities, summary }; entries may include valueEstimate when runs used estimateValue
GET /analytics/value                  aggregate human-value metrics from activity log (query: from, to, functionId, projectId, groupBy)
GET /analytics/openrouter/credits     account balance and total usage
GET /analytics/openrouter/generations generation records (query: dateMin, dateMax, model, userTag, limit)
GET /analytics/openai/usage            org usage buckets — requires OPENAI_ADMIN_KEY (query: startTime, endTime, groupBy, projectIds, models, limit)
GET /analytics/openai/costs            org cost buckets  — requires OPENAI_ADMIN_KEY (query: startTime, endTime, groupBy, projectIds, limit)
```

**OpenRouter:** uses `x-openrouter-key` header (BYOK) or `OPENROUTER_API_KEY` env.

**OpenAI:** requires `OPENAI_ADMIN_KEY` env var (an admin-scoped key from your OpenAI org settings). Standard project keys do not have access to organization-level analytics.

**Filter generations by function or project:**

```
GET /analytics/openrouter/generations?userTag=cognni-prod:extract.requirements&dateMin=2026-03-01
```

The `userTag` filter matches the attribution tag this package injects into the OpenRouter `user` field, so you can pull all generations for a specific function or `project:function` pair.

---

### Server env vars

| Var | Default | Description |
|---|---|---|
| `FUNCX_PORT` / `PORT` | `6120` | HTTP listen port (ports-manager zone `funcx` / `api`; env overrides zone default) |
| `FUNCX_MCP_PORT` | `4901` | MCP Streamable HTTP port |
| `FUNCX_API_KEY` | — | If set, requires `x-api-key` (preferred over legacy key vars below) |
| `FUNCX_LOCAL_ROOT` | `<cwd>/.funcx` | Local-first dual catalog root |
| `LIGHT_SKILLS_API_KEY` | — | If set, requires `x-api-key` header (legacy: `AIFUNCTIONS_API_KEY`) |
| `OPENROUTER_API_KEY` | — | Default OpenRouter key (overridden per-request by `x-openrouter-key`) |
| `PREFER_OPENROUTER` | `true` | Prefer OpenRouter when `OPENROUTER_API_KEY` is set; missing key → vendor-direct fallback with warning. Legacy migration: `USE_OPENROUTER` (FuncX-only when `PREFER_OPENROUTER` unset). |
| `MAX_CONCURRENCY` | `10` | Max parallel LLM calls |
| `RATE_LIMIT_PER_MINUTE` | `60` | Max run requests per minute per key (BYOK or server); responses include `X-RateLimit-Remaining`, `X-RateLimit-Reset` |
| `JOB_TTL` | `3600` | Seconds before completed jobs are cleaned up |
| `mode` | `debug` | `debug` enables in-process runtime logging/capture; `prod` disables unless `configureFuncxLogging({ enabled: true })` |
| `FUNCX_LOGS_LEVEL` | `warn` | Package log threshold: `off`, `error`, `warn`, `info`, `debug`, `verbose` (overridden by `configureFuncxLogging({ logLevel })`) |
| `FUNCX_LOG_LEVEL` | — | Legacy alias for `FUNCX_LOGS_LEVEL` when the canonical var is unset |
| `FUNCX_LOG_FORMAT` | `text` | Console log format: `text` or `json` (overridden by `configureFuncxLogging({ logFormat })`) |
| `LOGXER_PACKAGE_LEVELS` | — | Bulk per-prefix levels, e.g. `FUNCX:debug,OTHER:off` (`@x12i/logxer` ≥ 4.5.0; host should call `applyPackageLogLevelsFromEnv()`) |
| `LOGXER_PACKAGE_LOGS_DEFAULT` | — | Default level when a prefix is not listed in `LOGXER_PACKAGE_LEVELS` |
| `VALIDATE_SKILL_OUTPUT` | `0` | If `1`, all runs include schema validation |
| `SKILLS_LOCAL_PATH` | — | Local git path, required for `:push` endpoint |
| `OPENAI_ADMIN_KEY` | — | Admin-scoped OpenAI key, required for `GET /analytics/openai/*` |
| `FUNCX_ACTIVIX_ENABLED` | `0` | Set to `1` to persist activities to Mongo via Activix when `configureActivix({ enabled: false })` was not called; requires a URI below; startup fails if Mongo is unavailable |
| `MONGO_ACTIVIX_URI` | — | Preferred connection string for Activix when enabled |
| `MONGO_LOGS_URI` | — | Fallback URI for Activix (after `MONGO_ACTIVIX_URI`) |
| `MONGO_URI` | — | Final fallback URI for Activix |
| `ACTIVIX_DB_NAME` | — | Optional; with `MONGO_*_DB` vars, selects the Mongo database name for Activix (see `@x12i/activix` docs) |

Run endpoints enforce a 100KB max request body (413 when exceeded). Backend default timeout per LLM call is 60s; for a 120s run timeout (e.g. aifunction.dev free tier) configure your backend or client.

---

## Content (functions repo) workflow

```bash
npm run content:sync              # sync instructions to .content and push
npm run content:sync:optimize     # optimize instructions (requires OPENROUTER_API_KEY)
npm run content:index             # build library index with LLM (schemas/examples; needs OPENROUTER_API_KEY)
npm run content:index:static      # build library index from content only (no LLM)
npm run content:index:copy-fallback    # copy built index to .docs/library-index.fallback.json
npm run content:index:copy-full-fallback # build full snapshot and write .docs/library-index.full.fallback.json
npm run content:index:real        # content:index + copy-fallback
npm run content:index:real:full   # content:index + copy-fallback + copy-full-fallback
npm run content:fixtures          # validate examples vs io.output schemas
npm run content:layout-lint      # enforce folder-based layout under functions/
npm run content:primitives:sync   # merge seed bundles → Catalox `ai-functions` catalog (see below)
```

### Catalox merged primitive seeds

`content:primitives:sync` runs `scripts/syncPrimitiveSeedsToCatalox.ts`, which **merges** all bundles below and upserts **one normalized catalog item per primitive** (currently **144** items when all bundles are present). Code-bound ids store meta + IO schemas with empty instructions/rules; content-backed ids include full instruction packs.

**Existing env on v4 blob layout:** run `content:migrate:ai-functions:apply` once before deploying v5 runtime code (see [FuncX v5](#funcx-v5--normalized-ai-functions-catalog) above).

Regenerate seeds before sync:

```bash
npm run content:agent-platform:emit-seeds   # memorix, narrix, reportix, recipes, funcxCatalog, tokenBudget, humanValue
npm run content:ai-skills:emit-seeds
npm run content:primitives:sync
```

| Bundle | Path | Role |
| --- | --- | --- |
| Generic primitives | `content-seed/functions/primitives.seed.json` | Core library primitives (e.g. extract, classify, judge) |
| Catalog AI tasks | `content-seed/functions/catalogAiTasks.seed.json` | Catalog NL / ai-tasks helpers |
| Generic execution research | `content-seed/functions/genericExecutionResearch.seed.json` | Execution / research helpers |
| Runx JS / API / UI / script | `content-seed/functions/runx*.seed.json` | Runx authoring families |
| Runx condition / evolution | `content-seed/functions/runx.condition.seed.json`, `runx.evolution.seed.json` | Condition + evolution authoring |
| JSON repair | `content-seed/functions/repairJson*.seed.json` | Document/element repair (code-bound meta) |
| Condition skills | `content-seed/functions/conditionSkills.seed.json` | Content condition checker |
| Memorix / Narrix / Reportix | `content-seed/functions/memorix.seed.json`, `narrix.seed.json`, `reportix.seed.json` | Agent platform families |
| Content recipes | `content-seed/functions/contentRecipes.seed.json` | Recipe-driven composition |
| FuncX catalog | `content-seed/functions/funcxCatalog.seed.json` | Catalog discovery IO |
| Token budget | `content-seed/functions/tokenBudget.seed.json` | `tokenBudget.*` (code-bound) |
| Human value | `content-seed/functions/humanValue.seed.json` | `humanValue.*` (code-bound) |
| AI skills | `content-seed/functions/aiSkills.seed.json` | Gateway analysis skill |
| Code-bound meta | `content-seed/functions/codeBound.seed.json` | Readonly meta for router-only functions |

Companion **`*.io.schemas.json`** files are **generated** from TypeScript (`npm run content:agent-platform:emit-schemas` and Runx `emitRunx*` scripts) before syncing.

**Normalized item shape** (Mongo collection `catalogData-ai-functions-items` internally):

```json
{
  "data": {
    "itemId": "kv_<sha256('fx/' + functionId)>",
    "title": "...",
    "description": "...",
    "instructionsText": "...",
    "promptText": "",
    "rules": [],
    "io": { "inputSchema": {}, "outputSchema": {} },
    "testCases": [],
    "meta": {}
  },
  "indexed": { "functionId": "modifyItem", "scopeGeneric": true },
  "metadata": { "status": "draft", "updatedBy": "funcx-runtime" }
}
```

Runtime reads **`indexed.functionId`** and **`data.*`** only — not legacy `data.body` / `data.funcxRef` blob rows.

---

## Agent toolbox: workers, tool loop, and orchestrators

Function packs with several distinct workers (different system prompts, different JSON output shapes) and an optional orchestrator that calls them as tools can use the agent toolbox. Register workers once; the same registry drives both direct dispatch and orchestrator tool definitions — no second list to maintain.

### Minimal example: two workers, one orchestrator

```ts
import { createClient } from "@x12i/funcx";
import {
  createWorkerRegistry,
  runOrchestrator,
  composeInstructions,
} from "@x12i/funcx/functions";

// 1. Create an OpenRouter client.
const client = createClient({ backend: "openrouter" });

// 2. Register workers.  id === tool name exposed to the model.
const registry = createWorkerRegistry([
  {
    id: "graph_create",
    instructions: {
      normal: "You are a graph builder. Given a description, return a JSON graph with nodes and edges.",
    },
    tool: {
      description: "Create a workflow graph from a plain-text description.",
      parameters: {
        type: "object",
        properties: { description: { type: "string", description: "Plain-text description of the workflow." } },
        required: ["description"],
      },
    },
  },
  {
    id: "graph_explain",
    instructions: {
      normal: "You are a graph analyst. Given a JSON graph, return a plain-text explanation of its structure.",
    },
    tool: {
      description: "Explain an existing workflow graph in plain language.",
      parameters: {
        type: "object",
        properties: { graph: { type: "object", description: "The graph to explain." } },
        required: ["graph"],
      },
    },
  },
]);

// 3. Build the orchestrator system prompt from named segments.
const system = await composeInstructions({
  persona: "You are a workflow assistant.",
  rules:   "Use graph_create to build graphs, then graph_explain to describe them.",
  format:  "Respond concisely after all tool calls are complete.",
});

// 4. Run the orchestrator.  It calls graph_create then graph_explain and
//    returns the final model text.  No manual tool-dispatch loop required.
const result = await runOrchestrator({
  client,
  registry,
  system,
  userMessage: "Create a three-step onboarding workflow and explain it.",
  maxSteps: 6,
  // chatOptions sets the model for the orchestrator's own calls.
  // workerOpts sets the model for individual worker calls (can differ).
  chatOptions: { model: "deepseek/deepseek-v4-flash", maxTokens: 1024, temperature: 0.3 },
  workerOpts:  { model: "deepseek/deepseek-v4-flash", maxTokens: 512,  temperature: 0.1 },
  onStep:      (e) => console.log(`orchestrator step ${e.stepIndex}`),
  onWorkerEnd: (e) => console.log(`${e.workerId} finished in ${e.latencyMs}ms`),
});

console.log(result.text);
// => "The onboarding workflow starts with..."
console.log(result.steps, result.totalUsage);
```

**Token and latency budget:** each `runOrchestrator` call makes at least one model round-trip per tool invocation plus one final call for the text response. Plan for `(number of workers called + 1)` LLM requests. For a two-worker flow expect 3 calls and roughly 3× the per-call latency. Use `onStep` / `onWorkerEnd` callbacks to instrument cost and latency in production.

### Direct worker dispatch (no orchestrator)

```ts
const output = await registry.runWorker({
  workerId: "graph_create",
  request: { description: "User registration flow" },
  buildPrompt: (r) => `Create a graph for: ${r.description}`,
});
```

### Catalog-driven tools

When a worker pack grows, adding worker #7 only requires updating the registry. The tool list passed to the model derives from the same ids — no second array to keep in sync:

```ts
// All registered workers become tools automatically.
const tools = registry.getToolSpecs(); // ToolDefinition[]

// Or from an external catalog object (throws on id mismatch):
import { buildToolsFromCatalog } from "@x12i/funcx/functions";
const tools = buildToolsFromCatalog(registry, {
  graph_create:  { description: "...", parameters: { ... } },
  graph_explain: { description: "...", parameters: { ... } },
});
```

### Low-level tool loop

`runToolLoop` is the underlying primitive. Use it when you need full control over the message array or want to mix non-registry handlers:

```ts
import { runToolLoop } from "@x12i/funcx/functions";

const result = await runToolLoop({
  client,         // must implement client.chat()
  messages: [
    { role: "system", content: system },
    { role: "user",   content: "Go." },
  ],
  tools,
  handlers: {
    graph_create:  async (args) => JSON.stringify(await myCreateFn(args)),
    graph_explain: async (args) => await myExplainFn(args),
  },
  chatOptions:     { model: "deepseek/deepseek-v4-flash", maxTokens: 1024 },
  maxSteps:        10,
  toolErrorPolicy: "result",   // or "throw"
  signal:          AbortSignal.timeout(30_000),
});
```

---

## Privacy & data handling

- **Library usage (npm):** everything runs in your environment.
- **REST server:** does not store API keys. By default, recent usage is kept only in an in-memory ring buffer for `GET /activity`. If **`FUNCX_ACTIVIX_ENABLED=1`**, function-run and LLM-call metadata (and request/response snapshots as recorded for activity tracing) are written to MongoDB as described in [Optional MongoDB activity persistence (Activix)](#optional-mongodb-activity-persistence-activix).

---

## Security notes

- Never commit `.env`
- Don't log provider keys
- Add `.content/` to `.gitignore`

---

## Testing

```bash
npm run test:unit          # unit tests only (no API key required; includes humanValue, tokenBudget, runx/io schema tests)
npm run test:live          # live integration tests (requires OPENROUTER_API_KEY / OPEN_ROUTER_KEY)
npm run test:live:agent    # live tests for the agent toolbox only
npm test                   # full suite
npm run typecheck          # TypeScript check

Human value unit tests (also in `test:unit` after build): `npx tsx --test test/humanValue.*.test.ts`
```

---

## Documentation

- [Managing instructions and rules](.docs/MANAGING-INSTRUCTIONS-AND-RULES.md) — Catalox content layout and normalized catalog fields
- [Catalox model catalog verification](.docs/CATALOX-MODEL-CATALOG-VERIFICATION.md) — model metadata is ai-profiles only, not Catalox
- [Library functions — manual mode](.docs/LIBRARY-FUNCTIONS-MANUAL-MODE.md) — full list of built-in functions and programmatic use with rules/optimization (includes `runx*`, `runx.api*`, `runx.ui*`, `runx.script*` authoring IDs)
- [Gateway invoke analysis](.docs/GATEWAY-INVOKE-ANALYSIS.md) — pre-invoke packet review (`functionId`, deterministic vs LLM layers)
- [Library index: JSON and API](.docs/library-index-json-and-api.md) — index format, update commands, and HTTP API for the catalog
- [API contract](docs/API_CONTRACT.md) and [Contract sync](docs/CONTRACT_SYNC.md) — when present in `docs/`; otherwise the REST API section above is the source of truth for endpoints

## Links

- GitHub: [x12i/funcx](https://github.com/x12i/funcx) (repository and homepage)
- npm package: [`@x12i/funcx`](https://www.npmjs.com/package/@x12i/funcx)

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