Private
Public Access
0
0
Files
manual_slop/docs/guide_personas.md
T
conductor-tier2 161ebb0da6 docs(fix): correct nav link case + relative-path level
Gitea (and any case-sensitive filesystem) was rendering the [Top]
nav links in /docs as broken because of two bugs:

1. Case-sensitivity: 22 links used '../README.md' (all-uppercase)
   but the actual file is 'docs/Readme.md' (capital R, lowercase
   rest). 21 guide_*.md nav bars were affected, plus 1 internal
   cross-link in Readme.md itself. Works on Windows (case-
   insensitive) but broken on Linux/Gitea.

   Fix: 22 occurrences across 22 files changed
   '../README.md' -> '../Readme.md'

2. Wrong relative-path level: 16 links used '../../conductor/...'
   from 'docs/guide_*.md' to reach 'conductor/'. This goes up 2
   levels to 'projects/', which doesn't exist. The correct path
   from 'docs/guide_*.md' to 'conductor/' is 1 level up
   ('../conductor/...'). 12 unique patterns across 10 files
   affected.

   Fix: 16 occurrences across 10 files changed
   '../../conductor/' -> '../conductor/'

3. Bonus: 1 planned-guide link in guide_context_curation.md
   referenced a never-written 'guide_context_presets.md'. The
   ContextPreset schema is now fully covered in the new
   'guide_context_aggregation.md' (per the 2026-06-08 docs
   refresh). Fix: link target updated.

No content was changed, only link paths. 24 files, 37 link
replacements, 37 deletions.

