PlayKit.ai

NPC Conversations

Create intelligent game characters with NPCClient

NPC Conversations

NPCClient is a client class designed specifically for game NPC (Non-Player Character) conversations. It features automatic history management, character design, memory systems, action triggering, and reply predictions - letting you focus on character design and dialogue logic.

ChatClient vs NPCClient

The SDK provides two clients for AI conversation, each with a different design goal.

ChatClient — developer-facing

ChatClient exposes the minimum necessary API: you supply the messages, it returns a response. History, context, and output format are entirely yours to manage.

const chat = sdk.createChatClient();
const response = await chat.chat('What items do you sell?', 'You are a merchant.');
// You own the message list, output format, and any parsing

Use it for: custom chat UIs, structured data generation (generateStructuredByName), tool/function calling pipelines, multi-step AI workflows.

NPCClient — game-designer-facing

NPCClient is a character system. You define the personality; the SDK handles history, memory, reply predictions, and action dispatch. The API surface is deliberately small.

const npc = sdk.createNPCClient({
  characterDesign: 'You are a gruff blacksmith named Arn...'
});
const reply = await npc.talk('I need a sword.');

Use it for: interactive NPCs, companion characters, dialogue scenes.

Why NPC uses Actions instead of structured output

NPCClient has no structured output method by design. The alternative is the Actions system:

Speed. Structured output adds tokens and a parsing step — the model must produce valid JSON before your game can react. Actions are embedded inline in the dialogue and execute the moment the model signals them, with no parsing overhead.

Character integrity. A structured-output call produces a JSON object, not a sentence — the character voice breaks. Actions let the NPC speak naturally and trigger game logic in the same response, without exposing a data format to the player.

const response = await npc.talkWithActions('I want to buy a sword.', [
  { actionName: 'OpenShop',
    description: 'Open the shop inventory' },
  { actionName: 'GiveItem',
    description: 'Give an item to the player',
    parameters: [{ name: 'item_name', type: 'string', description: 'Item name', required: true }] }
]);

console.log(response.text);         // NPC's natural dialogue
for (const action of response.actionCalls) {
  console.log(action.actionName, action.arguments);  // inline triggers
}

For structured data that lives outside the character (item stats, quest definitions, world state), use chat.generateStructuredByName() — not the NPC.

At a glance

ChatClientNPCClient
AudienceDevelopersGame designers
HistoryManualAutomatic
OutputRaw string / typed objectNatural dialogue + action calls
Structured datagenerateStructuredByNameUse Actions instead
Game logic integrationParse response manuallyAction system
MemorysetMemory / getMemory
Save / restoresaveHistory / loadHistory

Create NPCClient

import { PlayKitSDK } from 'playkit-sdk';

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  apiKey: 'your-api-key'
});

await sdk.initialize();

const npc = sdk.createNPCClient();

const wizard = sdk.createNPCClient({
  characterDesign: 'You are an ancient wizard who speaks in riddles.',
  temperature: 0.8,
  maxHistoryLength: 30
});

Basic Conversation

Simple Talk

const response = await npc.talk('Hello, who are you?');
console.log('NPC:', response);

const response2 = await npc.talk('What quest do you have for me?');
console.log('NPC:', response2);

Streaming Responses

Display responses in real-time as they generate:

await npc.talkStream(
  'Tell me about the ancient kingdom',
  (chunk) => {
    process.stdout.write(chunk);
  },
  (fullText) => {
    console.log('\n--- Response complete ---');
  }
);

Check Speaking Status

if (npc.isTalking) {
  console.log('NPC is still responding...');
}

Character Design

Character design defines the NPC's personality, background, and behavior.

Set Character Design

const npc = sdk.createNPCClient({
  characterDesign: `You are Eldric, a mysterious wizard.
Background: You have lived for 300 years in the Enchanted Forest.
Personality: Wise but cryptic, often speaking in riddles.
Goal: Guide adventurers while testing their wisdom.
Speech style: Formal, uses archaic words like "thee" and "hark".`
});

// Or set after creation
npc.setCharacterDesign(`You are a cheerful tavern keeper named Rose.
You love gossip and know everything happening in town.`);

Get Current Design

const design = npc.getCharacterDesign();
console.log('Current character:', design);

Memory System

Memories let you dynamically add context that affects NPC responses without changing the base character design.

Set Memories

// Add player information
npc.setMemory('playerName', 'Aragorn');
npc.setMemory('playerClass', 'Ranger');
npc.setMemory('relationship', 'Friendly - helped the NPC before');

// Add world state
npc.setMemory('currentQuest', 'Defeat the Dragon of Mount Doom');
npc.setMemory('worldState', 'The kingdom is at war with the northern tribes');

// NPC responses now incorporate these memories
const response = await npc.talk('Do you remember me?');
// NPC will reference knowing Aragorn, the ranger who helped before

Update Memories

// Memories can be updated as the game progresses
npc.setMemory('relationship', 'Very friendly - saved the NPC\'s life');
npc.setMemory('playerLevel', '15');

Remove Memories

// Remove specific memory
npc.setMemory('temporaryBuff', null);  // or empty string

// Clear all memories (keeps character design)
npc.clearMemories();

Get Memories

// Get specific memory
const playerName = npc.getMemory('playerName');

// Get all memory names
const memoryNames = npc.getMemoryNames();
console.log('Stored memories:', memoryNames);
// ['playerName', 'playerClass', 'relationship', ...]

Actions (Tool Calling)

NPCs can trigger game actions based on conversation context.

Define Actions

const actions = [
  {
    actionName: 'give_item',
    description: 'Give an item to the player as a reward or gift',
    parameters: [
      {
        name: 'itemName',
        type: 'string',
        description: 'Name of the item to give',
        required: true
      },
      {
        name: 'quantity',
        type: 'number',
        description: 'How many to give',
        required: false
      }
    ]
  },
  {
    actionName: 'start_quest',
    description: 'Offer a new quest to the player',
    parameters: [
      {
        name: 'questId',
        type: 'string',
        description: 'The quest identifier',
        required: true
      }
    ]
  },
  {
    actionName: 'change_attitude',
    description: 'Change NPC attitude toward player',
    parameters: [
      {
        name: 'attitude',
        type: 'stringEnum',
        description: 'New attitude level',
        enumOptions: ['hostile', 'neutral', 'friendly', 'ally']
      }
    ]
  }
];

Talk with Actions

const response = await npc.talkWithActions(
  'I completed your quest! The dragon is defeated.',
  actions
);

console.log('NPC says:', response.text);

if (response.hasActions) {
  for (const action of response.actionCalls) {
    console.log('Action:', action.actionName);
    console.log('Arguments:', action.arguments);

    // Execute in your game
    switch (action.actionName) {
      case 'give_item':
        givePlayerItem(action.arguments.itemName, action.arguments.quantity);
        break;
      case 'start_quest':
        startQuest(action.arguments.questId);
        break;
      case 'change_attitude':
        updateNpcAttitude(action.arguments.attitude);
        break;
    }
  }
}

Streaming with Actions

await npc.talkWithActionsStream(
  'Can you help me?',
  actions,
  (chunk) => {
    displayText(chunk);
  },
  (response) => {
    if (response.hasActions) {
      handleActions(response.actionCalls);
    }
  }
);

Report Action Results

For multi-turn interactions where the NPC needs to know action outcomes:

const response = await npc.talkWithActions('Open the chest', actions);

// Execute actions and report results back
for (const action of response.actionCalls) {
  const result = executeAction(action);
  npc.reportActionResult(action.id, result);
}

// Or report multiple at once
npc.reportActionResults({
  'call_abc123': 'Chest opened successfully. Found 50 gold.',
  'call_def456': 'Player received the golden key.'
});

Reply Predictions

Automatically generate suggested responses the player might say next.

Enable Auto-Prediction

const npc = sdk.createNPCClient({
  characterDesign: 'You are a quest giver.',
  generateReplyPrediction: true,
  predictionCount: 4  // Generate 4 suggestions
});

// Listen for predictions
npc.on('replyPredictions', (predictions) => {
  console.log('Suggested replies:', predictions);
  displayReplyOptions(predictions);
});

await npc.talk('Welcome, adventurer! I have a dangerous mission.');

Manual Prediction

// Generate predictions on demand (first arg is optional tempPrompt, second is count)
const predictions = await npc.generateReplyPredictions(undefined, 4);
console.log(predictions);
// [
//   "What kind of mission?",
//   "I'm ready for anything!",
//   "What's the reward?",
//   "Sounds too dangerous for me."
// ]

Configure Predictions

// Enable/disable
npc.setGenerateReplyPrediction(true);

// Change count (2-6)
npc.setPredictionCount(3);

History Management

View History

// Get full history
const history = npc.getHistory();
console.log(`${history.length} messages in conversation`);

// Get history length
const length = npc.getHistoryLength();

Clear History

// Clear conversation (keeps character design and memories)
npc.clearHistory();

Revert Messages

// Undo last user/assistant exchange
const reverted = npc.revertHistory();
if (reverted) {
  console.log('Last exchange removed');
}

// Remove specific number of messages
const removed = npc.revertChatMessages(4);
console.log(`Removed ${removed} messages`);

Append Messages

// Manually add to history
npc.appendMessage({
  role: 'user',
  content: 'This is injected context'
});

// Shorthand
npc.appendChatMessage('assistant', 'Previous response to remember');

Save and Load

Persist conversations across game sessions.

Save Conversation

// Serialize to JSON string
const saveData = npc.saveHistory();

// Store in your game's save system
localStorage.setItem('npc_wizard_conversation', saveData);
// Or: saveToDatabase(playerId, 'wizard_npc', saveData);

Load Conversation

// Retrieve saved data
const saveData = localStorage.getItem('npc_wizard_conversation');

if (saveData) {
  const success = npc.loadHistory(saveData);
  if (success) {
    console.log('Conversation restored!');
    // NPC remembers previous conversation
  }
}

Save Data Structure

The saved data includes:

  • Character design
  • All memories
  • Conversation history
interface ConversationSaveData {
  characterDesign: string;
  memories: Array<{ name: string; content: string }>;
  history: Message[];
}

