Skip to content

Feature: Personas plugin — canonical persona store + runtime invocation tools + REST API for SwitchUI #143

Description

@Interstellar-code

Feature: Personas plugin — canonical persona store + runtime invocation tools + REST API for SwitchUI

Summary

Build a dedicated personas plugin that owns persona management as a first-class Hermes Agent capability. Migrate the 16 curated persona templates out of the SwitchUI frontend into the plugin as the single source of truth. Provide runtime tools (persona_list, persona_get, persona_apply) so any agent on any platform (CLI, Telegram, Discord, gateway) can invoke personas on demand. Expose a REST API so the SwitchUI wizard consumes personas as a thin client over the canonical store instead of owning a parallel copy.

Ownership inversion: SwitchUI owns the UI; Hermes owns the personas. This eliminates persona drift between the frontend copy and backend copies, and removes the wrong dependency where runtime persona invocation would require a browser-frontend plugin to be active.


Background

The three-tier agent system

The system uses a 3-tier agent architecture:

  • Tier 1 — Orchestrator (hermes-switch, COO). Permanent profile created via bootstrap. Owns routing, delegation, system memory.
  • Tier 2 — Named domain specialists (neo code/CTO, morpheus design/architecture, trinity finance). Permanent dedicated profiles, each with their own SOUL.md (stable identity) + config.yaml (model/provider/memory/tools) + memories/.
  • Tier 3 — Ad hoc specialists created via the SwitchUI profile wizard using a 16-template persona database. Two lifecycle states:
    • Ephemeral — spawned for one task with no persistent memory (the ~90% case)
    • Promoted — graduates to a dedicated profile when invoked repeatedly (the ~10% case)

Key insight: SOUL.md ≠ persona. SOUL.md is the agent's core identity/worldview (e.g., Neo's CTO worldview is fixed). Persona is a specialized lens overlaid on top for a specific task (e.g., "Security Auditor," "Migration Specialist," "Performance Engineer" — each a focal point applied to the same Neo identity).

How the system prompt is assembled (verified)

Neo traced the runtime system prompt assembly end-to-end (file:line evidence below). There are two independent layers concatenated at API-call time:

Layer 1 — Stable/cached prompt (built once per session):

Layer 2 — Ephemeral prompt (re-injected every API call):

  • gateway/run.py:3170-3180_load_ephemeral_system_prompt() reads cfg_get(cfg, "agent", "system_prompt")
  • agent/conversation_loop.py:1005-1009 — concatenated: effective_system = (cached + "\n\n" + ephemeral).strip()
  • Not cached → re-evaluated every turn, breaks prefix cache
  • This is the overlay — persona territory

Implication for personas: The ephemeral layer (config.yaml agent.system_prompt) is architecturally the correct home for persona overlays — it does not replace identity, it appends to it. For ephemeral T3 subagents spawned via delegate_task, the persona is injected directly into the goal/context (no config.yaml involved). For promoted T3 profiles, a persona_ref field pointing to the plugin library is the zero-drift mechanism (requires a small core change — see Open Questions).

The current problem

Today, the 16 persona templates live in the SwitchUI frontend repo (/Volumes/Ext-nvme/Development/hermes-switchui/assets/personas/curated/*.md), not in Hermes Agent. This creates three problems:

  1. Wrong ownership — Personas are a Hermes capability (applied at runtime by T1/T2 agents), but they're owned by a specific frontend. A user on CLI/Telegram/Discord who wants personas has no access to them.
  2. Drift — The wizard copies persona text from the template file into the new profile's config.yaml agent.system_prompt. When the template is updated, existing profiles keep the old copy. Two sources of truth, gradual divergence.
  3. Fragile path dependency — For a T1/T2 agent to use a persona mid-operation today, it would need to know the SwitchUI asset path on an external drive. Environment-dependent, breaks on any other machine.

Proposed Design

Plugin location

plugins/personas/
├── __init__.py              # register(ctx) entry point
├── plugin.yaml              # manifest
├── SKILL.md                 # optional operator doc
├── _library.py              # persona loading + validation
├── _state.py                # atomic persistence (optional, if needed)
├── dashboard/
│   ├── manifest.json        # declares the REST API mount
│   └── plugin_api.py        # FastAPI router
├── library/                 # CANONICAL persona store (migrated from SwitchUI)
│   ├── design/
│   │   └── *.md
│   ├── devops/
│   │   └── *.md
│   ├── engineering/
│   │   └── *.md
│   ├── product/
│   │   └── *.md
│   ├── research/
│   │   └── *.md
│   ├── testing/
│   │   └── *.md
│   └── writing/
│       └── *.md
└── tests/
    ├── test_register_contract.py
    └── test_api_routes.py

Three responsibilities

1. Canonical persona store

The library/ directory holds the 16+ persona template files migrated from SwitchUI's assets/personas/curated/. This becomes the single source of truth.

Persona file format (already defined by SwitchUI — preserve it):

  • YAML frontmatter: default_model, suggested_mcps, suggested_toolsets, default_memory_provider
  • Markdown body: 4-8KB system_prompt defining the persona's specialized behavior

The plugin validates persona files on load (frontmatter schema + non-empty body). Malformed files are skipped with a logged warning, not a crash.

2. Runtime tools (registered via ctx.register_tool())

Three tools available to every agent that has the plugin enabled:

persona_list(category=None)

  • Returns list of personas with metadata (id, name, category, frontmatter summary)
  • Optional category filter (design, devops, engineering, product, research, testing, writing)
  • No parameters required — returns all personas

persona_get(persona_id)

  • Returns full persona content formatted for injection
  • Includes frontmatter suggestions (so the caller can pre-fill model/tools if desired)
  • Returns 404-style error if persona_id is unknown

persona_apply(persona_id, target="delegate")

  • Returns persona text ready to pass into a delegate_task goal/context
  • target="delegate" (default): formatted as a context block for subagent injection
  • target="profile": formatted as config.yaml system_prompt value (for promoted-profile generation)
  • This is the primary runtime invocation path for T1/T2 → T3 dispatch

Tool handler signatures must follow the safe pattern: def handler(args: dict, **_injected): — extract params from args, always include **_injected for future gateway injections. See the plugin-authoring skill's references/tool-handler-signatures.md for the fail pattern to avoid.

3. REST API (mounted at /api/plugins/personas/)

SwitchUI wizard calls this instead of reading local asset files. Endpoints:

  • GET /personas — list personas (optional ?category= filter)
  • GET /personas/{persona_id} — full persona detail
  • GET /categories — list available categories with counts
  • POST /promote — (future) generate a promoted-profile config.yaml from a persona + parent profile

Auth pattern: Same as existing plugins — dashboard session token as Authorization: Bearer <token>. Follow the hermes-switch-ui plugin's plugin_api.py pattern (auth dependency, 32KB body cap, best-effort responses).

Dashboard manifest mounting: Each plugin gets its own /api/plugins/<name>/ auto-mount via dashboard/manifest.json. No piggybacking on the hermes-switch-ui router.


Migration Plan

Step 1: Create the plugin skeleton

  • plugins/personas/ with __init__.py, plugin.yaml, library/ directory
  • Implement register() with the three tools
  • Implement dashboard/plugin_api.py with the REST endpoints

Step 2: Migrate the 16 persona templates

  • Move (not copy) the persona files from hermes-switchui/assets/personas/curated/plugins/personas/library/<category>/
  • Preserve the directory structure (7 categories)
  • Preserve the frontmatter format and body content
  • Update SwitchUI to stop shipping its own copy (see Step 4)

Step 3: Write tests

  • test_register_contract.py — verify register() runs without side effects, tools appear in registry
  • test_api_routes.py — verify REST endpoints respond correctly with auth
  • Follow the hermes-switch-ui test patterns (already excellent — 4 test files, ~900 LOC covering registration, API routes, state, version compat)

Step 4: Update SwitchUI to consume the API

  • Replace the wizard's local asset reads with GET /api/plugins/personas/ calls
  • The wizard becomes a thin client: list → select → (for promoted profiles) POST /promote
  • Remove SwitchUI's assets/personas/curated/ directory once migration is verified

Design Decisions (Already Made)

Decision Choice Rationale
New plugin vs extend hermes-switch-ui New personas plugin Avoids wrong dependency — runtime persona invocation should not require a browser-frontend plugin. See "Why not extend hermes-switch-ui" below.
Migrate vs sync the templates Migrate Personas are a Hermes concept. SwitchUI depends on the plugin, which is reasonable.
Reference vs copy for promoted profiles Reference via persona_ref Zero drift. Requires small core change (see Open Questions).
Where personas live in delegate_task dispatch Read fresh at spawn time T1/T2 reads the template file via persona_get/persona_apply and injects into delegate_task goal. Works today, zero drift, no code change needed.

Why not extend hermes-switch-ui?

Neo inspected plugins/hermes-switch-ui/ (evidence: __init__.py:82-139, _state.py, dashboard/plugin_api.py). It is a bidirectional bridge between the Hermes backend and the SwitchUI browser frontend:

  • Backend→agent: one-time "SwitchUI exists" nudge via pre_llm_call hook (__init__.py:57-62), plus switchui_info/switchui_status tools
  • Frontend→backend: REST sync API (/register, /settings, /heartbeat, /status) for liveness/config sync

Its state model is built around manifest + reported_settings + last_heartbeat — none of which map to persona CRUD. Its pre_llm_call hook injects a static nudge, not dynamic persona composition. Putting a persona store + runtime tools here would fuse two unrelated responsibilities and create the wrong dependency: runtime persona invocation would require a browser-frontend-awareness plugin to be active. The plugin is currently enabled in only 1 of 5 profiles.

The hermes-switch-ui plugin's infrastructure (atomic state writes, FastAPI router pattern, test structure) should be referenced as a proven template when building plugins/personas/, not shared at runtime.


Open Design Questions (Need Decision Before Implementation)

Q1: persona_ref resolution — core PR or plugin hook?

For promoted T3 profiles (~10% case), we want a persona_ref field in config.yaml that the gateway resolves to a persona file at runtime (zero drift, no copy). Two implementation options:

Option A — Small core PR (~15 lines):

  • Modify _load_ephemeral_system_prompt() in gateway/run.py:3170-3180 to check for persona_ref first
  • If set, resolve to plugins/personas/library/<ref> and load file content
  • Falls back to literal system_prompt if persona_ref is absent
  • Cleanest — resolved once at agent init, no per-turn cost
  • Touches core code

Option B — pre_llm_call hook in the personas plugin:

Recommendation: Option A for promoted profiles (persistent, cache cost matters). Option B is a fallback if a core PR is undesirable.

Q2: Memory inheritance for promoted T3 profiles

When a T3 persona is promoted to a dedicated profile, should it:

  • Option A — Symlink memories/ to parent: inherits parent's accumulated knowledge, read-only in practice
  • Option B — Empty memories/: clean slate, no inheritance
  • Option C — Copy parent memories/ at promotion time: snapshot, drifts after

This is a lifecycle question that may be deferred until the first promotion happens.

Q3: Should the plugin own the persona file format schema?

Currently the format (frontmatter + body) is SwitchUI's convention. If the plugin becomes canonical, the schema should be documented in the plugin (a SCHEMA.md or section in SKILL.md) and validated on load. Confirm whether the existing frontmatter fields (default_model, suggested_mcps, suggested_toolsets, default_memory_provider) are the complete set or if more are needed.


Acceptance Criteria

  • plugins/personas/ exists with valid plugin.yaml and register(ctx)
  • 16 persona template files migrated from SwitchUI into library/<category>/
  • persona_list, persona_get, persona_apply tools registered and callable
  • REST API mounted at /api/plugins/personas/ with auth
  • Tests pass (test_register_contract.py, test_api_routes.py)
  • Plugin enabled in at least hermes-switch profile config
  • SwitchUI wizard updated to consume the REST API (coordinated change, may be separate PR)
  • persona_ref resolution decided (Option A or B) and documented

Related Issues


References

Codebase evidence (verified by Neo, read-only trace):

  • agent/system_prompt.py:91-95 — SOUL.md is stable tier slot feat(workflow-engine): plugin contract refactor — phases 1-6 #1
  • gateway/run.py:3170-3180_load_ephemeral_system_prompt() reads config.yaml agent.system_prompt
  • agent/conversation_loop.py:1005-1009 — two-layer concatenation at API-call time
  • plugins/hermes-switch-ui/__init__.py:82-139 — the existing SwitchUI plugin's register() (template to follow)

SwitchUI persona source (to migrate):

  • /Volumes/Ext-nvme/Development/hermes-switchui/assets/personas/curated/*.md (16 files, 7 categories)

Design principles:

  • SwitchUI owns the UI; Hermes owns the personas
  • Persona is an overlay on SOUL.md identity, never a replacement
  • Reference over copy — eliminate drift by design
  • delegate_task is the primary T3 invocation path (90% case); profiles are created only on promotion (10% case)

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp/toolsTool registry, model_tools, toolsetsenhancementNew feature or requestpluginPlugin-related

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions