TypeScript SDK

Install the @foilengine/sdk package for Node.js, web, Deno, or Bun projects.

Installation

npm install @foilengine/sdk

Quick Start

import { FoilEngineClient } from '@foilengine/sdk';

const client = new FoilEngineClient({
  apiKey: 'pk_live_...',
  llmApiKey: 'sk-...',   // your LLM provider API key
  llmModel: 'gpt-4o',    // any LiteLLM-supported model
});

// Discover your personas
const personas = await client.personas.list();
console.log(personas[0].name); // "Grumpy Barista"

// Initialize a session
const session = await client.chat.initSession(personas[0].id, {
  userSessionId: 'player-001',
  playerName: 'Alex',
  playerGender: 'non-binary',
});
console.log(session.message); // NPC's greeting

// Send a message
const response = await client.chat.sendMessage(personas[0].id, {
  message: 'What do you recommend?',
  userSessionId: 'player-001',
});
console.log(response.message);       // NPC's reply
console.log(response.current_state); // State machine state
console.log(response.score);         // Session score

Configuration

const client = new FoilEngineClient({
  apiKey: 'pk_live_...',             // required
  llmApiKey: 'sk-...',              // required for chat endpoints
  llmModel: 'gpt-4o',              // optional – default LLM model
  llmEvalModel: 'gpt-4o-mini',     // optional – model for evaluation
  llmResponseModel: 'gpt-4o',      // optional – model for NPC responses
  llmSummarizationModel: 'gpt-4o-mini', // optional – model for summaries
  llmEvalApiKey: 'sk-ant-...',     // optional – different provider for eval
  llmResponseApiKey: 'sk-...',     // optional – different provider for responses
  llmSummarizationApiKey: 'sk-...', // optional – different provider for summaries
  baseUrl: 'http://localhost:8000',  // default: https://api.foilengine.io
  timeout: 30,                       // default: 30 (seconds)
  maxRetries: 3,                     // default: 3
  debug: true,                       // optional – log requests/responses
  cacheTtl: 60,                      // optional – cache personas.list() for 60s (0 to disable)
  hooks: [{ afterResponse: (url, res, ms) => console.log(url, ms) }], // optional – lifecycle hooks
});

Available Methods

Personas

MethodReturnsDescription
client.personas.list()Promise<Persona[]>List all published personas

Utilities

MethodReturnsDescription
client.validateLlmKey(model?)Promise<ValidateLlmKeyResult>Validate your LLM API key

Machines

MethodReturnsDescription
client.machines.list(personaId, userSessionId)Promise<MachineInfo[]>List available machines for a player

Chat

MethodReturnsDescription
client.chat.initSession(personaId, options)Promise<SessionInit>Start a new conversation
client.chat.sendMessage(personaId, options)Promise<ChatResponse>Send a message, get NPC reply
client.chat.sendMessageStream(personaId, options)AsyncGenerator<ChatStreamEvent>Stream NPC response via SSE (metadata first, then text chunks, then done)
client.chat.getSession(personaId, userSessionId)Promise<SessionStatus>Check if a session exists
client.chat.getHistory(personaId, userSessionId)Promise<ChatHistory>Get full message history
client.chat.reset(personaId, userSessionId)Promise<ResetResult>Delete a session

Error Handling

import {
  FoilEngineClient,
  NotFoundError,
  AuthenticationError,
  RateLimitError,
} from '@foilengine/sdk';

const client = new FoilEngineClient({
  apiKey: 'pk_live_...',
  llmApiKey: 'sk-...',
});

try {
  const session = await client.chat.getSession(personaId, 'player-001');
} catch (e) {
  if (e instanceof NotFoundError) {
    // No existing session — initialize one
    const session = await client.chat.initSession(personaId, { ... });
  } else if (e instanceof RateLimitError) {
    console.log(`Retry after ${e.retryAfter}s`);
  } else if (e instanceof AuthenticationError) {
    console.error('Invalid API key');
  }
}
Error ClassStatusWhen
BadRequestError400Invalid UUID, missing fields
AuthenticationError401Missing or invalid API key
ForbiddenError403Not owner or persona unpublished
NotFoundError404Resource not found
RateLimitError429Rate limit exceeded
ServerError500Internal server error
💡Tip
The SDK automatically retries on 429 and 5xx errors with jittered exponential backoff (up to 3 retries). Uses native fetch with zero dependencies. Enable debug: true to see request/response logs.