Events

NPCClient extends EventEmitter for reactive programming.

// Response events
npc.on('response', (text) => {
  console.log('NPC responded:', text);
});

npc.on('actions', (actionCalls) => {
  console.log('NPC triggered actions:', actionCalls);
});

npc.on('replyPredictions', (predictions) => {
  updateUI(predictions);
});

// Memory events
npc.on('memory_set', (name, content) => {
  console.log(`Memory "${name}" set to:`, content);
});

npc.on('memory_removed', (name) => {
  console.log(`Memory "${name}" removed`);
});

npc.on('memories_cleared', () => {
  console.log('All memories cleared');
});

// History events
npc.on('history_cleared', () => {
  console.log('Conversation reset');
});

npc.on('history_reverted', () => {
  console.log('History reverted');
});

npc.on('history_loaded', () => {
  console.log('Previous conversation loaded');
});

Configuration Reference

NPCConfig Options

OptionTypeDefaultDescription
characterDesignstring-NPC personality and behavior prompt
modelstring-AI model to use
temperaturenumber0.7Response randomness (0.0-2.0)
maxHistoryLengthnumber50Maximum messages to keep
generateReplyPredictionbooleanfalseAuto-generate reply suggestions
predictionCountnumber4Number of predictions (2-6)
fastModelstring-Faster model for predictions

Action Parameter Types

TypeDescription
stringFree text input
numberNumeric value
booleanTrue/false
stringEnumOne of specified options

Best Practices

1. Design Rich Characters

// Good: detailed character with clear personality
const npc = sdk.createNPCClient({
  characterDesign: `You are Greta, the blacksmith.
Background: Former adventurer, retired after losing her arm to a dragon.
Personality: Gruff but kind-hearted. Respects strength and courage.
Speech: Direct, uses forge metaphors. Calls everyone "spark".
Secret: She knows the location of the legendary Dragonbane sword.`
});

// Bad: vague character
const npc = sdk.createNPCClient({
  characterDesign: 'You are a blacksmith.'
});

2. Use Memories for Dynamic Context

// Good: update memories as game state changes
npc.setMemory('playerReputation', 'Hero of the village');
npc.setMemory('lastInteraction', 'Player bought a sword yesterday');
npc.setMemory('shopInventory', 'Low on iron, has rare mithril');

// Bad: putting everything in character design

3. Limit History for Performance

// Good: reasonable history limit
const npc = sdk.createNPCClient({
  characterDesign: '...',
  maxHistoryLength: 20  // Enough context, good performance
});

// Bad: unlimited history
const npc = sdk.createNPCClient({
  characterDesign: '...',
  maxHistoryLength: 1000  // Too much, slow and expensive
});

4. Handle Actions Properly

// Good: validate and execute actions safely
if (response.hasActions) {
  for (const action of response.actionCalls) {
    if (canExecuteAction(action.actionName)) {
      const result = executeAction(action);
      npc.reportActionResult(action.id, result);
    } else {
      npc.reportActionResult(action.id, 'Action not available');
    }
  }
}

5. Save Conversations at Key Points

// Good: save at natural break points
async function endConversation() {
  const saveData = npc.saveHistory();
  await saveToCloud(playerId, npcId, saveData);
}

// Save when player leaves area, game saves, etc.

Complete Example

import { PlayKitSDK } from 'playkit-sdk';

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  apiKey: 'your-api-key'
});

await sdk.initialize();

// Create NPC
const merchant = sdk.createNPCClient({
  characterDesign: `You are Marcus, a traveling merchant.
You sell potions, scrolls, and curiosities from distant lands.
You're friendly but always looking for a good deal.
You have rumors about nearby dungeons to share with good customers.`,
  temperature: 0.8,
  generateReplyPrediction: true,
  predictionCount: 3
});

// Set up memories
merchant.setMemory('playerName', 'Hero');
merchant.setMemory('playerGold', '500');
merchant.setMemory('previousPurchases', 'Health potion x2');

// Define shop actions
const shopActions = [
  {
    actionName: 'sell_item',
    description: 'Sell an item to the player',
    parameters: [
      { name: 'item', type: 'string', required: true },
      { name: 'price', type: 'number', required: true }
    ]
  },
  {
    actionName: 'share_rumor',
    description: 'Share information about nearby locations',
    parameters: [
      { name: 'location', type: 'string', required: true },
      { name: 'details', type: 'string', required: true }
    ]
  }
];

// Listen for events
merchant.on('replyPredictions', (predictions) => {
  displayDialogueOptions(predictions);
});

// Have conversation
const greeting = await merchant.talk('Hello there!');
displayNpcText(greeting);

// Player wants to buy something
const response = await merchant.talkWithActions(
  'Do you have any healing potions?',
  shopActions
);

displayNpcText(response.text);

if (response.hasActions) {
  for (const action of response.actionCalls) {
    if (action.actionName === 'sell_item') {
      showPurchaseDialog(action.arguments.item, action.arguments.price);
    }
  }
}

// Save for later
const saveData = merchant.saveHistory();
localStorage.setItem('merchant_conversation', saveData);

Next Steps