RYNXDocumentation
rynxlabs is a server-side TypeScript SDK. Keep provider credentials on your trusted backend—never in a browser or game binary.

RYNX Labs

RYNX Labs AI NPC Brain SDK

The AI brain for living game characters.

rynxlabs is a server-side, framework-agnostic TypeScript SDK for building stateful AI NPCs with persistent memory, explicit personality constraints, context-aware dialogue, and bounded action recommendations.

The SDK treats an AI model as a recommendation engine, never as an authority over game state. A model may propose dialogue, emotion, intent, memory updates, and one developer-approved action. It cannot execute gameplay code, mutate the world, grant rewards, start quests, or call game systems directly. Your application validates the recommendation and decides what happens.

Contents

Capabilities

  • Persistent, player-specific NPC memory

  • Declarative personality traits, goals, speaking styles, and behavioral boundaries

  • Context-aware dialogue based on location, quests, world state, and player progression

  • Structured emotion, intent, safety, and action output

  • Per-request action allowlists controlled by the game developer

  • Runtime request and response validation with Zod

  • OpenAI as the default provider through the official JavaScript SDK

  • Provider abstraction for future model vendors or self-hosted inference systems

  • Injectable memory and logging implementations

  • Typed operational errors for validation, provider, timeout, rate-limit, and response failures

  • Dependency-free integration helpers for server frameworks

  • Backend client examples for Unity, Godot, and Unreal Engine

  • ESM and CommonJS artifacts with generated TypeScript declarations

Technical principles

The model does not execute game actions

selectedAction is data. RYNX never maps an action ID to a function, dispatches an event, changes inventory, modifies player progression, or mutates game state. Execution remains an explicit application responsibility:

RYNX SDK

const  response = await  rynx.npc.respond(request);

  

if (response.safety.allowed && response.selectedAction?.id === "start_quest") {

const  eligible  =  await questService.isEligible(request.player.id, "lost-signal");

  

if (eligible) {

await questService.start(request.player.id, "lost-signal");

}

}

Boundaries are layered

RYNX combines multiple controls instead of relying on a prompt alone:

  1. Input schemas reject malformed configuration, profiles, contexts, and action definitions.

  2. User-controlled strings are sanitized before prompt construction.

  3. The system prompt marks game and player content as untrusted data.

  4. The provider requests a strict JSON-schema response.

  5. The returned payload is validated again with Zod.

  6. selectedAction.id is checked against the request's runtime allowlist.

  7. Unsupported actions are removed and recorded as a safety failure.

  8. Only validated memory updates are persisted.

Technology Stack

LayerTechnologyPurpose
LanguageTypeScript · strict modePublic API types, provider contracts, and compile-time safety
RuntimeNode.js 18+Server-side execution with modern JavaScript APIs
Default AI ProviderOfficial openai SDKAuthentication, structured model requests, and provider error handling
Runtime ValidationZodValidates configuration, profiles, requests, actions, memories, and model output
Build SystemtsupESM/CJS bundles, source maps, and type declarations
CompilerTypeScriptStatic type checks and declaration validation
TestingVitestOffline unit and integration-style tests
LintingESLint + typescript-eslintType-aware static analysis
FormattingPrettierConsistent source and documentation formatting
Local ExamplesdotenvEnvironment-variable loading for server examples only
PersistencePluggable MemoryStoreNo database dependency in the base package

The runtime package has only two direct dependencies: openai and zod. Frameworks and databases are intentionally excluded from the core dependency graph.

Installation

RYNX SDK

npm  i  rynxlabs

Requirements:

  • Node.js 18 or newer

  • A modern TypeScript project or Node.js application

  • An OpenAI API key when using the default provider

  • A trusted backend runtime; never embed provider credentials in a browser or game binary

For local examples, copy .env.example to .env and set OPENAI_API_KEY. The .env file is ignored by Git; .env.example remains tracked.

Quick start

RYNX SDK

import  { InMemoryMemoryStore, Rynx }  from  "rynxlabs";

  

const  rynx = new  Rynx({

apiKey: process.env.OPENAI_API_KEY!,

model:  "gpt-5-mini",

timeoutMs:  30_000,

maxRetries:  2,

memoryStore:  new  InMemoryMemoryStore(),

});

  

