Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2758,6 +2758,17 @@ def new_session(self, silent=False):
if hasattr(self.agent, "_invalidate_system_prompt"):
self.agent._invalidate_system_prompt()

# Tools-mode startup prewarm: fire in background so first turn
# gets lightweight personalization without blocking.
_hcfg = getattr(self.agent, "_honcho_config", None)
if (
_hcfg is not None
and getattr(_hcfg, "recall_mode", None) == "tools"
and getattr(_hcfg, "tools_startup_context", False)
and getattr(_hcfg, "peer_name", None)
):
self.agent._schedule_honcho_startup_prewarm(_hcfg.peer_name)

if self._session_db:
try:
self._session_db.create_session(
Expand Down
56 changes: 56 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,59 @@ def _shutdown_all_gateway_honcho(self) -> None:
for session_key in list(managers.keys()):
self._shutdown_gateway_honcho(session_key)

def _schedule_gateway_honcho_startup_prewarm(self, session_key: str) -> None:
"""Fire background Honcho startup prewarm for tools mode after session reset.

Non-blocking. Snapshot stored in self._honcho_startup_cache keyed by
peer_name (stable Honcho identity, not ephemeral session key).
Consumed on the first real turn of the new session via the RunAgent
instance created for that turn.
"""
try:
from honcho_integration.client import HonchoClientConfig, get_honcho_client
from honcho_integration.session import HonchoSessionManager
import threading as _threading

hcfg = HonchoClientConfig.from_global_config()
if (
not hcfg.enabled
or not hcfg.api_key
or hcfg.recall_mode != "tools"
or not hcfg.tools_startup_context
or not hcfg.peer_name
):
return

peer_id = hcfg.peer_name
if not hasattr(self, "_honcho_startup_cache"):
self._honcho_startup_cache: dict = {}

def _run():
try:
client = get_honcho_client(hcfg)
manager = HonchoSessionManager(
honcho=client,
config=hcfg,
context_tokens=hcfg.context_tokens,
)
snapshot = manager.fetch_startup_snapshot(peer_id)
if snapshot:
self._honcho_startup_cache[peer_id] = snapshot
logger.debug(
"Gateway Honcho startup snapshot ready for peer %s", peer_id
)
except Exception as exc:
logger.debug(
"Gateway Honcho startup prewarm failed (non-fatal): %s", exc
)

t = _threading.Thread(
target=_run, name="honcho-gw-startup-prewarm", daemon=True
)
t.start()
except Exception as exc:
logger.debug("Gateway Honcho startup prewarm setup failed: %s", exc)

# -- Setup skill availability ----------------------------------------

def _has_setup_skill(self) -> bool:
Expand Down Expand Up @@ -2277,6 +2330,9 @@ async def _handle_reset_command(self, event: MessageEvent) -> str:

self._shutdown_gateway_honcho(session_key)

# Tools-mode startup prewarm for next session
self._schedule_gateway_honcho_startup_prewarm(session_key)

# Reset the session
new_entry = self.session_store.reset_session(session_key)

Expand Down
9 changes: 9 additions & 0 deletions honcho_integration/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ def peer_memory_mode(self, peer_name: str) -> str:
# "context" — auto-injected context only, Honcho tools removed
# "tools" — Honcho tools only, no auto-injected context
recall_mode: str = "hybrid"
# Optional lightweight cached startup context for recallMode=tools.
# When True, Hermes pre-fetches user representation + peer card in the
# background on /new or session reset and injects it on the first real turn.
tools_startup_context: bool = False
# Session resolution
session_strategy: str = "per-session"
session_peer_prefix: bool = False
Expand Down Expand Up @@ -240,6 +244,11 @@ def from_global_config(
or raw.get("recallMode")
or "hybrid"
),
tools_startup_context=bool(
host_block.get("toolsStartupContext")
if host_block.get("toolsStartupContext") is not None
else raw.get("toolsStartupContext", False)
),
session_strategy=session_strategy,
session_peer_prefix=session_peer_prefix,
sessions=raw.get("sessions", {}),
Expand Down
56 changes: 56 additions & 0 deletions honcho_integration/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,62 @@ def get_peer_card(self, session_key: str) -> list[str]:
logger.debug("Failed to fetch peer card from Honcho: %s", e)
return []

def fetch_startup_snapshot(self, peer_id: str) -> dict[str, str]:
"""
Fetch a lightweight user snapshot for tools-mode startup context.

Unlike get_prefetch_context, this does NOT require an active session
in the local cache. It bootstraps a minimal peer object from peer_id
alone, so it can be called right after /new before any session exists.

Returns a dict with 'representation' and 'card' keys (empty strings
on failure). Intentionally excludes AI peer context and dialectic —
those are V2 concerns.
"""
try:
user_peer = self._get_or_create_peer(peer_id)
# Use a minimal temporary session to call context().
# We need a session object for context(); pick the first cached
# session if available, otherwise create a throw-away one keyed
# by peer_id so we don't pollute the real session namespace.
session_key = next(iter(self._sessions_cache), None)
if session_key:
honcho_session = self._sessions_cache[session_key]
# Resolve assistant peer from the cached session
local_sess = next(
(s for s in self._cache.values() if s.honcho_session_id == session_key),
None,
)
assistant_peer_id = (
local_sess.assistant_peer_id
if local_sess
else (self._config.ai_peer if self._config else "hermes-assistant")
)
else:
# No session cached yet — create a bootstrap session object.
# This does NOT add peers or load messages; it just gives us
# a handle to call context() against.
assistant_peer_id = self._config.ai_peer if self._config else "hermes-assistant"
session_key = self._sanitize_id(f"startup-{peer_id}")
honcho_session = self.honcho.session(session_key)

result: dict[str, str] = {}
ctx = honcho_session.context(
summary=False,
tokens=self._context_tokens,
peer_target=peer_id,
peer_perspective=assistant_peer_id,
)
card = ctx.peer_card or []
result["representation"] = ctx.peer_representation or ""
result["card"] = (
"\n".join(card) if isinstance(card, list) else str(card)
)
return result
except Exception as e:
logger.debug("fetch_startup_snapshot failed for peer %s: %s", peer_id, e)
return {}

def search_context(self, session_key: str, query: str, max_tokens: int = 800) -> str:
"""
Semantic search over Honcho session context.
Expand Down
73 changes: 73 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1895,9 +1895,74 @@ def _activate_honcho(
logger.debug("Honcho context pre-warmed for first turn")
except Exception as exc:
logger.debug("Honcho context prefetch failed (non-fatal): %s", exc)
elif hcfg.tools_startup_context and hcfg.peer_name:
self._schedule_honcho_startup_prewarm(hcfg.peer_name)

self._register_honcho_exit_hook()

def _schedule_honcho_startup_prewarm(self, peer_id: str) -> None:
"""Fire a background fetch of user snapshot for tools-mode startup context.

Non-blocking. Result stored in self._honcho_startup_snapshot keyed by
peer_id. Consumed once on the first real user turn via _pop_startup_snapshot().
"""
import threading as _threading

if not self._honcho:
return

def _run():
try:
snapshot = self._honcho.fetch_startup_snapshot(peer_id)
if snapshot:
if not hasattr(self, "_honcho_startup_snapshot"):
self._honcho_startup_snapshot = {}
self._honcho_startup_snapshot[peer_id] = snapshot
logger.debug(
"Honcho tools startup snapshot ready for peer %s", peer_id
)
except Exception as exc:
logger.debug(
"Honcho tools startup prewarm failed (non-fatal): %s", exc
)

t = _threading.Thread(
target=_run, name="honcho-startup-prewarm", daemon=True
)
t.start()

def _pop_startup_snapshot(self) -> str:
"""Return and clear the startup snapshot for the current session's peer.

Returns empty string if no snapshot is ready or feature is disabled.
Only consumes once — subsequent calls return "".
"""
if not self._honcho_config or not self._honcho_config.tools_startup_context:
return ""
peer_id = getattr(self._honcho_config, "peer_name", None)
if not peer_id:
return ""
cache = getattr(self, "_honcho_startup_snapshot", {})
snapshot = cache.pop(peer_id, None)
if not snapshot:
return ""
parts = []
rep = snapshot.get("representation", "")
card = snapshot.get("card", "")
if rep:
parts.append("## User representation\n" + rep)
if card:
parts.append(card)
if not parts:
return ""
return (
"# Honcho Memory (startup context)\n"
"Lightweight cross-session personalization fetched at session start. "
"Use this for personalization on the first turn. "
"Call honcho_context or honcho_search for deeper recall.\n\n"
+ "\n\n".join(parts)
)

def _register_honcho_exit_hook(self) -> None:
"""Register a process-exit flush hook without clobbering signal handlers."""
if self._honcho_exit_hook_registered or not self._honcho:
Expand Down Expand Up @@ -5201,6 +5266,14 @@ def run_conversation(
self._honcho_turn_context = prefetched_context
except Exception as e:
logger.debug("Honcho prefetch failed (non-fatal): %s", e)
elif _recall_mode == "tools" and not conversation_history:
try:
startup_ctx = self._pop_startup_snapshot()
if startup_ctx:
self._honcho_turn_context = startup_ctx
logger.debug("Honcho tools startup snapshot injected on first turn")
except Exception as e:
logger.debug("Honcho startup snapshot inject failed (non-fatal): %s", e)

# Add user message
user_msg = {"role": "user", "content": user_message}
Expand Down
Loading