Persona
Client for creating and interacting with emotion-aware AI personas.
MolrooPersona
MolrooPersona is the SDK class for interacting with the molroo emotion engine. Each instance represents a single AI character with persistent emotional state.
Quick start
The recommended way to create a persona is via the unified Molroo client:
import { Molroo } from '@molroo-io/sdk';
import { createOpenAI } from '@ai-sdk/openai';
const molroo = new Molroo({ apiKey: 'mk_live_xxx' });
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const persona = await molroo.createPersona(
{
identity: { name: 'Sera', role: 'barista', speakingStyle: 'warm and casual' },
personality: { O: 0.7, C: 0.6, E: 0.8, A: 0.9, N: 0.3, H: 0.8 },
},
{ llm: openai('gpt-4o-mini') },
);
// Chat with external history management
let history: Message[] = [];
const result = await persona.chat('Good morning! What do you recommend?', { history });
console.log(result.text); // LLM-generated response
console.log(result.response.emotion); // { V: 0.65, A: 0.42, D: 0.38 }
history = result.updatedHistory; // Save for next callProperties
| Property | Type | Description |
|---|---|---|
id | string | Unique persona instance ID |
personaId | string | Alias for id |
Static Factory Methods
MolrooPersona.create(config, input)
Create a new persona on the API and return a connected instance. Accepts either a natural language description (requires llm) or explicit PersonaConfigData.
// From description (requires llm)
const persona = await MolrooPersona.create(
{ apiKey: 'mk_live_xxx', llm: openai('gpt-4o-mini') },
'A kind and curious barista who remembers customer names',
);
// From explicit config
const persona = await MolrooPersona.create(
{ apiKey: 'mk_live_xxx', llm: openai('gpt-4o-mini') },
{
identity: { name: 'Sera', role: 'barista' },
personality: { O: 0.7, C: 0.6, E: 0.8, A: 0.9, N: 0.3, H: 0.8 },
},
);| Parameter | Type | Description |
|---|---|---|
config.baseUrl | string? | API base URL. Default: https://api.molroo.io |
config.apiKey | string | API key |
config.llm | LLMInput | LLM adapter or Vercel AI SDK provider for chat(). Required when using description string. |
config.engineLlm | LLMInput | Optional separate LLM for appraisal (split mode) |
config.memory | MemoryAdapter | Optional memory adapter for advanced features |
config.recall | RecallLimits | Optional recall limits when using a MemoryAdapter |
config.events | EventAdapter | Optional event emission adapter |
input | string | PersonaConfigData | Natural language description or explicit persona config |
Returns: Promise<MolrooPersona>
MolrooPersona.connect(config, personaId)
Connect to an existing persona by ID. Verifies the persona exists.
const persona = await MolrooPersona.connect(
{ apiKey: 'mk_live_xxx', llm: openai('gpt-4o-mini') },
'persona_abc123',
);Returns: Promise<MolrooPersona>
MolrooPersona.listPersonas(config)
List all personas for the authenticated tenant.
const { personas } = await MolrooPersona.listPersonas({
apiKey: 'mk_live_xxx',
});Returns: Promise<{ personas: PersonaSummary[]; nextCursor?: string }>
Input — react()
persona.react(input, options?)
Unified input method introduced in v0.10.0. Anything the persona perceives from the outside world goes through react(). The input argument is a ReactInput discriminated union on kind.
type ReactInput =
| { kind: 'perceive'; message: string; [key: string]: unknown }
| { kind: 'hear'; message: string; from: string; [key: string]: unknown }
| { kind: 'experience'; description: string; appraisal: AppraisalVector; [key: string]: unknown }
| { kind: 'event'; type: string; description: string; appraisal: AppraisalVector; [key: string]: unknown }
| { kind: 'socialize'; message: string; from: string; relationship: { trust: number; familiarity: number }; [key: string]: unknown }TypeScript will refuse mismatched fields (e.g. relationship on a hear input).
Returns: Promise<AgentResponse>
perceive — generic stimulus
const response = await persona.react({
kind: 'perceive',
message: 'You did a great job!',
});
console.log(response.emotion); // { V: 0.7, A: 0.5, D: 0.3 }hear — message from a named speaker
const response = await persona.react({
kind: 'hear',
message: 'Good morning! What do you recommend today?',
from: 'Alice',
});experience — internal or sensory experience with explicit appraisal
const response = await persona.react({
kind: 'experience',
description: 'A beautiful sunset over the ocean.',
appraisal: {
goal_relevance: 0.3,
goal_congruence: 0.8,
expectedness: 0.4,
controllability: 0.0,
agency: -1,
norm_compatibility: 1,
internal_standards: 0.9,
adjustment_potential: 0.8,
urgency: 0.0,
},
});event — typed external event with explicit appraisal
const response = await persona.react({
kind: 'event',
type: 'gift_received',
description: 'A customer left flowers on the counter.',
appraisal: {
goal_relevance: 0.6,
goal_congruence: 0.9,
expectedness: 0.2,
controllability: 0.0,
agency: -1,
norm_compatibility: 1,
internal_standards: 0.8,
adjustment_potential: 0.9,
urgency: 0.0,
},
});socialize — message with relationship context
const response = await persona.react({
kind: 'socialize',
message: 'I missed you while I was away.',
from: 'Bob',
relationship: { trust: 0.8, familiarity: 0.9 },
});Admin — update()
persona.update(patch)
Unified admin patch method introduced in v0.10.0. Write any combination of persona state fields in a single call. Fields are applied in declaration order; unspecified fields are left unchanged. update({}) is a no-op.
interface PersonaUpdate {
emotion?: Partial<{ V: number; A: number; D: number }>;
annotation?: string;
nextResponse?: string;
styleProfile?: StyleProfile;
config?: PersonaConfigData;
snapshot?: PersonaSnapshot;
}| Field | Equivalent legacy method | Description |
|---|---|---|
emotion | setEmotion() | Override VAD emotion state |
annotation | annotate() | Free-text annotation written into persona state |
nextResponse | setNextResponse() | Force the persona's next reply text |
styleProfile | setStyleProfile() | Replace the speaking style profile |
config | patch() | Update identity / personality config |
snapshot | putSnapshot() | Restore from a saved snapshot |
// Set emotion and prime a response in one round-trip
await persona.update({
emotion: { V: 0.5, A: 0.3 },
nextResponse: 'Good morning! The usual?',
});
// Full state reset from snapshot with updated config
await persona.update({
snapshot: savedSnapshot,
config: {
identity: { name: 'Sera', role: 'head barista' },
},
});Returns: Promise<void>
Chat Methods
persona.chat(message, options?)
High-level chat with LLM integration. The SDK:
- Calls
getPromptContext()to get the server-assembled system prompt - Optionally recalls memories from MemoryAdapter (if configured)
- Sends to the LLM for text generation + appraisal
- Sends the appraisal to the API via
perceive()for emotion computation - Returns
PersonaChatResultwithupdatedHistory
Requires an LLM. Without one, use perceive() directly.
let history: Message[] = [];
const result = await persona.chat('How are you feeling today?', {
from: 'Alex',
history,
});
console.log(result.text); // LLM-generated text
console.log(result.response.emotion); // { V: 0.6, A: 0.4, D: 0.3 }
console.log(result.state?.mood); // slow-moving baseline mood
console.log(result.updatedHistory); // updated conversation history
// Save history for next call
history = result.updatedHistory;| Parameter | Type | Description |
|---|---|---|
message | string | User message |
options.from | string | InterlocutorContext | Source entity -- a name string or structured context with description/extensions injected into the system prompt. Default: 'user' |
options.history | Message[]? | Conversation history for LLM context. Manage externally using updatedHistory. |
options.consumerSuffix | string? | Extra text appended to the system prompt. Use for app-specific context only — see consumerSuffix guidelines. |
options.onToolCall | function? | Callback invoked when the LLM requests a tool call during generation |
Returns: Promise<PersonaChatResult>
interface PersonaChatResult {
/** LLM-generated response text. */
text: string;
/** Emotion engine response with VAD, discrete emotion, and side effects. */
response: AgentResponse;
/** Persona state at the time of interaction (if available). */
state?: PersonaState;
/** Updated conversation history including this turn. Manage externally. */
updatedHistory: Message[];
}See PersonaChatResult and ChatOptions for full type details.
consumerSuffix guidelines
The server-assembled system prompt (via getPromptContext()) already includes identity, personality, emotion state, mood, and StyleProfile constraints. Do not duplicate these in consumerSuffix.
Do include (app-specific context the SDK doesn't know about):
- Relationship details between user and persona
- Example messages for tone matching
- Time gap context ("returning after 3 hours")
- Game/app state (inventory, quest progress, scene description)
- Custom behavioral rules specific to your app
Do not include (already in the server prompt):
- Persona name, role, or speaking style (from
identity) - Personality trait descriptions (from
personality) - Current emotion or mood state (from live engine state)
- StyleProfile constraints (from
extractStyleProfile())
Duplicating server prompt content wastes tokens and can confuse the LLM with contradictory instructions.
Combined vs. Split mode
In combined mode (default), a single LLM call returns both response text and appraisal. The emotion is computed after the response.
In split mode (when engineLlm is provided), the appraisal is generated first by engineLlm, the engine updates the emotion, then llm generates the response with the updated emotional state. This means the response reflects the emotion after processing the current message.
Interlocutor context
Pass structured information about the conversation partner via from:
const result = await persona.chat('Hello traveler!', {
from: {
name: 'Kim',
description: 'A weary traveler who just arrived at the tavern.',
extensions: {
inventory: 'sword, healing potion',
quest: 'Find the lost artifact of Eldoria',
},
},
history,
});
history = result.updatedHistory;When from is an InterlocutorContext object, the SDK automatically injects the description and extensions into the system prompt. The name field is used as the source entity for memory recall and emotion processing.
Time Methods
persona.tick(seconds)
Advance persona time. Triggers mood decay, body budget recovery, and internal processing.
await persona.tick(3600); // Advance 1 hourReturns: Promise<{ pendingEvents?: unknown[] }>
State Methods
persona.getState()
Get the current emotional and psychological state.
const state = await persona.getState();
console.log(state.emotion); // { V: 0.3, A: 0.2, D: 0.1 }
console.log(state.mood); // slow-changing mood baseline
console.log(state.somatic); // somatic marker descriptionsReturns: Promise<PersonaState>
persona.getPromptContext(suffix?, source?)
Get the server-assembled system prompt built from the persona's live emotional state. Used internally by chat(), but available for custom LLM integrations.
const ctx = await persona.getPromptContext('Be extra helpful today.', 'Alex');
console.log(ctx.systemPrompt); // Full system prompt with emotion context
console.log(ctx.personaPrompt); // Raw persona prompt data
console.log(ctx.tools); // Available tools (if configured)| Parameter | Type | Description |
|---|---|---|
suffix | string? | Extra text appended to the system prompt (consumer suffix) |
source | string? | Source entity name (used for relationship-aware prompt context) |
Returns: Promise<{ systemPrompt: string; personaPrompt: Record<string, unknown>; tools?: Array<Record<string, unknown>> }>
System prompt structure
The server assembles the system prompt from the persona's live state. Understanding this structure helps you avoid duplicating content in consumerSuffix.
┌─────────────────────────────────────────────────┐
│ Identity │
│ Name, role, core values, speaking style, │
│ description, extensions │
├─────────────────────────────────────────────────┤
│ Behavioral Instructions │
│ "Stay in character", "Embody your │
│ psychological state", language matching │
├─────────────────────────────────────────────────┤
│ ## Current Psychological State │
│ • Emotion (VAD → natural language) always │
│ • Mood (slow-changing baseline) if set │
│ • Somatic markers (body sensations) if set │
│ • Goals & motivations if set │
│ • Narrative arc & self-perception if set │
│ • Memory context (past interactions) if source│
├─────────────────────────────────────────────────┤
│ ## Expression Style if StyleProfile │
│ • Message length constraints │
│ • Formality level │
│ • Emoji/laugh/punctuation guidance │
│ • Signature expressions & sentence endings │
│ • Forbidden patterns │
│ • Emotion-driven modulation reasons │
├─────────────────────────────────────────────────┤
│ Consumer Suffix if provided │
│ (your app-specific context goes here) │
└─────────────────────────────────────────────────┘Sections marked "if set" are included only when the corresponding state is non-neutral. This keeps the prompt focused and reduces token usage.
persona.getSnapshot()
Get a full snapshot of the persona's internal state for backup.
const snapshot = await persona.getSnapshot();Returns: Promise<PersonaSnapshot>
Style Methods
persona.extractStyleProfile(corpus, options?)
Extract a speaking style profile from sample text and save it to the persona. Extraction runs server-side. Once set, style constraints are automatically included in the system prompt and modulated by the persona's current emotion.
const profile = await persona.extractStyleProfile([
'omg that latte art is gorgeous!!',
'haha yeah i always double-shot my espressos~',
// ... 20-30 messages recommended
]);| Parameter | Type | Description |
|---|---|---|
corpus | string[] | Array of sample messages from the target speaking style |
options.timestamps | number[]? | Message timestamps for temporal analysis |
options.otherMessages | string[]? | Other people's messages for contrast analysis |
Returns: Promise<StyleProfile> — the extracted profile (also auto-saved to the persona)
See the Speaking Style guide for full details.
Lifecycle Methods
persona.destroy()
Soft-delete this persona. Can be restored with restore().
await persona.destroy();persona.restore()
Restore a previously soft-deleted persona.
await persona.restore();PersonaConfigData
Configuration data for creating or updating a persona. See Configuration Types for full details including Identity, PersonalityTraits, and Goal types.
interface PersonaConfigData {
personality?: PersonalityTraits; // 6-factor: { O, C, E, A, N, H }
identity?: Identity; // name, role, speakingStyle, coreValues
goals?: Goal[]; // persona goals and motivations
[key: string]: unknown; // extensible
}Personality factors:
| Key | Factor | Range |
|---|---|---|
O | Openness to Experience | 0 - 1 |
C | Conscientiousness | 0 - 1 |
E | Extraversion | 0 - 1 |
A | Agreeableness | 0 - 1 |
N | Neuroticism | 0 - 1 |
H | Honesty-Humility | 0 - 1 |
Legacy methods (removed in v1.0)
The following methods were available in v0.9.x and still work in v0.10.x, but will be removed in v1.0. Migrate to react() and update() at your convenience.
See the v1.0 migration guide for a side-by-side mapping.
persona.perceive(message, options?)
Low-level emotion processing without LLM. Send a message (or appraisal) directly to the emotion engine.
const response = await persona.perceive('You did a great job!', {
from: 'Alex',
appraisal: {
goal_relevance: 0.8,
goal_congruence: 0.9,
expectedness: 0.3,
controllability: 0.2,
agency: -0.7,
norm_compatibility: 0.9,
internal_standards: 0.8,
adjustment_potential: 0.7,
urgency: 0.1,
},
});
console.log(response.emotion); // { V: 0.7, A: 0.5, D: 0.3 }| Parameter | Type | Default | Description |
|---|---|---|---|
message | string | -- | Stimulus message |
options.from | string | InterlocutorContext | 'user' | Source entity -- a name string or structured InterlocutorContext |
options.appraisal | AppraisalVector | -- | Pre-computed appraisal vector for emotion computation |
options.type | string? | 'chat_message' | Event type identifier (e.g., 'news_event', 'environment_change') |
options.stimulus | object? | -- | Low-level state overrides. See stimulus. |
options.payload | Record<string, unknown>? | -- | Extra event data merged into the event payload alongside the message |
options.priorEpisodes | Episode[] | -- | Context memories for appraisal-aware processing |
options.relationshipContext | { trust, familiarity }? | -- | Relationship info between source entity and persona for appraisal bias |
options.skipMemory | boolean? | false | Skip saving the generated memoryEpisode to the memory adapter |
Returns: Promise<AgentResponse>
See PerceiveOptions and AgentResponse for full type details.
Migrate to: react({ kind: 'perceive', message, ... })
persona.hear(message, from, options?)
Convenience wrapper around perceive() for messages from a named speaker.
await persona.hear('Good morning!', 'Alice');Migrate to: react({ kind: 'hear', message, from, ... })
persona.experience(description, appraisal, options?)
Send an internal or sensory experience with an explicit appraisal vector.
await persona.experience('A beautiful sunset.', {
goal_relevance: 0.3,
goal_congruence: 0.8,
expectedness: 0.4,
controllability: 0.0,
agency: -1,
norm_compatibility: 1,
internal_standards: 0.9,
adjustment_potential: 0.8,
urgency: 0.0,
});Migrate to: react({ kind: 'experience', description, appraisal, ... })
persona.event(type, description, options)
Convenience wrapper around perceive() for dispatching typed events. Unlike perceive(), the event type is the first argument and appraisal is required.
await persona.event('breaking_news', 'A major earthquake struck the region.', {
appraisal: {
goal_relevance: 0.7,
goal_congruence: -0.6,
expectedness: 0.1,
controllability: 0.0,
agency: -1,
norm_compatibility: 0,
internal_standards: 0,
adjustment_potential: 0.2,
urgency: 0.9,
},
});| Parameter | Type | Description |
|---|---|---|
type | string | Event type name (e.g., 'breaking_news', 'weather_change', 'gift_received') |
description | string | Event description (passed as the stimulus message to perceive()) |
options | PerceiveOptions & { appraisal: AppraisalVector } | Same as PerceiveOptions, but appraisal is required |
Returns: Promise<AgentResponse>
Migrate to: react({ kind: 'event', type, description, appraisal, ... })
persona.socialize(message, from, relationship, options?)
Send a message with explicit relationship context.
await persona.socialize('I missed you.', 'Bob', { trust: 0.8, familiarity: 0.9 });Migrate to: react({ kind: 'socialize', message, from, relationship, ... })
persona.setEmotion(vad)
Directly override the persona's emotion state in VAD space.
await persona.setEmotion({ V: -0.5, A: 0.8 }); // Set to anxious state| Parameter | Type | Description |
|---|---|---|
vad.V | number? | Valence (-1 to +1) |
vad.A | number? | Arousal (0 to 1) |
vad.D | number? | Dominance (-1 to +1) |
Migrate to: update({ emotion: { V, A, D } })
persona.annotate(text)
Write a free-text annotation into persona state.
await persona.annotate('is cheerful today');Migrate to: update({ annotation: 'is cheerful today' })
persona.setNextResponse(text)
Force the persona's next reply text.
await persona.setNextResponse('OK!');Migrate to: update({ nextResponse: 'OK!' })
persona.setStyleProfile(profile)
Manually set a previously extracted StyleProfile. Useful when reusing a profile across multiple personas.
await persona.setStyleProfile(profile);| Parameter | Type | Description |
|---|---|---|
profile | StyleProfile | A StyleProfile object |
Migrate to: update({ styleProfile: profile })
persona.patch(updates)
Update the persona's configuration (identity, personality).
await persona.patch({
config: {
identity: { name: 'Sera', role: 'head barista' },
personality: { O: 0.7, C: 0.7, E: 0.8, A: 0.9, N: 0.3, H: 0.8 },
},
});Migrate to: update({ config: { identity, personality } })
persona.putSnapshot(snapshot)
Restore the persona's state from a previously saved snapshot.
await persona.putSnapshot(savedSnapshot);Migrate to: update({ snapshot })