const  response = await  rynx.npc.respond({

npc: {

id:  "captain-ava",

name:  "Captain Ava",

role:  "Harbor commander",

personality: {

traits: ["disciplined", "protective", "observant"],

speakingStyle:  "Direct, calm, and concise.",

goals: ["Protect the harbor", "Find the missing signal"],

boundaries: [

"Never reveal private military coordinates",

"Never promise rewards without player eligibility",

],

},

},

player: {

id:  "player-42",

name:  "Mika",

progression: {

level:  12,

completedQuests: ["first-arrival"],

reputation:  "trusted",

},

},

context: {

location:  "Harbor District",

quest:  "lost-signal",

worldState: ["Storm approaching", "Signal tower offline"],

playerMessage:  "Can you help me find the signal?",

},

allowedActions: [

{

id:  "guide_player",

description:  "Give the player a destination or next step",

},

{

id:  "start_quest",

description:  "Offer the Lost Signal quest",

},

{

id:  "deny_request",

description:  "Politely decline an unavailable request",

},

],

});

  

console.log(response.dialogue);

console.log(response.selectedAction);

Architecture and request lifecycle

RYNX SDK

Game Server
    │
    │  Rynx.npc.respond(request)
    ▼
┌─────────────────────────────────────────────┐
│ Zod Input Validation                        │
└─────────────────────────────────────────────┘
    │
    ├── Invalid request → RynxValidationError
    ▼
┌─────────────────────────────────────────────┐
│ Load Memories                               │
│ npcId + playerId                            │
└─────────────────────────────────────────────┘
    ▼
┌─────────────────────────────────────────────┐
│ Sanitize Data & Build                       │
│ Constrained System Prompt                   │
└─────────────────────────────────────────────┘
    ▼
┌─────────────────────────────────────────────┐
│ AIProvider.generate(...)                    │
│ OpenAI structured JSON by default           │
└─────────────────────────────────────────────┘
    ▼
┌─────────────────────────────────────────────┐
│ Zod Response Validation                     │
└─────────────────────────────────────────────┘
    │
    ├── Invalid output → RynxResponseError
    ▼
┌─────────────────────────────────────────────┐
│ Runtime Allowed-Action Enforcement          │
│ Unsupported action → null + safety reason   │
└─────────────────────────────────────────────┘
    ▼
┌─────────────────────────────────────────────┐
│ Persist Validated Memory Updates            │
│ Skipped when memory is disabled             │
└─────────────────────────────────────────────┘
    ▼
┌─────────────────────────────────────────────┐
│ Return Typed `NpcResponse`                  │
└─────────────────────────────────────────────┘
    ▼
Developer-Owned Authorization & Action Execution

The public Rynx client exposes two service boundaries:

RYNX SDK

class  Rynx {

readonly  npc: NpcService;

readonly  memory: MemoryService;

}

NpcService owns profiles, prompt orchestration, provider calls, response validation, safety enforcement, and automatic memory persistence. MemoryService exposes the configured store without coupling application code to a specific database.

Response contract

Every successful respond() call returns a validated NpcResponse:

RYNX SDK

type  NpcResponse = {

dialogue: string;

emotion: {

label: string;

intensity: number; // 0..1

};

intent: string;

selectedAction: {

id: string;

reason: string;

confidence: number; // 0..1

} | null;

memoryUpdates: Array<{

type: "fact" | "relationship" | "event" | "preference";

content: string;

importance: number; // 0..1

}>;

safety: {

allowed: boolean;

reasons: string[];

};

metadata: {

model: string;

requestId: string;

latencyMs: number;

};

};

Example:

RYNX SDK

{

dialogue: "The signal tower is north. Stay on the marked path.",

emotion: { label: "concerned", intensity: 0.6 },

intent: "guide the player safely",

selectedAction: {

id: "guide_player",

reason: "The player requested a safe route.",

confidence: 0.95

},

memoryUpdates: [

{

type:  "event",

content:  "Mika asked Captain Ava about the missing signal.",

importance:  0.7

}

],

safety: { allowed: true, reasons: [] },

metadata: {

model: "gpt-5-mini",

requestId: "req_7d3155d0-...",

latencyMs: 850

}

}

metadata.latencyMs covers SDK orchestration from request handling through validated response construction. requestId is safe to use for application-level tracing.

NPC profiles

Profiles may be supplied inline to respond() or managed through the in-process profile API:

RYNX SDK

