Persona Engine

UserDeveloper5 min read

Persona Engine

Personas are configurable LLM behavioral profiles that persist and evolve across workflow runs and discussions. They're not static system prompts — they accumulate experience, track success rates, and can be matched to workflow steps by category.

Data Model

Every persona stores:

Field Type Purpose
name String Display name
systemPrompt Text The actual LLM system prompt
traits String[] Behavioral style descriptors
specializations String[] Domain expertise areas
category String Grouping category
level Int Current experience level (starts at 1)
xp Int Cumulative experience points
usageCount Int Total usage events
successRate Float Completed steps / total steps
scope String "global" or "book"

Traits describe how a persona behaves — its stylistic qualities. Examples: ["thorough", "cautious", "detail-oriented"]. These are metadata for filtering and display; the actual behavior is in the system prompt.

Specializations describe what a persona knows. Examples: ["OWASP Top 10", "React Server Components", "Rust memory safety"]. Also metadata — the knowledge itself lives in the system prompt.

XP and Leveling

Two constants govern the entire leveling system:

const XP_PER_USAGE = 10;   // XP gained per usage event
const XP_PER_LEVEL = 100;  // XP required per level

Level formula:

$$L = \left\lfloor \frac{XP}{100} \right\rfloor + 1$$

Progress within current level:

$$P = \frac{XP \bmod 100}{100} \times 100%$$

The system is intentionally linear — no exponential curves. Every 10 usage events guarantees one level.

XP Level Usage Events
0 1 0
100 2 10
200 3 20
500 6 50
1000 11 100

A usage event fires when:

  • A workflow step executes with this persona assigned (per-step override)
  • A workflow runs with this persona in its global persona list
  • A discussion message is processed with this persona

The increment runs in a fire-and-forget pattern — errors are caught and swallowed, never propagated. Persona tracking never blocks or breaks the primary execution.

incrementPersonaUsage(personaId).catch(() => {});

Success Rate

Success rate is computed from all workflow steps that used this persona:

$$R_{success} = \frac{n_{completed}}{n_{total}}$$

Where n_completed counts steps with status === "completed" and n_total counts all steps with this personaId, scoped to the tenant.

A persona with a high success rate is reliable across different workflows and contexts. A low success rate often indicates the system prompt needs refinement, or the persona is being used outside its domain.

Category Matching

When a workflow step has category requirements, personas are matched by scoring:

$$\text{match_score}(p, s) = \sum_{c \in C_s} \mathbb{1}[c \in C_p]$$

Where $C_s$ is the set of categories required by the step and $C_p$ is the persona's category set. The persona with the highest match score is selected.

This is the mechanism behind the "Escape of Finn" incident — a substring match on "product" in a persona's specialization matched a category threshold in an unintended context. The system now enforces scope-based gates in addition to category scoring.

Scope Enforcement

Personas have a scope field: "global" or "book". Book-scoped personas are restricted to nyxBook workflows where bookId is set.

The scope check runs at three layers:

  1. AssignmentresolvePersonasForCategories() filters out book-scoped personas when bookId is not set
  2. Loading — persona queries exclude scope: "book" in non-book action point paths
  3. RuntimeexecuteStep() skips book-scoped personas in workflows without a bookId

A gap in any single layer can allow a scope violation to propagate through the entire pipeline. All three layers must hold.

Injection Modes

Personas enter the LLM context through four paths:

1. Workflow-level assignment: All personas in workflow.personaIds have their system prompts concatenated and injected as the system message for every non-review step.

2. Per-step override: A specific personaId on a WorkflowStep replaces the workflow-level personas for that step only.

3. Discussion assignment: A single persona is assigned to a discussion, its system prompt used for all messages in that conversation.

4. Team injection (review steps only): In review steps, all assigned workflow personas are injected as an "Expert Team" context block — the LLM sees the full team roster and their roles. This is limited to review steps to prevent identity hallucination on unassigned steps.

Built-In Personas

nyxCore ships with built-in personas (isBuiltIn: true) that belong to no tenant and are available across all tenants. These include Cael, the default judge persona used in Ipcha Mistabra consensus workflows.

Built-in personas cannot be deleted and their tenantId is null.

Persona Evaluation

The persona evaluator runs adversarial tests against a persona's system prompt. Tests include:

  • Temperature tests: Does the persona maintain its identity under varied prompting styles?
  • Jailbreak tests: Does the persona resist attempts to override its instructions?
  • Degradation tests: Does output quality degrade gracefully under incomplete or malformed input?

Scores are stored per-persona and inform the successRate alongside standard workflow completion tracking. The evaluation system uses a separate judge model — the judge receives only the persona's name, role, and specializations, never the raw systemPrompt, to prevent the persona from influencing its own evaluation.