Three correctness gaps when honcho.json's identity-mapping config changes mid-flight: 1. The gateway's agent cache signature ignored honcho identity keys, so editing peerName / pinPeerName / userPeerAliases / runtimePeerPrefix was silently dropped until an unrelated cache eviction. Extend _extract_cache_busting_config to fingerprint the resolved honcho config so the AIAgent rebuilds on the next message. 2. cmd_setup let single → multi flips orphan the pinned-pool history under peerName without warning. Detect the transition, warn that runtime users will resolve to fresh empty peers, and auto-steer to hybrid (alias the operator's runtime IDs back to peerName) so the operator's own continuity survives. yes / no overrides available. 3. README didn't document the orphaning behaviour. Add a "Migrating single → multi" callout under Deployment shapes. Tests: - TestPinTransition (test_pin_peer_name.py): fresh-manager flip resolves to runtime, in-process flip is gated by the per-key session cache (documents the gateway-cache-must-bust contract), 3 cache-bust signature tests for pin / aliases / prefix. - TestProfilePeerUniqueness: two profiles pinned to distinct peerNames resolve to distinct peers; host-level peerName overrides root when pinned. - test_single_to_multi_steers_to_hybrid_by_default and test_single_to_multi_yes_override_keeps_multi (test_cli.py): wizard guard end-to-end coverage. |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| cli.py | ||
| client.py | ||
| plugin.yaml | ||
| README.md | ||
| session.py | ||
Honcho Memory Provider
AI-native cross-session user modeling with multi-pass dialectic reasoning, session summaries, bidirectional peer tools, and persistent conclusions.
Honcho docs: https://docs.honcho.dev/v3/guides/integrations/hermes
Requirements
pip install honcho-ai- Honcho API key from app.honcho.dev, or a self-hosted instance
Setup
hermes honcho setup # full interactive wizard (cloud or local)
hermes memory setup # generic picker, also works
Or manually:
hermes config set memory.provider honcho
echo "HONCHO_API_KEY=***" >> ~/.hermes/.env
Architecture Overview
Two-Layer Context Injection
Context is injected into the user message at API-call time (not the system prompt) to preserve prompt caching. Only a static mode header goes in the system prompt. The injected block is wrapped in <memory-context> fences with a system note clarifying it's background data, not new user input.
Two independent layers, each on its own cadence:
Layer 1 — Base context (refreshed every contextCadence turns):
- SESSION SUMMARY — from
session.context(summary=True), placed first - User Representation — Honcho's evolving model of the user
- User Peer Card — key facts snapshot
- AI Self-Representation — Honcho's model of the AI peer
- AI Identity Card — AI peer facts
Layer 2 — Dialectic supplement (fired every dialecticCadence turns):
Multi-pass .chat() reasoning about the user, appended after base context.
Both layers are joined, then truncated to fit contextTokens budget via _truncate_to_budget (tokens × 4 chars, word-boundary safe).
Cold Start vs Warm Session Prompts
Dialectic pass 0 automatically selects its prompt based on session state:
- Cold (no base context cached): "Who is this person? What are their preferences, goals, and working style? Focus on facts that would help an AI assistant be immediately useful."
- Warm (base context exists): "Given what's been discussed in this session so far, what context about this user is most relevant to the current conversation? Prioritize active context over biographical facts."
Not configurable — determined automatically.
Dialectic Depth (Multi-Pass Reasoning)
dialecticDepth (1–3, clamped) controls how many .chat() calls fire per dialectic cycle:
| Depth | Passes | Behavior |
|---|---|---|
| 1 | single .chat() |
Base query only (cold or warm prompt) |
| 2 | audit + synthesis | Pass 0 result is self-audited; pass 1 does targeted synthesis. Conditional bail-out if pass 0 returns strong signal (>300 chars or structured with bullets/sections >100 chars) |
| 3 | audit + synthesis + reconciliation | Pass 2 reconciles contradictions across prior passes into a final synthesis |
Proportional Reasoning Levels
When dialecticDepthLevels is not set, each pass uses a proportional level relative to dialecticReasoningLevel (the "base"):
| Depth | Pass levels |
|---|---|
| 1 | [base] |
| 2 | [minimal, base] |
| 3 | [minimal, base, low] |
Override with dialecticDepthLevels: an explicit array of reasoning level strings per pass.
Three Orthogonal Dialectic Knobs
| Knob | Controls | Type |
|---|---|---|
dialecticCadence |
How often — minimum turns between dialectic firings | int |
dialecticDepth |
How many — passes per firing (1–3) | int |
dialecticReasoningLevel |
How hard — reasoning ceiling per .chat() call |
string |
Input Sanitization
run_conversation strips leaked <memory-context> blocks from user input before processing. When saveMessages persists a turn that included injected context, the block can reappear in subsequent turns via message history. The sanitizer removes <memory-context> blocks plus associated system notes.
Tools
Five bidirectional tools. All accept an optional peer parameter ("user" or "ai", default "user").
| Tool | LLM call? | Description |
|---|---|---|
honcho_profile |
No | Peer card — key facts snapshot |
honcho_search |
No | Semantic search over stored context (800 tok default, 2000 max) |
honcho_context |
No | Full session context: summary, representation, card, messages |
honcho_reasoning |
Yes | LLM-synthesized answer via dialectic .chat() |
honcho_conclude |
No | Write a persistent fact/conclusion about the user |
Tool visibility depends on recallMode: hidden in context mode, always present in tools and hybrid.
Config Resolution
Config is read from the first file that exists:
| Priority | Path | Scope |
|---|---|---|
| 1 | $HERMES_HOME/honcho.json |
Profile-local (isolated Hermes instances) |
| 2 | ~/.hermes/honcho.json |
Default profile (shared host blocks) |
| 3 | ~/.honcho/config.json |
Global (cross-app interop) |
Host key is derived from the active Hermes profile: hermes (default) or hermes.<profile>.
For every key, resolution order is: host block > root > env var > default.
Full Configuration Reference
Identity & Connection
| Key | Type | Default | Description |
|---|---|---|---|
apiKey |
string | — | API key. Falls back to HONCHO_API_KEY env var |
baseUrl |
string | — | Base URL for self-hosted Honcho. Local URLs auto-skip API key auth |
environment |
string | "production" |
SDK environment mapping |
enabled |
bool | auto | Master toggle. Auto-enables when apiKey or baseUrl present |
workspace |
string | host key | Honcho workspace ID. Shared environment — all profiles in the same workspace can see the same user identity and related memories |
peerName |
string | — | User peer identity |
aiPeer |
string | host key | AI peer identity |
Identity Mapping (Gateway Multi-User)
In gateway deployments (Telegram, Discord, Slack, etc.) each user arrives with a platform-native runtime ID (Telegram UID, Discord snowflake, Slack user). These three keys control how those runtime IDs map to Honcho peers. The resolver is config-driven and deterministic — no automatic merging or runtime inference.
| Key | Type | Default | Description |
|---|---|---|---|
pinUserPeer |
bool | false |
When true, every gateway runtime user collapses to peerName. Single-operator deployments where you want all your platforms (and any other users) to share one peer. Aliased from pinPeerName (the original key, still accepted) |
pinPeerName |
bool | false |
Legacy name for pinUserPeer. Backwards-compatible; same effect |
userPeerAliases |
object | {} |
Map of runtime IDs to peer IDs ({"86701400": "eri"}). Many-to-one is the intended pattern — alias all your runtime IDs to one peer name. One-to-many is not supported; one runtime ID resolves to exactly one peer |
runtimePeerPrefix |
string | "" |
Prepended to unknown runtime IDs to namespace them (e.g. "telegram_" → telegram_86701400). Used only when no alias matches. Prevents collisions between platforms whose runtime IDs share the same shape |
Resolver ladder (first match wins):
1. pinUserPeer / pinPeerName=true → return peerName (ignore runtime ID)
2. userPeerAliases[runtime_id] → return aliased peer
3. userPeerAliases[runtime_id_alt] → check alt-ID too (Telegram UID + username, etc.)
4. runtimePeerPrefix + runtime_id → namespaced peer, with sha256 collision escalation
5. raw sanitized runtime_id → fallback peer
6. peerName → no runtime ID at all (CLI/TUI)
7. session-key fallback → no config either
Why no pinAiPeer? The AI peer is already pinned by construction — aiPeer is the only AI-side identity setting and the resolver never overrides it. Only the user-side peer has the runtime-vs-config tension that pinUserPeer resolves.
Host vs root semantics. All three keys are accepted at both root and hosts.<host> levels. Host-level wins. For maps and prefixes, host-level replaces the root value as a whole (not merge), so a host can intentionally own its identity universe or wipe it with userPeerAliases: {} / runtimePeerPrefix: "".
Deployment shapes (hermes honcho setup asks one prompt to set these):
- Single-operator —
pinUserPeer: true. All gateway users →peerName. Recommended for personal use where you connect Hermes to your own Telegram/Discord/etc. - Multi-user gateway —
pinUserPeer: false, optionalruntimePeerPrefix. Each runtime user → own peer. Recommended for bots serving many humans. - Hybrid —
pinUserPeer: false,userPeerAliasesmapping the operator's runtime IDs topeerName. Multi-user gateway where YOU are routed but others stay distinct.
Migrating single → multi. Flipping pinUserPeer from true to false does not migrate data. Memory accumulated under peerName while pinned stays there; runtime users now resolve to fresh, empty peers. To preserve your own continuity, use the hybrid shape — alias your runtime IDs back to peerName so your turns keep landing on the pooled history while other users get their own peers. The setup wizard offers this path automatically when it detects a single → multi transition.
Memory & Recall
| Key | Type | Default | Description |
|---|---|---|---|
recallMode |
string | "hybrid" |
"hybrid" (auto-inject + tools), "context" (auto-inject only, tools hidden), "tools" (tools only, no injection). Legacy "auto" → "hybrid" |
observationMode |
string | "directional" |
Preset: "directional" (all on) or "unified" (shared pool). Use observation object for granular control |
observation |
object | — | Per-peer observation config (see Observation section) |
Write Behavior
| Key | Type | Default | Description |
|---|---|---|---|
writeFrequency |
string/int | "async" |
"async" (background), "turn" (sync per turn), "session" (batch on end), or integer N (every N turns) |
saveMessages |
bool | true |
Persist messages to Honcho API |
Session Resolution
| Key | Type | Default | Description |
|---|---|---|---|
sessionStrategy |
string | "per-directory" |
"per-directory", "per-session", "per-repo" (git root), "global" |
sessionPeerPrefix |
bool | false |
Prepend peer name to session keys |
sessions |
object | {} |
Manual directory-to-session-name mappings |
Session Name Resolution
The Honcho session name determines which conversation bucket memory lands in. Resolution follows a priority chain — first match wins:
| Priority | Source | Example session name |
|---|---|---|
| 1 | Manual map (sessions config) |
"myproject-main" |
| 2 | /title command (mid-session rename) |
"refactor-auth" |
| 3 | Gateway session key (Telegram, Discord, etc.) | "agent-main-telegram-dm-8439114563" |
| 4 | per-session strategy |
Hermes session ID (20260415_a3f2b1) |
| 5 | per-repo strategy |
Git root directory name (hermes-agent) |
| 6 | per-directory strategy |
Current directory basename (src) |
| 7 | global strategy |
Workspace name (hermes) |
Gateway platforms always resolve via priority 3 (per-chat isolation) regardless of sessionStrategy. The strategy setting only affects CLI sessions.
If sessionPeerPrefix is true, the peer name is prepended: eri-hermes-agent.
What each strategy produces
per-directory— basename of$PWD. Opening hermes in~/code/myappand~/code/othergives two separate sessions. Same directory = same session across runs.per-repo— git root directory name. All subdirectories within a repo share one session. Falls back toper-directoryif not inside a git repo.per-session— Hermes session ID (timestamp + hex). Everyhermesinvocation starts a fresh Honcho session. Falls back toper-directoryif no session ID is available.global— workspace name. One session for everything. Memory accumulates across all directories and runs.
Multi-Profile Pattern
Multiple Hermes profiles can share one workspace while maintaining separate AI identities. Config resolution is host block > root > env var > default — host blocks inherit from root, so shared settings only need to be declared once:
{
"apiKey": "***",
"workspace": "hermes",
"peerName": "yourname",
"hosts": {
"hermes": {
"aiPeer": "hermes",
"recallMode": "hybrid",
"sessionStrategy": "per-directory"
},
"hermes.coder": {
"aiPeer": "coder",
"recallMode": "tools",
"sessionStrategy": "per-repo"
}
}
}
Both profiles see the same user (yourname) in the same shared environment (hermes), but each AI peer builds its own observations, conclusions, and behavior patterns. The coder's memory stays code-oriented; the main agent's stays broad.
Host key is derived from the active Hermes profile: hermes (default) or hermes.<profile> (e.g. hermes -p coder → host key hermes.coder).
Dialectic & Reasoning
| Key | Type | Default | Description |
|---|---|---|---|
dialecticDepth |
int | 1 |
Passes per dialectic cycle (1–3, clamped). 1=single query, 2=audit+synthesis, 3=audit+synthesis+reconciliation |
dialecticDepthLevels |
array | — | Optional array of reasoning level strings per pass. Overrides proportional defaults. Example: ["minimal", "low", "medium"] |
dialecticReasoningLevel |
string | "low" |
Base reasoning level for .chat(): "minimal", "low", "medium", "high", "max" |
dialecticDynamic |
bool | true |
When true, model can override reasoning level per-call via honcho_reasoning tool. When false, always uses dialecticReasoningLevel |
dialecticMaxChars |
int | 600 |
Max chars of dialectic result injected into system prompt |
dialecticMaxInputChars |
int | 10000 |
Max chars for dialectic query input to .chat(). Honcho cloud limit: 10k |
Token Budgets
| Key | Type | Default | Description |
|---|---|---|---|
contextTokens |
int | SDK default | Token budget for context() API calls. Also gates prefetch truncation (tokens × 4 chars) |
messageMaxChars |
int | 25000 |
Max chars per message sent via add_messages(). Exceeding this triggers chunking with [continued] markers. Honcho cloud limit: 25k |
Cadence (Cost Control)
| Key | Type | Default | Description |
|---|---|---|---|
contextCadence |
int | 1 |
Minimum turns between base context refreshes (session summary + representation + card) |
dialecticCadence |
int | 1 |
Minimum turns between dialectic .chat() firings |
injectionFrequency |
string | "every-turn" |
"every-turn" or "first-turn" (inject context on the first user message only, skip from turn 2 onward) |
reasoningLevelCap |
string | — | Hard cap on reasoning level: "minimal", "low", "medium", "high" |
Observation (Granular)
Maps 1:1 to Honcho's per-peer SessionPeerConfig. When present, overrides observationMode preset.
"observation": {
"user": { "observeMe": true, "observeOthers": true },
"ai": { "observeMe": true, "observeOthers": true }
}
| Field | Default | Description |
|---|---|---|
user.observeMe |
true |
User peer self-observation (Honcho builds user representation) |
user.observeOthers |
true |
User peer observes AI messages |
ai.observeMe |
true |
AI peer self-observation (Honcho builds AI representation) |
ai.observeOthers |
true |
AI peer observes user messages (enables cross-peer dialectic) |
Presets:
"directional"(default): all fourtrue"unified": userobserveMe=true, AIobserveOthers=true, restfalse
Hardcoded Limits
| Limit | Value |
|---|---|
| Search tool max tokens | 2000 (hard cap), 800 (default) |
| Peer card fetch tokens | 200 |
Environment Variables
| Variable | Fallback for |
|---|---|
HONCHO_API_KEY |
apiKey |
HONCHO_BASE_URL |
baseUrl |
HONCHO_ENVIRONMENT |
environment |
HERMES_HONCHO_HOST |
Host key override |
CLI Commands
| Command | Description |
|---|---|
hermes honcho setup |
Full interactive setup wizard |
hermes honcho status |
Show resolved config for active profile |
hermes honcho enable / disable |
Toggle Honcho for active profile |
hermes honcho mode <mode> |
Change recall or observation mode |
hermes honcho peer --user <name> |
Update user peer name |
hermes honcho peer --ai <name> |
Update AI peer name |
hermes honcho tokens --context <N> |
Set context token budget |
hermes honcho tokens --dialectic <N> |
Set dialectic max chars |
hermes honcho map <name> |
Map current directory to a session name |
hermes honcho sync |
Create host blocks for all Hermes profiles |
Example Config
{
"apiKey": "***",
"workspace": "hermes",
"peerName": "username",
"contextCadence": 2,
"dialecticCadence": 3,
"dialecticDepth": 2,
"hosts": {
"hermes": {
"enabled": true,
"aiPeer": "hermes",
"recallMode": "hybrid",
"observation": {
"user": { "observeMe": true, "observeOthers": true },
"ai": { "observeMe": true, "observeOthers": true }
},
"writeFrequency": "async",
"sessionStrategy": "per-directory",
"dialecticReasoningLevel": "low",
"dialecticDepth": 2,
"dialecticMaxChars": 600,
"saveMessages": true
},
"hermes.coder": {
"enabled": true,
"aiPeer": "coder",
"sessionStrategy": "per-repo",
"dialecticDepth": 1,
"dialecticDepthLevels": ["low"],
"observation": {
"user": { "observeMe": true, "observeOthers": false },
"ai": { "observeMe": true, "observeOthers": true }
}
}
},
"sessions": {
"/home/user/myproject": "myproject-main"
}
}