const  profile = rynx.npc.createProfile({

id:  "captain-ava",

name:  "Captain Ava",

role:  "Harbor commander",

personality: {

traits: ["disciplined", "protective"],

speakingStyle:  "Direct and calm.",

goals: ["Protect the harbor"],

boundaries: ["Never reveal military coordinates"],

},

});

  

const  current = rynx.npc.getProfile("captain-ava");

  

const  updated = rynx.npc.updateProfile("captain-ava", {

role:  "Acting admiral",

personality: {

speakingStyle:  "Formal, direct, and concise.",

},

});

The built-in profile registry is process-local. Persist canonical profile definitions in your own content system or database if they must survive restarts or be shared across server instances.

Memory system

Memory is scoped by the (npcId, playerId) pair. Valid model-generated updates are persisted after response validation unless persistMemory: false is set.

RYNX SDK

const  memories = await  rynx.memory.getMemories("captain-ava", "player-42");

  

await  rynx.memory.addMemories("captain-ava", "player-42", [

{

type:  "preference",

content:  "Mika prefers direct answers.",

importance:  0.8,

},

]);

  

await  rynx.memory.clearMemories("captain-ava", "player-42");

  

await  rynx.npc.respond({

...request,

persistMemory:  false,

});

Each stored record contains:

RYNX SDK

interface  MemoryRecord {

id: string;

npcId: string;

playerId: string;

type: "fact" | "relationship" | "event" | "preference";

content: string;

importance: number;

createdAt: Date;

}

Custom memory store

Implement MemoryStore to connect PostgreSQL, Redis, DynamoDB, an embedding index, or another durable system:

RYNX SDK

import  type  { MemoryInput, MemoryRecord, MemoryStore }  from  "rynxlabs";

  

class  DatabaseMemoryStore  implements  MemoryStore {

async  getMemories(npcId: string, playerId: string, limit = 20): Promise<MemoryRecord[]> {

return database.memories.findRelevant({ npcId, playerId, limit });

}

  

async  addMemories(

npcId: string,

playerId: string,

memories: readonly  MemoryInput[],

): Promise<MemoryRecord[]> {

return database.memories.insertMany({ npcId, playerId, memories });

}

  

async  clearMemories(npcId: string, playerId: string): Promise<void> {

await database.memories.deleteMany({ npcId, playerId });

}

}

The included InMemoryMemoryStore is intended for development, testing, prototypes, and single-process games. It prioritizes importance and then recency. It is not durable and is not shared across processes.

Allowed actions and safety

An allowed action has a stable machine-readable ID and a model-facing description:

RYNX SDK

const  allowedActions = [

{

id:  "guide_player",

description:  "Give the player a destination or next step",

},

{

id:  "deny_request",

description:  "Politely decline a request that is unavailable or unsafe",

},

] as  const;

Treat the response as a proposal and independently enforce domain rules:

RYNX SDK

switch (response.selectedAction?.id) {

case  "guide_player":

navigationUi.showObjective("signal-tower");

break;

  

case  "deny_request":

case  undefined:

break;

  

default:

// Defensive application-level fallback.

logger.warn("Unhandled NPC recommendation", {

requestId: response.metadata.requestId,

});

}

If a provider returns launch_missile when only guide_player and deny_request are allowed, the SDK returns:

RYNX SDK

{

selectedAction: null,

safety: {

allowed: false,

reasons: ["Unsupported action 'launch_missile' was removed."]

}

}

This behavior prevents model output from expanding its own authority. It does not replace authentication, player eligibility checks, server-side authorization, cooldown validation, anti-cheat systems, or transaction controls.

Provider architecture

OpenAI is used when no custom provider is supplied. The built-in adapter:

  • Uses the official OpenAI JavaScript SDK

  • Requests structured JSON-schema output

  • Disables SDK-internal retries and applies the configured RYNX retry policy

  • Retries eligible rate-limit, connection, and server failures

  • Maps timeout and rate-limit failures into typed RYNX errors

  • Avoids returning credentials in response objects

Custom provider

Implement AIProvider to add another inference service without changing game-facing SDK calls:

RYNX SDK

import  type  { AIProvider, AIProviderRequest, AIProviderResponse }  from  "rynxlabs";

  

class  StudioInferenceProvider  implements  AIProvider {

async  generate(request: AIProviderRequest): Promise<AIProviderResponse> {

const  result  =  await studioModel.generate({

system: request.systemPrompt,

requestId: request.requestId,

});

  

return {

content: result.structuredOutput,

model: result.modelVersion,

};

}

}

  

const  rynx = new  Rynx({

// Configuration validation still requires a non-empty key. Custom providers

// should receive and manage their own credentials outside returned objects.

apiKey: process.env.STUDIO_PROVIDER_KEY!,

provider:  new  StudioInferenceProvider(),

});

Provider output is always treated as unknown and must pass the same Zod response validation and action allowlist enforcement as OpenAI output.

Validation and error model

Public Zod schemas are exported for validating incoming HTTP bodies before invoking the SDK:

RYNX SDK

import  { NpcRespondRequestSchema }  from  "rynxlabs";

  

const  parsed = NpcRespondRequestSchema.safeParse(untrustedBody);

  

if (!parsed.success) {

return {

status:  400,

body: { error:  "Invalid NPC request", issues: parsed.error.issues },

};

}

  

const  response = await  rynx.npc.respond(parsed.data);

Typed errors

| Error | Meaning | Typical handling |

| ------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------- |

| RynxConfigurationError | Invalid API key, model, URL, timeout, retry count, or injectable dependency | Fail during application startup |

| RynxValidationError | Invalid profile, request, action, or profile patch | Return a client-safe 400 response |

| RynxProviderError | Provider transport, API, or malformed JSON failure | Log request ID and return a temporary service error |

| RynxTimeoutError | Provider request exceeded its timeout | Retry at the application boundary when appropriate |

| RynxRateLimitError | Provider rate limit was reached | Apply backoff, queueing, or load shedding |

| RynxResponseError | Provider output failed the strict RYNX response contract | Do not execute or persist the output |

RYNX SDK

import  {

RynxProviderError,

RynxRateLimitError,

RynxResponseError,

RynxTimeoutError,

RynxValidationError,

}  from  "rynxlabs";

  

try  {

const  response  =  await rynx.npc.respond(request);

return response;

}  catch (error) {

if (error instanceof  RynxValidationError) {

logger.info("Rejected NPC request", { issues: error.issues });

} else  if (error instanceof  RynxRateLimitError) {

logger.warn("NPC provider rate limited");

} else  if (error instanceof  RynxTimeoutError) {

logger.warn("NPC provider timed out");

} else  if (error instanceof  RynxResponseError) {

logger.error("NPC provider returned an invalid contract");

} else  if (error instanceof  RynxProviderError) {

logger.error("NPC provider request failed");

}

  

throw error;

}

Do not serialize raw error causes to untrusted clients. Provider causes may include operational metadata even though the SDK uses key-safe public messages.

Configuration reference

RYNX SDK

type  RynxConfig = {

apiKey: string;

model?: string;

timeoutMs?: number;

maxRetries?: number;

memoryStore?: MemoryStore;

logger?: Logger;

baseUrl?: string;

provider?: AIProvider;

};

Configuration

OptionTypeDefaultDescription
apiKeystringRequiredServer-side provider credential. Must be non-empty.
modelstringgpt-5-miniModel used by the default OpenAI provider.
timeoutMsPositive integer30000Provider request timeout, in milliseconds.
maxRetriesInteger (0–10)2Retries for eligible transient provider failures.
memoryStoreMemoryStoreInMemoryMemoryStoreMemory persistence and retrieval implementation.
loggerLoggerSilent loggerStructured debug, info, warn, and error sink.
baseUrlAbsolute URLOpenAI defaultOptional OpenAI-compatible API endpoint.
providerAIProviderOpenAIProviderCustom AI provider implementation.

Logger contract

RYNX SDK

interface  Logger {

debug(message: string, context?: Readonly<Record<string, unknown>>): void;

info(message: string, context?: Readonly<Record<string, unknown>>): void;

warn(message: string, context?: Readonly<Record<string, unknown>>): void;

error(message: string, context?: Readonly<Record<string, unknown>>): void;

}

Logger contexts contain safe SDK metadata such as request IDs and rejected action IDs. Never add provider credentials to custom logger wrappers.

Integrations

Express

See the complete Express server example. The endpoint validates the request before contacting the provider:

RYNX SDK

import  express  from  "express";

import  { NpcRespondRequestSchema, Rynx }  from  "rynxlabs";

  

const  app = express();

const  rynx = new  Rynx({ apiKey: process.env.OPENAI_API_KEY!  });

  

app.use(express.json({ limit:  "64kb"  }));

  

app.post("/api/npc/respond", async (req, res) =>  {

const  parsed  = NpcRespondRequestSchema.safeParse(req.body);

  

if (!parsed.success) {

res.status(400).json({ error:  "Invalid request", issues: parsed.error.issues });

return;

}

  

try {

res.json(await rynx.npc.respond(parsed.data));

} catch {

res.status(502).json({ error:  "NPC provider request failed" });

}

});

createExpressNpcHandler() is also exported as a lightweight Express-compatible adapter without making Express a runtime dependency.

Next.js App Router

See the Next.js route example. Place it at app/api/npc/respond/route.ts, import server-only, and read OPENAI_API_KEY exclusively from the server environment. Browser code should call this route, never OpenAI directly.

Unity

The Unity example uses UnityWebRequest to call the developer's authenticated backend endpoint. Do not place an OpenAI key in C# source, PlayerPrefs, Resources, asset bundles, or build-time client configuration.

Godot

The Godot example uses HTTPRequest and JSON to call the backend. Exported games must not contain provider credentials.

Unreal Engine

The Unreal example uses the HTTP module from C++. Add HTTP to module dependencies and send requests only to your trusted backend.

Security and production hardening

RYNX constrains model output, but production deployments should also provide:

  • Authentication for every NPC endpoint

  • Authorization binding the authenticated account to player.id

  • Request-body size limits

  • Per-player, per-IP, and global rate limits

  • Timeouts and concurrency limits at the reverse proxy and application layers

  • Input moderation appropriate to the game's audience

  • Server-derived progression and eligibility data instead of client-asserted authority

  • Idempotency and transactional checks for consequential game operations

  • Audit logs keyed by metadata.requestId

  • Memory retention, deletion, privacy, and regional storage policies

  • Encryption and secret management for provider credentials

  • Output escaping appropriate to the rendering target

  • Monitoring for elevated validation failures, unsupported action attempts, and provider errors

Prompt injection defenses reduce risk but cannot turn arbitrary model output into trusted authorization. The final authority must remain deterministic server-side game code.

Package formats and exports

The package is built with tsup and publishes only the compiled distribution, README, license, and package metadata.

RYNX SDK

dist/index.js ESM entry

dist/index.cjs CommonJS entry

dist/index.d.ts ESM TypeScript declarations

dist/index.d.cts CommonJS TypeScript declarations

*.map Source maps

The package export map supports both module systems:

RYNX SDK

// ESM

import  { Rynx }  from  "rynxlabs";

  

// CommonJS

const { Rynx } = require("rynxlabs");

Development and testing

RYNX SDK

npm  install

npm  run  typecheck

npm  run  lint

npm  test

npm  run  build

npm  run  format:check

Additional scripts:

| Command | Purpose |

| ------------------------ | ------------------------------------------------------ |

| npm run dev | Rebuild in watch mode |

| npm run test:watch | Run Vitest interactively |

| npm run format | Apply Prettier formatting |

| npm run prepublishOnly | Run typecheck, lint, tests, and build before packaging |

Tests inject mock providers and make no outbound OpenAI requests. The suite covers configuration defaults and failures, typed response validation, invalid output rejection, unsupported action removal, memory persistence and opt-out, provider error propagation, prompt-injection resistance, and server-side key usage in integration examples.

Project structure

RYNX SDK

rynxlabs/

src/
  index.ts                  Public exports
  client.ts                 Rynx composition root
  config.ts                 Configuration schema and defaults
  errors.ts                 Typed error hierarchy
  schemas.ts                Public schema exports
  types.ts                  Public type exports

  providers/
    types.ts                Provider interfaces
    openai-provider.ts      Official OpenAI adapter

  npc/
    npc-service.ts          Response orchestration and NPC profiles
    prompts.ts              Constrained prompt construction
    schemas.ts              NPC request and response schemas
    types.ts                NPC domain types

  memory/
    types.ts                Memory contracts
    memory-store.ts         Public memory facade
    in-memory-store.ts      Volatile reference implementation

  integrations/             Dependency-free integration helpers
  utils/                    IDs, sanitization, retry, and logging

tests/                      Offline Vitest suite and fixtures

examples/
  basic-node/
  express-server/
  nextjs-api-route/
  unity/
  godot/
  unreal/

License

MIT - Copyright 2026 RYNX Labs.

MIT · © 2026 RYNX Labs