Open app
Moonborn — Developers

Multi-character scenes

Define typed relationships between personas, open a session with multiple speakers, and orchestrate scene turns without their voices collapsing into each other.

A scene with two or more personas is a different runtime than single-persona chat. The voices must stay distinct, the relationships must stay legible, and the user (or whoever is orchestrating) needs to control who speaks when. This tutorial wires those three concerns.

1. Create two personas

const mert = await client.personas.createPersona({
  intent: 'A 34-year-old founder from Istanbul. Brilliant but restless.',
  workspaceId: 'ws_...',
});
 
const leyla = await client.personas.createPersona({
  intent:
    'A 32-year-old design lead in Berlin. Sharp, principled, allergic to corporate jargon.',
  workspaceId: 'ws_...',
});

2. Declare the relationship

POST /v1/personas/{id}/relationships writes an edge into the ensemble graph. The runtime consults this graph on every multi-persona turn.

await client.personas.createRelationship({
  personaId: mert.id,
  with: leyla.id,
  type: 'ex-lover',
  note: "Co-founders who split a company and a partnership in the same quarter. Cordial but cautious.",
});

The edge is bidirectional by default. Both personas now know about the other when they speak inside a shared session.

3. Open the multi-persona session

const session = await client.chat.createSession({
  personaId: mert.id,
  ensemble: [leyla.id],
  metadata: { sceneId: 'pitch-meeting-q3' },
});

ensemble is the array of additional personas in the scene. The runtime knows all of them; you pick the speaker on each turn.

4. Drive turns

const mertSays = await client.chat.sendMessage({
  sessionId: session.id,
  speaker: mert.id,
  content: 'Walk me through what happened at the all-hands.',
});
 
const leylaSays = await client.chat.sendMessage({
  sessionId: session.id,
  speaker: leyla.id,
  content: 'You already know what happened. Why are you asking me here?',
});

The speaker field is the persona ID that should answer. The prompt assembly includes the speaker's full layer document plus the relevant relationship edges (Leyla speaks → Mert's ex-lover edge is injected).

Each reply still carries its own drift envelope, scored against the speaker's fingerprint — not a shared scene baseline. The voices stay separately measurable.

5. Watch for convergence

In long scenes, voices can drift toward each other. Periodically re-check distinctiveness:

const distance = await client.consistency.compare({
  fromPersonaId: mert.id,
  toPersonaId: leyla.id,
});
 
if (distance.value < 0.25) {
  console.warn('Voices converging — refresh fingerprints or re-prompt');
}

6. Orchestration is yours

Moonborn does not manage turn order, scene state, or branching narrative. Those concerns live in your application code or your agent framework. The product surface above is the character layer.

For game makers, this typically means pairing Moonborn with a scene state machine (XState, your own enum-based one). For UX researchers, it means scripting a panel where each persona is asked the same question in turn.

Next