mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(honcho): plugin drift overhaul -- observation config, chunking, setup wizard, docs, dead code cleanup
Salvaged from PR #5045 by erosika. - Replace memoryMode/peer_memory_modes with granular per-peer observation config - Add message chunking for Honcho API limits (25k chars default) - Add dialectic input guard (10k chars default) - Add dialecticDynamic toggle for reasoning level auto-bump - Rewrite setup wizard with cloud/local deployment picker - Switch peer card/profile/search from session.context() to direct peer APIs - Add server-side observation sync via get_peer_configuration() - Fix base_url/baseUrl config mismatch for self-hosted setups - Fix local auth leak (cloud API keys no longer sent to local instances) - Remove dead code: memoryMode, peer_memory_modes, linkedHosts, suppress flags, SOUL.md aiPeer sync - Add post_setup hook to memory_setup.py for provider-specific setup wizards - Comprehensive README rewrite with full config reference - New optional skill: autonomous-ai-agents/honcho - Expanded memory-providers.md with multi-profile docs - 9 new tests (chunking, dialectic guard, peer lookups), 14 dead tests removed - Fix 2 pre-existing TestResolveConfigPath filesystem isolation failures
This commit is contained in:
parent
12724e6295
commit
c02c3dc723
12 changed files with 1265 additions and 443 deletions
|
|
@ -144,10 +144,6 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
self._last_context_turn = -999
|
||||
self._last_dialectic_turn = -999
|
||||
|
||||
# B2: peer_memory_mode gating (stub)
|
||||
self._suppress_memory = False
|
||||
self._suppress_user_profile = False
|
||||
|
||||
# Port #1957: lazy session init for tools-only mode
|
||||
self._session_initialized = False
|
||||
self._lazy_init_kwargs: Optional[dict] = None
|
||||
|
|
@ -187,9 +183,15 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
def get_config_schema(self):
|
||||
return [
|
||||
{"key": "api_key", "description": "Honcho API key", "secret": True, "env_var": "HONCHO_API_KEY", "url": "https://app.honcho.dev"},
|
||||
{"key": "base_url", "description": "Honcho base URL", "default": "https://api.honcho.dev"},
|
||||
{"key": "baseUrl", "description": "Honcho base URL (for self-hosted)"},
|
||||
]
|
||||
|
||||
def post_setup(self, hermes_home: str, config: dict) -> None:
|
||||
"""Run the full Honcho setup wizard after provider selection."""
|
||||
import types
|
||||
from plugins.memory.honcho.cli import cmd_setup
|
||||
cmd_setup(types.SimpleNamespace())
|
||||
|
||||
def initialize(self, session_id: str, **kwargs) -> None:
|
||||
"""Initialize Honcho session manager.
|
||||
|
||||
|
|
@ -233,48 +235,10 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
except Exception as e:
|
||||
logger.debug("Honcho cost-awareness config parse error: %s", e)
|
||||
|
||||
# ----- Port #1969: aiPeer sync from SOUL.md -----
|
||||
try:
|
||||
hermes_home = kwargs.get("hermes_home", "")
|
||||
if hermes_home and not cfg.raw.get("aiPeer"):
|
||||
soul_path = Path(hermes_home) / "SOUL.md"
|
||||
if soul_path.exists():
|
||||
soul_text = soul_path.read_text(encoding="utf-8").strip()
|
||||
if soul_text:
|
||||
# Try YAML frontmatter: "name: Foo"
|
||||
first_line = soul_text.split("\n")[0].strip()
|
||||
if first_line.startswith("---"):
|
||||
# Look for name: in frontmatter
|
||||
for line in soul_text.split("\n")[1:]:
|
||||
line = line.strip()
|
||||
if line == "---":
|
||||
break
|
||||
if line.lower().startswith("name:"):
|
||||
name_val = line.split(":", 1)[1].strip().strip("\"'")
|
||||
if name_val:
|
||||
cfg.ai_peer = name_val
|
||||
logger.debug("Honcho ai_peer set from SOUL.md: %s", name_val)
|
||||
break
|
||||
elif first_line.startswith("# "):
|
||||
# Markdown heading: "# AgentName"
|
||||
name_val = first_line[2:].strip()
|
||||
if name_val:
|
||||
cfg.ai_peer = name_val
|
||||
logger.debug("Honcho ai_peer set from SOUL.md heading: %s", name_val)
|
||||
except Exception as e:
|
||||
logger.debug("Honcho SOUL.md ai_peer sync failed: %s", e)
|
||||
|
||||
# ----- B2: peer_memory_mode gating (stub) -----
|
||||
try:
|
||||
ai_mode = cfg.peer_memory_mode(cfg.ai_peer)
|
||||
user_mode = cfg.peer_memory_mode(cfg.peer_name or "user")
|
||||
# "honcho" means Honcho owns memory; suppress built-in
|
||||
self._suppress_memory = (ai_mode == "honcho")
|
||||
self._suppress_user_profile = (user_mode == "honcho")
|
||||
logger.debug("Honcho peer_memory_mode: ai=%s (suppress_memory=%s), user=%s (suppress_user_profile=%s)",
|
||||
ai_mode, self._suppress_memory, user_mode, self._suppress_user_profile)
|
||||
except Exception as e:
|
||||
logger.debug("Honcho peer_memory_mode check failed: %s", e)
|
||||
# ----- Port #1969: aiPeer sync from SOUL.md — REMOVED -----
|
||||
# SOUL.md is persona content, not identity config. aiPeer should
|
||||
# only come from honcho.json (host block or root) or the default.
|
||||
# See scratch/memory-plugin-ux-specs.md #10 for rationale.
|
||||
|
||||
# ----- Port #1957: lazy session init for tools-only mode -----
|
||||
if self._recall_mode == "tools":
|
||||
|
|
@ -547,19 +511,71 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
"""Track turn count for cadence and injection_frequency logic."""
|
||||
self._turn_count = turn_number
|
||||
|
||||
@staticmethod
|
||||
def _chunk_message(content: str, limit: int) -> list[str]:
|
||||
"""Split content into chunks that fit within the Honcho message limit.
|
||||
|
||||
Splits at paragraph boundaries when possible, falling back to
|
||||
sentence boundaries, then word boundaries. Each continuation
|
||||
chunk is prefixed with "[continued] " so Honcho's representation
|
||||
engine can reconstruct the full message.
|
||||
"""
|
||||
if len(content) <= limit:
|
||||
return [content]
|
||||
|
||||
prefix = "[continued] "
|
||||
prefix_len = len(prefix)
|
||||
chunks = []
|
||||
remaining = content
|
||||
first = True
|
||||
while remaining:
|
||||
effective = limit if first else limit - prefix_len
|
||||
if len(remaining) <= effective:
|
||||
chunks.append(remaining if first else prefix + remaining)
|
||||
break
|
||||
|
||||
segment = remaining[:effective]
|
||||
|
||||
# Try paragraph break, then sentence, then word
|
||||
cut = segment.rfind("\n\n")
|
||||
if cut < effective * 0.3:
|
||||
cut = segment.rfind(". ")
|
||||
if cut >= 0:
|
||||
cut += 2 # include the period and space
|
||||
if cut < effective * 0.3:
|
||||
cut = segment.rfind(" ")
|
||||
if cut < effective * 0.3:
|
||||
cut = effective # hard cut
|
||||
|
||||
chunk = remaining[:cut].rstrip()
|
||||
remaining = remaining[cut:].lstrip()
|
||||
if not first:
|
||||
chunk = prefix + chunk
|
||||
chunks.append(chunk)
|
||||
first = False
|
||||
|
||||
return chunks
|
||||
|
||||
def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
|
||||
"""Record the conversation turn in Honcho (non-blocking)."""
|
||||
"""Record the conversation turn in Honcho (non-blocking).
|
||||
|
||||
Messages exceeding the Honcho API limit (default 25k chars) are
|
||||
split into multiple messages with continuation markers.
|
||||
"""
|
||||
if self._cron_skipped:
|
||||
return
|
||||
if not self._manager or not self._session_key:
|
||||
return
|
||||
|
||||
msg_limit = self._config.message_max_chars if self._config else 25000
|
||||
|
||||
def _sync():
|
||||
try:
|
||||
session = self._manager.get_or_create(self._session_key)
|
||||
session.add_message("user", user_content[:4000])
|
||||
session.add_message("assistant", assistant_content[:4000])
|
||||
# Flush to Honcho API
|
||||
for chunk in self._chunk_message(user_content, msg_limit):
|
||||
session.add_message("user", chunk)
|
||||
for chunk in self._chunk_message(assistant_content, msg_limit):
|
||||
session.add_message("assistant", chunk)
|
||||
self._manager._flush_session(session)
|
||||
except Exception as e:
|
||||
logger.debug("Honcho sync_turn failed: %s", e)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue