TypeScript SDK
Install the @foilengine/sdk package for Node.js, web, Deno, or Bun projects.
Installation
npm install @foilengine/sdkQuick 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 scoreConfiguration
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
| Method | Returns | Description |
|---|---|---|
client.personas.list() | Promise<Persona[]> | List all published personas |
Utilities
| Method | Returns | Description |
|---|---|---|
client.validateLlmKey(model?) | Promise<ValidateLlmKeyResult> | Validate your LLM API key |
Machines
| Method | Returns | Description |
|---|---|---|
client.machines.list(personaId, userSessionId) | Promise<MachineInfo[]> | List available machines for a player |
Chat
| Method | Returns | Description |
|---|---|---|
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 Class | Status | When |
|---|---|---|
BadRequestError | 400 | Invalid UUID, missing fields |
AuthenticationError | 401 | Missing or invalid API key |
ForbiddenError | 403 | Not owner or persona unpublished |
NotFoundError | 404 | Resource not found |
RateLimitError | 429 | Rate limit exceeded |
ServerError | 500 | Internal 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}`));| Event | Data | When |
|---|---|---|
state_change | from_state, to_state, decision | State machine transitions to a new state |
score_change | old_score, new_score, delta | Session score changes |
machine_completed | outcome, final_score, session_id | Conversation reaches a terminal state |
machines_unlocked | machines (list of machine_key, name) | New machines become available after completion |
session_ended | outcome, final_score, session_id | Session 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.');