You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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):
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:
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.
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.
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.
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):
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:
Hook checks for persona_ref in config and injects persona text
No core change
Re-injects every turn — same prefix-cache cost as today's ephemeral layer
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
Feature: Personas plugin — canonical persona store + runtime invocation tools + REST API for SwitchUI
Summary
Build a dedicated
personasplugin 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:
hermes-switch, COO). Permanent profile created via bootstrap. Owns routing, delegation, system memory.neocode/CTO,morpheusdesign/architecture,trinityfinance). Permanent dedicated profiles, each with their own SOUL.md (stable identity) + config.yaml (model/provider/memory/tools) + memories/.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):
agent/system_prompt.py:91-95— SOUL.md is slot feat(workflow-engine): plugin contract refactor — phases 1-6 #1 in the stable tierDEFAULT_AGENT_IDENTITYis used (agent/system_prompt.py:97-99)Layer 2 — Ephemeral prompt (re-injected every API call):
gateway/run.py:3170-3180—_load_ephemeral_system_prompt()readscfg_get(cfg, "agent", "system_prompt")agent/conversation_loop.py:1005-1009— concatenated:effective_system = (cached + "\n\n" + ephemeral).strip()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 viadelegate_task, the persona is injected directly into the goal/context (no config.yaml involved). For promoted T3 profiles, apersona_reffield 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:config.yaml agent.system_prompt. When the template is updated, existing profiles keep the old copy. Two sources of truth, gradual divergence.Proposed Design
Plugin location
Three responsibilities
1. Canonical persona store
The
library/directory holds the 16+ persona template files migrated from SwitchUI'sassets/personas/curated/. This becomes the single source of truth.Persona file format (already defined by SwitchUI — preserve it):
default_model,suggested_mcps,suggested_toolsets,default_memory_providerThe 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)persona_get(persona_id)persona_apply(persona_id, target="delegate")delegate_taskgoal/contexttarget="delegate"(default): formatted as a context block for subagent injectiontarget="profile": formatted as config.yamlsystem_promptvalue (for promoted-profile generation)Tool handler signatures must follow the safe pattern:
def handler(args: dict, **_injected):— extract params fromargs, always include**_injectedfor future gateway injections. See the plugin-authoring skill'sreferences/tool-handler-signatures.mdfor 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 detailGET /categories— list available categories with countsPOST /promote— (future) generate a promoted-profile config.yaml from a persona + parent profileAuth pattern: Same as existing plugins — dashboard session token as
Authorization: Bearer <token>. Follow thehermes-switch-uiplugin'splugin_api.pypattern (auth dependency, 32KB body cap, best-effort responses).Dashboard manifest mounting: Each plugin gets its own
/api/plugins/<name>/auto-mount viadashboard/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/directoryregister()with the three toolsdashboard/plugin_api.pywith the REST endpointsStep 2: Migrate the 16 persona templates
hermes-switchui/assets/personas/curated/→plugins/personas/library/<category>/Step 3: Write tests
test_register_contract.py— verify register() runs without side effects, tools appear in registrytest_api_routes.py— verify REST endpoints respond correctly with authhermes-switch-uitest patterns (already excellent — 4 test files, ~900 LOC covering registration, API routes, state, version compat)Step 4: Update SwitchUI to consume the API
GET /api/plugins/personas/callsassets/personas/curated/directory once migration is verifiedDesign Decisions (Already Made)
personaspluginpersona_refpersona_get/persona_applyand 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:pre_llm_callhook (__init__.py:57-62), plusswitchui_info/switchui_statustools/register,/settings,/heartbeat,/status) for liveness/config syncIts state model is built around
manifest+reported_settings+last_heartbeat— none of which map to persona CRUD. Itspre_llm_callhook 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_refresolution — core PR or plugin hook?For promoted T3 profiles (~10% case), we want a
persona_reffield 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):
_load_ephemeral_system_prompt()ingateway/run.py:3170-3180to check forpersona_reffirstplugins/personas/library/<ref>and load file contentsystem_promptifpersona_refis absentOption B —
pre_llm_callhook in the personas plugin:persona_refin config and injects persona textRecommendation: 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:
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.mdor 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 validplugin.yamlandregister(ctx)library/<category>/persona_list,persona_get,persona_applytools registered and callable/api/plugins/personas/with authtest_register_contract.py,test_api_routes.py)hermes-switchprofile configpersona_refresolution decided (Option A or B) and documentedRelated 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 #1gateway/run.py:3170-3180—_load_ephemeral_system_prompt()reads config.yamlagent.system_promptagent/conversation_loop.py:1005-1009— two-layer concatenation at API-call timeplugins/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: