Profiles
A profile in Hermes-Relay is an upstream-Hermes agent directory — an isolated Hermes instance on your server with its own config, model, and identity. When you pair with a server, the phone auto-discovers every profile on that server and exposes them in the agent sheet — a bottom sheet that opens when you tap the agent name in the Chat top bar.
The three layers
| Layer | What it picks | Scope | Where it lives |
|---|---|---|---|
| Connection | Which Hermes server | One pairing per server | Top-bar chip on the left (hidden with a single connection) |
| Profile | Which agent on that server | Per chat turn, clears on Connection switch | Agent sheet — tap the agent name in the top bar |
| Personality | Which system-prompt preset | Per chat turn | Agent sheet — same sheet, below Profile |
Pick in order: the server (top bar), then the agent + persona (agent sheet). Switching either Profile or Personality shows a toast confirming the new selection.
Where profiles come from
Upstream Hermes profiles live at ~/.hermes/profiles/<name>/ — each is a full, isolated Hermes environment with:
- Its own
config.yaml(model, personalities, provider keys, everything) - Its own
.env(API credentials) - Its own
SOUL.md(the profile's identity / system prompt) - Its own sessions, memory, skills, cron jobs, state database
See the upstream docs: hermes profile create, hermes profile use, hermes -p <name> <command> — each profile gets its own CLI alias.
Create them with:
hermes profile create mizu
mizu setupOr clone from an existing profile:
hermes profile create coder --cloneThe phone doesn't create profiles — you do that on the server. The phone just picks them up on the next pairing (or the next auth.ok round-trip after a relay restart).
What "switching profile" does on the phone
When a profile is selected, the phone first checks whether the relay advertised that profile's own Hermes API server.
- With a profile API server: chat, session browsing, memory, tools, model, and SOUL come from that profile's routed API. The chat session drawer clears and refetches through that profile route, so you see that profile's sessions instead of the default agent's sessions.
- Without a profile API server: the app falls back to the compatibility overlay. It sends the profile
model.defaultandSOUL.mdon each chat turn, but memory, sessions, tools, and provider auth still come from the active Connection. - Voice: relay-owned voice routes receive the selected profile too. Voice Settings shows whether TTS/STT, streaming voice output, or realtime voice came from profile config or fell back to relay/global defaults. Saving voice output or experimental realtime settings while a named profile is active writes that profile's
voice_output:/realtime_voice:section, so profiles likemizukiandvictorcan keep different voices.
If you want true profile isolation, run that profile's gateway as its own service on its own port:
hermes -p mizu platform start api --port 8643Then make sure the relay advertises that API server in the profile metadata, or add that gateway as a separate Connection on the phone. Each routed profile API has its own sessions, memory, and state because it is a distinct gateway.
Picker behaviour
- Hidden when empty. If the server has no
~/.hermes/profiles/*/entries (and just the default root config), the Profile section of the agent sheet doesn't render. - "Default" option at the top of the Profile list. Selecting it clears the override and uses the server's
config.yaml/model.default. - Disabled mid-stream. You can't switch profile during an in-flight chat turn.
- Persisted per Connection (v0.7.0). Your pick survives app restart and follows the Connection it was made on — switching to Connection B brings up B's last-selected profile (or its default if never set), switching back to A restores A's selection. Removing a Connection also clears its remembered pick.
- Jump from Settings. The "Active agent" card at the top of Settings summarizes the current Connection / Profile / Personality and navigates straight to Chat with the agent sheet pre-opened.
Runtime metadata (v0.7.0)
Each profile row in the agent sheet now shows what the relay observes about the profile on disk and at runtime:
- Status dot (green vs grey). A 6 dp dot rendered next to the profile name. Green when the relay has recently probed the profile's gateway and got a response; grey when the probe is idle, stale, or the gateway isn't running. Gateway-off profiles stay selectable — the probe is best-effort and can be wrong across a server restart, so we hint (50% alpha row) rather than disable.
- "N skills" chip. Shown when
skill_count > 0. Counts the skills visible inside the profile directory's skills root. Hidden when zero. Useful for picking "the profile that has the scheduling skill" at a glance. - "SOUL" badge. Shown when the profile has a non-empty
SOUL.mdon disk. Decoupled from whether the system-message content actually loaded — a SOUL badge means "the file exists and isn't empty", an active SOUL in chat means the server actually served the content.
When a profile with a non-empty SOUL is active AND you pick a non-default personality, the agent sheet adds an inline "Profile SOUL overrides personality while active" caption under the personality section, mirroring the existing note under the profile section. Both are kept so the precedence rule is visible from either side of the sheet.
All three indicators are optional on the wire — if you're paired with a pre-v0.7.0 relay that doesn't report them, the dot renders grey, the chips stay hidden, and the badge doesn't appear. Nothing else changes.
Profile Inspector
From the Settings tab, tap the Inspect Agent card (directly under Active Agent) to open a full-screen viewer for the currently-selected profile. Four tabs:
- Config — the profile's
config.yamlrendered as a collapsible JSON tree. Nested objects collapse by default; tap to expand. Values render in monospace. The file path is shown at the top as a caption so you cancdto it from a shell if you want to edit.- Secrets are masked by default. Any value whose key name contains
key,token,secret,password, orcredential(case-insensitive) renders asabcd...wxyzfor values ≥12 chars or********for shorter ones. Tap the eye icon next to the value to reveal it for that row. Reveal state is session-scoped — leaving the screen wipes it. Numbers and booleans are never masked.
- Secrets are masked by default. Any value whose key name contains
- SOUL — the profile's
SOUL.mdrendered as markdown. The</>toggle in the top-right of the pane flips between rendered and raw monospace source. Byte size + file path show above the content. - Memory — one card per file under the profile's
memories/directory (non-recursive). Each card shows the filename and byte size; tap to expand and see its content. - Skills — every skill visible to the profile, grouped by category. Each row has a Switch for enabling/disabling the skill.
Editing (v0.7.1+)
Both SOUL and Memory panes now support in-app edits. Tap the pencil icon in the pane (or card) header to enter edit mode — content renders in a monospace editor with a line-numbered gutter. Bottom bar has Save and Cancel; Save PUTs to the relay, reloads the pane with the fresh content, and surfaces a brief "Saved" snackbar. Save failures keep you in edit mode so you can retry.
For memory entries, the + New entry button at the bottom of the Memory tab opens a filename prompt (must end in .md, no slashes, no leading .) and drops you into an empty editor. A filename that collides with an existing entry is rejected; edit the existing entry via its per-card pencil instead.
Skill toggles
The Switch next to each skill PUTs to /api/skills/toggle when tapped. Relays that haven't shipped the real implementation yet return 501; the app shows a "Skill toggle not yet supported on this server" snackbar, reverts the Switch visually, and ghosts out every row's toggle for the rest of the session with an "Enable/disable requires a newer server" caption under the list.
Very large files (SOUL or a memory entry) are still truncated server-side; when that happens the tab shows a banner noting only the first slice is visible — the editor refuses to open on truncated content so you don't accidentally overwrite the tail. Use the Refresh icon in the top bar to re-fetch every tab, or Retry inside a tab's error state to refetch just that one.
Picker naming
The Profile picker in the Agent sheet (Chat top-bar → tap the agent name) uses these conventions:
- Server default — the no-override row at the top. Clears any active profile pick and lets the server use its
config.yaml/model.default. Renamed from "Default" in v0.7.1 so a profile literally nameddefaultdoesn't collide with this row. - Actual profiles show their description as the primary label when present (readable names like "Victor" rather than directory names), with the profile key as a tertiary caption. A "• Running" or "• Idle" text label accompanies the existing green/grey status dot, and screen readers announce "Gateway running" / "Gateway idle" on the dot itself.
- When the server emits a
profiles.updatedpush (profile added, renamed, or removed on the server side), the app applies the new list immediately and shows a brief "Profiles updated" snackbar. A profile you had selected that the server then removes falls back to Server default automatically.
The Settings card is visible whether or not a profile is currently active; when there's no active profile, the card renders at half opacity with "No active agent" and does nothing when tapped.
Disabling discovery on the server
If you want Connections-only semantics (for example, a minimal deployment where every profile is its own Connection), set this in the relay's config:
relay:
profile_discovery_enabled: falseRestart the relay; the picker stays hidden and the app treats the install as single-agent.
Profile + Personality interaction
If you select a profile AND a personality, the profile wins — its SOUL.md is sent as system_message, not the personality's prompt. That's a deliberate choice: a profile is a richer concept (whole identity), and picking both implies you want the profile's full persona. Pick one at a time to keep behaviour obvious.
At a glance
- Connection = a whole Hermes server.
- Profile = a named agent on that server, discovered from
~/.hermes/profiles/. Picking one overlays its model + SOUL for chat turns. - Personality = a system-prompt preset within the agent's config.
See Connections for the server-level concept and Personalities for the preset-prompt layer.