Streaming Responses

Stream NPC responses token-by-token for a typing effect. Metadata (state, score, decision) arrives before any text.

for await (const event of client.chat.sendMessageStream(personaId, {
  message: 'Tell me about your potions.',
  userSessionId: 'player-001',
})) {
  if (event.event_type === 'metadata') {
    // All game state available before text starts
    const meta = event.data as ChatStreamMetadata;
    console.log(`State: ${meta.current_state}, Score: ${meta.score}`);
  } else if (event.event_type === 'text_delta') {
    // Progressive text chunks
    process.stdout.write((event.data as { text: string }).text);
  } else if (event.event_type === 'done') {
    // Full ChatResponse for final reconciliation
    const response = event.data as ChatResponse;
    console.log(`\nFinal: ${response.message}`);
  }
}

Events

Register callbacks to react to game-relevant events. Events fire automatically after each sendMessage() call.

client.on('state_change', (e) => console.log(`State: ${e.from_state} -> ${e.to_state}`));
client.on('score_change', (e) => console.log(`Score: ${e.old_score} -> ${e.new_score}`));
client.on('machine_completed', (e) => console.log(`Done! Outcome: ${e.outcome}`));
client.on('machines_unlocked', (e) => console.log(`Unlocked: ${JSON.stringify(e.machines)}`));
client.on('session_ended', (e) => console.log(`Session ended: ${e.outcome}`));
EventDataWhen
state_changefrom_state, to_state, decisionState machine transitions to a new state
score_changeold_score, new_score, deltaSession score changes
machine_completedoutcome, final_score, session_idConversation reaches a terminal state
machines_unlockedmachines (list of machine_key, name)New machines become available after completion
session_endedoutcome, final_score, session_idSession receives an outcome (ACCEPT, REJECT, KICK_OUT)

TypeScript Types

All request and response types are fully typed and exported:

import type {
  Persona,
  MachineInfo,
  SessionInit,
  ChatResponse,
  ChatHistory,
  ChatStreamEvent,
  ChatStreamMetadata,
  SessionStatus,
  UnlockedMachine,
  InitSessionOptions,
  SendMessageOptions,
  SendMessageStreamOptions,
  ValidateLlmKeyResult,
  RequestHook,
  EventName,
  EventCallback,
} from '@foilengine/sdk';

Complete Example

A full, runnable script that initializes a session, sends messages, handles streaming, and listens for events:

import { FoilEngineClient } from '@foilengine/sdk';

const client = new FoilEngineClient({
  apiKey: 'pk_live_...',
  llmApiKey: 'sk-...',
  llmModel: 'gpt-4o',
});

// Listen for game-relevant events
client.on('state_change', (e) => console.log(`  [state] ${e.from_state} -> ${e.to_state}`));
client.on('score_change', (e) => console.log(`  [score] ${e.old_score} -> ${e.new_score}`));
client.on('machine_completed', (e) => console.log(`  [done] Outcome: ${e.outcome}, Score: ${e.final_score}`));

// Discover personas
const personas = await client.personas.list();
const persona = personas[0];
console.log(`Chatting with: ${persona.name}`);

// Start a session
const session = await client.chat.initSession(persona.id, {
  userSessionId: 'player-001',
  playerName: 'Alex',
  playerGender: 'non-binary',
});
console.log(`NPC: ${session.message}`);

// Send a message (non-streaming)
const response = await client.chat.sendMessage(persona.id, {
  message: 'What do you have for sale?',
  userSessionId: 'player-001',
});
console.log(`NPC: ${response.message}`);
console.log(`  State: ${response.current_state}, Score: ${response.score}`);

// Send a message (streaming)
process.stdout.write('NPC: ');
for await (const event of client.chat.sendMessageStream(persona.id, {
  message: 'Tell me more about that.',
  userSessionId: 'player-001',
})) {
  if (event.event_type === 'text_delta') {
    process.stdout.write((event.data as { text: string }).text);
  } else if (event.event_type === 'done') {
    console.log(); // Newline after streaming
  }
}

// Check session status
const status = await client.chat.getSession(persona.id, 'player-001');
console.log(`Session state: ${status.current_state}, score: ${status.score}`);

// Reset when done
await client.chat.reset(persona.id, 'player-001');
console.log('Session reset.');