feat(memory): declare Honcho config schema, honcho-first registry

Add the grouped Honcho provider schema (connection/identity/session/
dialectic/recall/...) with inline + full-config field split and the
honcho_host_block storage backend. Keep Hindsight, mark its fields inline
so the compact panel is unchanged. Registry lists Honcho before Hindsight.
This commit is contained in:
Erosika 2026-06-22 14:44:50 -04:00
parent ab942330fc
commit 94a0cb283e

View file

@ -4,12 +4,11 @@ Each memory provider *declares* its configurable surface here — the fields, th
types, which values are secrets, and (for selects) the allowed options. A single
generic renderer in the desktop UI and a single generic ``GET/PUT
/api/memory/providers/{name}/config`` endpoint pair drive the whole experience,
so adding a new provider (mem0, honcho, ...) is pure declaration with zero
bespoke UI components or endpoints.
so adding a new provider is pure declaration with no bespoke UI components.
This module is intentionally pure data: it imports nothing from the config/env
layer. ``web_server`` owns the generic read/write logic that interprets these
declarations against config.yaml, the provider config file, and the env store.
declarations, dispatching on ``MemoryProvider.storage`` to the matching backend.
"""
from __future__ import annotations
@ -20,6 +19,15 @@ from dataclasses import dataclass, field as dataclass_field
KIND_TEXT = "text"
KIND_SELECT = "select"
KIND_SECRET = "secret"
KIND_BOOL = "bool"
KIND_NUMBER = "number"
KIND_JSON = "json"
# Storage backends understood by web_server. ``flat_json`` persists non-secret
# fields to ``<hermes_home>/<provider>/config.json``; ``honcho_host_block``
# targets Honcho's real config (honcho.json, scoped to the active profile host).
STORAGE_FLAT_JSON = "flat_json"
STORAGE_HONCHO_HOST_BLOCK = "honcho_host_block"
@dataclass(frozen=True)
@ -37,13 +45,15 @@ class ProviderField:
A field is stored in exactly one place, decided by ``kind``:
* ``text`` / ``select`` persisted to the provider's JSON config file
(``<hermes_home>/<provider>/config.json``) under ``key``.
* non-secret kinds persisted to the provider's config via its storage
backend under ``key``.
* ``secret`` persisted to the env store under ``env_key`` and never read
back out over the API (only an ``is_set`` flag is surfaced).
``aliases`` and ``env_fallbacks`` let a field read legacy values written by
earlier CLI/env setup without re-introducing per-provider code.
earlier CLI/env setup without re-introducing per-provider code. ``inline``
marks the curated subset shown in the compact panel; the rest surface only
in the full-config modal. ``group`` buckets fields within that modal.
"""
key: str
@ -56,6 +66,11 @@ class ProviderField:
env_key: str | None = None
aliases: tuple[str, ...] = ()
env_fallbacks: tuple[str, ...] = ()
inline: bool = False
group: str = ""
# Where a host-block backend stores the field: "host" (per-profile host
# block) or "root" (config root). Ignored by the flat-json backend.
scope: str = "host"
@property
def is_secret(self) -> bool:
@ -71,8 +86,308 @@ class MemoryProvider:
name: str
label: str
storage: str = STORAGE_FLAT_JSON
# Optional link to the provider's config docs, shown in the full-config modal.
docs_url: str = ""
fields: tuple[ProviderField, ...] = dataclass_field(default_factory=tuple)
def inline_fields(self) -> tuple[ProviderField, ...]:
return tuple(f for f in self.fields if f.inline)
# Reasoning effort levels shared by dialectic-related selects.
_REASONING_LEVELS = (
ProviderFieldOption("minimal", "Minimal"),
ProviderFieldOption("low", "Low"),
ProviderFieldOption("medium", "Medium"),
ProviderFieldOption("high", "High"),
ProviderFieldOption("max", "Max"),
)
HONCHO = MemoryProvider(
name="honcho",
label="Honcho",
storage=STORAGE_HONCHO_HOST_BLOCK,
docs_url="https://docs.honcho.dev/v3/guides/integrations/hermes",
fields=(
# — Connection —
ProviderField(
key="apiKey",
label="API key",
kind=KIND_SECRET,
env_key="HONCHO_API_KEY",
description="Authenticate with Honcho Cloud. Not needed for a self-hosted base URL.",
placeholder="Enter Honcho API key",
inline=True,
group="Connection",
),
ProviderField(
key="baseUrl",
label="Base URL",
kind=KIND_TEXT,
aliases=("base_url",),
env_fallbacks=("HONCHO_BASE_URL",),
description="Self-hosted Honcho URL. Overrides the environment when set.",
placeholder="https://… (self-hosted)",
inline=True,
group="Connection",
scope="root",
),
ProviderField(
key="environment",
label="Environment",
kind=KIND_SELECT,
default="production",
env_fallbacks=("HONCHO_ENVIRONMENT",),
description="Honcho environment. Ignored when a base URL is set.",
options=(
ProviderFieldOption("production", "Cloud"),
ProviderFieldOption("local", "Local"),
),
inline=True,
group="Connection",
),
ProviderField(
key="workspace",
label="Workspace",
kind=KIND_TEXT,
description="Honcho workspace ID. Defaults to the profile host.",
inline=True,
group="Connection",
),
# — Identity —
ProviderField(
key="peerName",
label="Peer name",
kind=KIND_TEXT,
description="Your stable user peer. Unifies memory across platforms for single-user setups.",
placeholder="e.g. eri",
inline=True,
group="Identity",
),
ProviderField(
key="aiPeer",
label="AI peer",
kind=KIND_TEXT,
description="The AI-side peer name. Defaults to the profile host.",
inline=True,
group="Identity",
),
# — Session —
ProviderField(
key="sessionStrategy",
label="Session strategy",
kind=KIND_SELECT,
default="per-directory",
description="How conversations map to Honcho sessions.",
options=(
ProviderFieldOption("per-directory", "Per directory"),
ProviderFieldOption("per-repo", "Per repo"),
ProviderFieldOption("per-session", "Per session"),
ProviderFieldOption("global", "Global"),
),
inline=True,
group="Session",
),
# —————— Full-config-only fields below (inline=False) ——————
# — Connection —
ProviderField(
key="timeout",
label="Request timeout",
kind=KIND_NUMBER,
aliases=("requestTimeout",),
env_fallbacks=("HONCHO_TIMEOUT",),
description="Request timeout in seconds for Honcho HTTP calls. Blank uses the default.",
placeholder="30",
group="Connection",
scope="root",
),
# — Identity —
ProviderField(
key="pinUserPeer",
label="Pin user peer",
kind=KIND_BOOL,
default="false",
aliases=("pinPeerName",),
description="Pin the user peer to the peer name, ignoring gateway runtime identity. Unifies memory for single-user setups.",
group="Identity",
),
ProviderField(
key="runtimePeerPrefix",
label="Runtime peer prefix",
kind=KIND_TEXT,
description="Prefix applied to unknown gateway runtime user IDs.",
placeholder="e.g. telegram_",
group="Identity",
),
ProviderField(
key="userPeerAliases",
label="User peer aliases",
kind=KIND_JSON,
description="Map gateway runtime user IDs to stable Honcho peers.",
placeholder='{"telegram_123": "eri"}',
group="Identity",
),
# — Session —
ProviderField(
key="sessionPeerPrefix",
label="Session peer prefix",
kind=KIND_BOOL,
default="false",
description="Prefix session peer names with the host.",
group="Session",
),
ProviderField(
key="sessions",
label="Session overrides",
kind=KIND_JSON,
description="Explicit session ID overrides keyed by resolver.",
placeholder='{"key": "session-id"}',
group="Session",
scope="root",
),
# — Message writing —
ProviderField(
key="saveMessages",
label="Save messages",
kind=KIND_BOOL,
default="true",
description="Persist conversation messages to Honcho.",
group="Message writing",
),
ProviderField(
key="writeFrequency",
label="Write frequency",
kind=KIND_TEXT,
default="async",
description="When to flush messages: async, turn, session, or every N turns.",
placeholder="async | turn | session | N",
group="Message writing",
),
# — Dialectic —
ProviderField(
key="dialecticReasoningLevel",
label="Reasoning level",
kind=KIND_SELECT,
default="low",
description="Reasoning effort for dialectic (peer.chat) calls.",
options=_REASONING_LEVELS,
group="Dialectic",
),
ProviderField(
key="dialecticDynamic",
label="Dynamic reasoning",
kind=KIND_BOOL,
default="true",
description="Let the model override the reasoning level per call.",
group="Dialectic",
),
ProviderField(
key="dialecticMaxChars",
label="Max result chars",
kind=KIND_NUMBER,
description="Max chars of dialectic result injected into the system prompt.",
placeholder="1200",
group="Dialectic",
),
ProviderField(
key="dialecticDepth",
label="Depth",
kind=KIND_NUMBER,
description="Dialectic passes per cycle (13).",
placeholder="1",
group="Dialectic",
),
ProviderField(
key="dialecticDepthLevels",
label="Per-pass levels",
kind=KIND_JSON,
description="Reasoning level per pass; array length matches depth.",
placeholder='["low", "medium"]',
group="Dialectic",
),
ProviderField(
key="dialecticMaxInputChars",
label="Max input chars",
kind=KIND_NUMBER,
description="Max chars of query input sent to peer.chat().",
placeholder="10000",
group="Dialectic",
),
# — Reasoning —
ProviderField(
key="reasoningHeuristic",
label="Reasoning heuristic",
kind=KIND_BOOL,
default="true",
description="Scale the reasoning level up on longer queries.",
group="Reasoning",
),
ProviderField(
key="reasoningLevelCap",
label="Reasoning level cap",
kind=KIND_SELECT,
default="high",
description="Ceiling for the heuristic-selected reasoning level.",
options=_REASONING_LEVELS,
group="Reasoning",
),
# — Recall —
ProviderField(
key="recallMode",
label="Recall mode",
kind=KIND_SELECT,
default="hybrid",
description="How memory retrieval works: hybrid, context-only, or tools-only.",
options=(
ProviderFieldOption("hybrid", "Hybrid"),
ProviderFieldOption("context", "Context only"),
ProviderFieldOption("tools", "Tools only"),
),
group="Recall",
),
ProviderField(
key="contextTokens",
label="Context token cap",
kind=KIND_NUMBER,
description="Cap on auto-injected context tokens. Blank leaves it uncapped.",
placeholder="(uncapped)",
group="Recall",
),
ProviderField(
key="initOnSessionStart",
label="Eager init",
kind=KIND_BOOL,
default="false",
description="Initialize the session eagerly in tools mode instead of on first tool call.",
group="Recall",
),
# — Limits —
ProviderField(
key="messageMaxChars",
label="Message max chars",
kind=KIND_NUMBER,
description="Max chars per message sent to Honcho.",
placeholder="25000",
group="Limits",
),
# — Observation —
ProviderField(
key="observationMode",
label="Observation mode",
kind=KIND_SELECT,
default="directional",
description="Per-peer observation preset. Directional observes all directions; unified shares one view.",
options=(
ProviderFieldOption("directional", "Directional"),
ProviderFieldOption("unified", "Unified"),
),
group="Observation",
),
),
)
HINDSIGHT = MemoryProvider(
name="hindsight",
@ -96,6 +411,7 @@ HINDSIGHT = MemoryProvider(
"Connect to an existing Hindsight instance",
),
),
inline=True,
),
ProviderField(
key="api_key",
@ -104,6 +420,7 @@ HINDSIGHT = MemoryProvider(
env_key="HINDSIGHT_API_KEY",
description="Used to authenticate with the Hindsight API.",
placeholder="Enter Hindsight API key",
inline=True,
),
ProviderField(
key="api_url",
@ -112,6 +429,7 @@ HINDSIGHT = MemoryProvider(
default="https://api.hindsight.vectorize.io",
aliases=("apiUrl",),
env_fallbacks=("HINDSIGHT_API_URL",),
inline=True,
),
ProviderField(
key="bank_id",
@ -119,6 +437,7 @@ HINDSIGHT = MemoryProvider(
kind=KIND_TEXT,
default="hermes",
aliases=("bankId",),
inline=True,
),
ProviderField(
key="recall_budget",
@ -131,14 +450,16 @@ HINDSIGHT = MemoryProvider(
ProviderFieldOption("mid", "mid"),
ProviderFieldOption("high", "high"),
),
inline=True,
),
),
)
# Registry of providers that expose a desktop config surface. Providers without
# an entry here (e.g. ``builtin``) simply render no config panel.
# an entry here (e.g. ``builtin``) simply render no config panel. Honcho leads.
MEMORY_PROVIDERS: dict[str, MemoryProvider] = {
HONCHO.name: HONCHO,
HINDSIGHT.name: HINDSIGHT,
}