Persona Engine
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:
- Assignment —
resolvePersonasForCategories()filters out book-scoped personas whenbookIdis not set - Loading — persona queries exclude
scope: "book"in non-book action point paths - Runtime —
executeStep()skips book-scoped personas in workflows without abookId
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.