Verification:
- All .md links in docs/ now resolve to existing files
  (validated by path-resolution check from each file's directory)
- The 3 new guides from the previous docs refresh commit
  (guide_discussions.md, guide_state_lifecycle.md,
  guide_context_aggregation.md) had the case bug inherited from
  guide_architecture.md's existing nav pattern; their top-of-file
  nav bars are now correct
- The 21 pre-existing guide nav bars that had the same bug
  (all 21 of them, except the 3 that used the correct case:
  guide_mma.md, guide_simulations.md, guide_tools.md) are now
  also fixed
- Inter-guide links (e.g. [Discussions](guide_discussions.md))
  were not affected; they were always correct because both the
  link text and the actual filename are lowercase

This is a docs-only fix. No code modified.
2026-06-08 19:51:55 -04:00

12 KiB

Personas (Unified Agent Profiles)

Top | MMA | Tools & IPC | Architecture


Overview

A Persona is a unified agent profile that consolidates the model choice, system prompt, tool preset, bias profile, context preset, and aggregation strategy into a single named, reusable entity. Personas eliminate the need to configure these settings individually per session or per Tier — the developer names a persona, and the entire configuration is applied atomically.

Personas are persisted in TOML files with scope-based inheritance (Global vs Project). The PersonaManager merges both scopes at load time, with project-level entries overriding global entries of the same name.

This guide covers:

  1. Data Model — The Persona schema and its fields
  2. Storage & Scope — Global and project TOML files, merge rules
  3. PersonaManager — CRUD operations
  4. MMA Integration — How personas are applied to workers
  5. Editor Modal — The GUI for creating and editing personas
  6. Testing — Test areas for persona behavior

Data Model

Persona (src/models.py:704)

class Persona:
    name: str
    preferred_models: List[Dict[str, Any]] = field(default_factory=list)
    system_prompt: str = ''
    tool_preset: Optional[str] = None
    bias_profile: Optional[str] = None
    context_preset: Optional[str] = None
    aggregation_strategy: Optional[str] = None
Field Type Purpose
name str Unique identifier. Used as the key in TOML.
preferred_models List[Dict] Ordered list of {provider, model, temperature, top_p, max_output_tokens} dicts. The first entry is the initial model; subsequent entries are escalation targets.
system_prompt str Replaces the default system prompt when this persona is active.
tool_preset Optional[str] Name of a ToolPreset to apply. See guide_tools.md.
bias_profile Optional[str] Name of a BiasProfile to apply. See guide_tools.md.
context_preset Optional[str] Name of a ContextPreset to apply for file selection.
aggregation_strategy Optional[str] One of auto, full, summarize, skeleton. Drives how files are aggregated for this persona.

Convenience Properties:

@property
def provider(self) -> Optional[str]:
    if not self.preferred_models: return None
    return self.preferred_models[0].get("provider")

@property
def model(self) -> Optional[str]:
    if not self.preferred_models: return None
    return self.preferred_models[0].get("model")

@property
def temperature(self) -> Optional[float]:
    if not self.preferred_models: return None
    return self.preferred_models[0].get("temperature")

# Similar: top_p, max_output_tokens

These read from preferred_models[0] (the primary model). The full escalation list is used by multi_agent_conductor.run_worker_lifecycle for retry-based model escalation.

preferred_models Schema

Each entry in the list is a dict with optional keys:

{
    "provider": "gemini",           # Required
    "model": "gemini-3.1-pro-preview",  # Required
    "temperature": 0.0,             # Optional, defaults to 0.0
    "top_p": 1.0,                   # Optional, defaults to 1.0
    "max_output_tokens": 8192,      # Optional, defaults to provider default
}

The list is ordered: index 0 is the first attempt, index 1 is the first escalation, etc. The ConductorEngine's model escalation logic iterates this list on retry.


Storage & Scope

File Locations

Scope Path Configured By
Global <user_config>/personas.toml src/paths.py:get_global_personas_path()
Project <project_root>/personas.toml src/paths.py:get_project_personas_path(project_root)

Both files use the same TOML schema:

[personas.<name>]
preferred_models = [
    { provider = "gemini", model = "gemini-3.1-pro-preview", temperature = 0.0 },
    { provider = "gemini", model = "gemini-3-flash-preview", temperature = 0.0 },
]
system_prompt = "You are a senior backend engineer with deep knowledge of Python asyncio."
tool_preset = "read_only"
bias_profile = "discovery_heavy"
context_preset = "codebase_full"
aggregation_strategy = "summarize"

Scope Inheritance

PersonaManager.load_all() merges global and project personas:

  1. Load global personas first.
  2. If project_root is set, load project personas and overwrite entries with matching names.
  3. Return the merged dict.

Example: If personas.toml (global) has a code_reviewer persona and personas.toml (project) also has a code_reviewer persona, the project version wins. This allows projects to override global defaults without losing the global fallback.

Scope Lookup

get_persona_scope(name) returns "global" or "project" based on which file contains the persona. Used by the editor modal to show the user where a persona is defined.

Save / Delete

  • save_persona(persona, scope="project") writes the persona to the specified scope's TOML file.
  • delete_persona(name, scope="project") removes the entry.
  • The default scope for both is "project". Use "global" for application-wide personas.

PersonaManager

PersonaManager is the CRUD interface.

from src.personas import PersonaManager
from src.models import Persona

manager = PersonaManager(project_root=Path("/path/to/project"))
all_personas = manager.load_all()  # Merged global + project
manager.save_persona(my_persona, scope="project")
manager.delete_persona("old_persona", scope="project")
scope = manager.get_persona_scope("code_reviewer")

load_all()

Returns a Dict[str, Persona] mapping name to Persona. Project entries override global entries of the same name.

save_persona(persona, scope)

Writes a Persona to the specified scope's TOML file. If the file doesn't exist, it's created (parent directories included). If the file exists, the entry is added or updated; other entries are preserved.

delete_persona(name, scope)

Removes the named entry from the specified scope's TOML file. If the entry doesn't exist, the operation is a no-op.

get_persona_scope(name)

Returns "global" or "project" based on which file contains the persona. Returns "project" if neither (effectively saying "would be saved to project if you save").

Internal Helpers

  • _load_file(path) -> Dict[str, Any]: Reads a TOML file, returns {} on error or missing file.
  • _save_file(path, data): Writes a TOML file using tomli_w. Creates parent directories as needed.

MMA Integration

When a Ticket has persona_id set, or when a Tier has a default persona, the ConductorEngine applies the persona to the worker. See guide_mma.md#persona-application for the full integration details.

Application order (in run_worker_lifecycle):

  1. Model selection:
    • ticket.model_override (if set) — used unconditionally
    • Persona preferred_models[0] (if persona applied) — initial model
    • Default tier model — fallback
  2. System prompt: ai_client.set_custom_system_prompt(persona.system_prompt) replaces the default.
  3. Bias profile: ai_client.set_bias_profile(persona.bias_profile) applies semantic nudging.
  4. Tool preset: ai_client.set_tool_preset(persona.tool_preset) configures enabled tools.
  5. Aggregation strategy: Used by the aggregate.py pipeline to choose full/summarize/skeleton.

Failure handling: If the persona fails to load (file not found, parse error, missing fields), the worker logs a warning and falls back to the default model list. The persona is not a hard failure point.

Tier-Scoped vs Ticket-Scoped Personas

  • Tier-scoped: Set in tier_usage[<tier>]["persona"]. Applied to every worker for that tier when no ticket-level persona is set.
  • Ticket-scoped: Set in ticket.persona_id. Overrides tier-level for that specific ticket.

Editor Modal

The GUI provides a Persona Editor modal (src/gui_2.py:_render_persona_editor_modal or similar) for creating and editing personas without manually editing TOML.

Fields exposed in the modal:

  • Name (text input, required)
  • Preferred Models (editable list, with provider/model/temperature/top_p/max_output_tokens per entry)
  • System Prompt (multi-line text input)
  • Tool Preset (dropdown of available presets)
  • Bias Profile (dropdown)
  • Context Preset (dropdown)
  • Aggregation Strategy (radio buttons: auto / full / summarize / skeleton)
  • Scope (radio: Global / Project)

Actions:

  • Save — Writes the persona to the selected scope.
  • Delete — Removes the persona (with confirmation).
  • Duplicate — Creates a copy with a new name.
  • Cancel — Discards changes.

The modal validates the name (must be unique, must be a valid TOML key) and the model entries (provider and model are required) before allowing save.


Configuration

Personas are project-scoped (or global) configuration. There is no central config.toml setting for personas themselves; they're standalone TOML files.

Related settings (in manual_slop.toml or config.toml):

[mma]
default_personas = {
    "Tier 1": "orchestrator",
    "Tier 2": "tech_lead",
    "Tier 3": "code_worker",
    "Tier 4": "qa_reviewer",
}

This sets the default persona for each tier. Tickets without persona_id use the tier's default.


Testing

Unit Tests

  • tests/test_persona_manager.pyPersonaManager CRUD, scope merging, file I/O
  • tests/test_persona_models.pyPersona serialization/deserialization
  • tests/test_persona_id.pyPersona ID validation and uniqueness

Integration Tests

  • tests/test_mma_prompts.py — Verifies persona-derived prompts are constructed correctly
  • tests/test_bias_efficacy.py — Verifies bias profile integration

Test Pattern

def test_persona_scope_overrides(tmp_path):
    # Global persona
    global_path = tmp_path / "global_personas.toml"
    global_path.write_text("""
[personas.coder]
system_prompt = "Global: code only."
""")
    
    # Project persona (override)
    project_path = tmp_path / "personas.toml"
    project_path.write_text("""
[personas.coder]
system_prompt = "Project: code only, focus on our domain."
""")
    
    manager = PersonaManager(project_root=tmp_path, ...)
    # Patch paths module to return the test paths
    with patch("src.paths.get_global_personas_path", return_value=global_path), \
         patch("src.paths.get_project_personas_path", return_value=project_path):
        all_personas = manager.load_all()
    
    assert "coder" in all_personas
    assert all_personas["coder"].system_prompt == "Project: code only, focus on our domain."

Limitations

  1. No Versioning: Personas are stored in TOML. Changes are not versioned unless the project uses git (which most do, but the persona changes are buried in diffs). Consider a "persona history" feature for future.

  2. No Inheritance Chains: A persona cannot reference another persona as a base. If a project wants to share settings across multiple personas, it must duplicate them.

  3. No Validation of Referenced Names: A persona can name a tool_preset that doesn't exist. The error surfaces only when the worker tries to apply the persona.

  4. No Live Reload: Changing a persona TOML file does not take effect until the application is restarted (or PersonaManager.load_all() is called again).

  5. No Conflict Resolution UI: If a project and global define different personas with the same name, the project wins silently. The user must check the editor modal to see the actual definition.


Future Work

  • Persona Composition — Allow a persona to reference another as a base, with override semantics.
  • Live Reload — Watch the persona TOML files and reload on change.
  • Persona History — Track changes over time, allow rollback.
  • Conflict Resolution UI — When global and project both define a persona, show both in the editor with a "use global" / "use project" / "merge" choice.
  • Persona Templates — Pre-built personas for common roles (code reviewer, test writer, doc writer) that users can clone.

See guide_mma.md#persona-application for the worker-side integration.