Merge branch 'simp/r3-27-W2' into simp/integration3

This commit is contained in:
Teknium
2026-09-03 00:11:44 -07:00
4 changed files with 70 additions and 105 deletions
+2 -4
View File
@@ -686,8 +686,7 @@ class HonchoMemoryProvider(DialecticMixin, MemoryProvider):
return json.dumps({"result": card} if card else self._empty_profile_hint(peer))
def _tool_search(self, args: dict) -> str:
query = (args.get("query") or "").strip()
if not query:
if not (query := (args.get("query") or "").strip()):
return tool_error("Missing required parameter: query")
max_tokens = min(int(args.get("max_tokens", 800)), 2000)
result = self._manager.search_context(self._session_key, query, max_tokens=max_tokens, peer=args.get("peer", "user"))
@@ -696,8 +695,7 @@ class HonchoMemoryProvider(DialecticMixin, MemoryProvider):
def _tool_reasoning(self, args: dict) -> str:
from plugins.memory.honcho.session import HonchoAuthError
query = (args.get("query") or "").strip()
if not query:
if not (query := (args.get("query") or "").strip()):
return tool_error("Missing required parameter: query")
try:
# Explicit reasoning bypasses the automatic-injection cap, and surfaces
+52 -79
View File
@@ -8,7 +8,7 @@ import sys
from pathlib import Path
from hermes_constants import get_hermes_home
from plugins.memory.honcho.client import _host_block, profile_host_key, resolve_active_host, resolve_config_path, HOST
from plugins.memory.honcho.client import _first_parsed, _host_block, profile_host_key, resolve_active_host, resolve_config_path, HOST
from hermes_cli.config import cfg_get
RULE = "─" * 40
@@ -511,11 +511,7 @@ def _apply_grant_to_host(hermes_host: dict, cred) -> None:
hermes_host["oauth"] = cred.oauth_block()
if cred.consent_peer_name: # default the peer prompt to the consent name
hermes_host["peerName"] = cred.consent_peer_name
def _open_in_browser(url: str) -> None:
import webbrowser
webbrowser.open(url)
print(" Authorized — token saved. Let's finish configuring.\n")
def _setup_local_auth(cfg: dict, hermes_host: dict) -> None:
@@ -553,10 +549,11 @@ def _setup_device_login(hermes_host: dict, write_path: Path, *, open_browser: bo
print(f" Waiting for approval (expires in {mins} min, Ctrl-C to cancel) ", end="", flush=True)
print("\n Requesting device code…")
import webbrowser
try:
cred = authorize_via_device_code(
config_path=write_path, source="hermes-cli", apply_config=False, display=_show,
open_url=_open_in_browser if open_browser else None, on_poll=lambda: print(".", end="", flush=True),
open_url=webbrowser.open if open_browser else None, on_poll=lambda: print(".", end="", flush=True),
)
except KeyboardInterrupt:
print("\n Cancelled. Re-run 'hermes honcho setup' to try again.\n")
@@ -565,14 +562,11 @@ def _setup_device_login(hermes_host: dict, write_path: Path, *, open_browser: bo
except AccessDenied:
print("\n Sign-in was denied on the approval page.\n" + _RETRY_HINT)
except Exception as e:
if isinstance(e, DeviceFlowError) and e.error == "http_429":
print("\n Too many device-code requests — wait a minute and re-run setup.\n")
else:
print(f"\n Device sign-in failed: {e}\n" + _RETRY_HINT)
print("\n Too many device-code requests — wait a minute and re-run setup.\n"
if isinstance(e, DeviceFlowError) and e.error == "http_429" else f"\n Device sign-in failed: {e}\n" + _RETRY_HINT)
else:
print(" approved")
_apply_grant_to_host(hermes_host, cred)
print(" Authorized — token saved. Let's finish configuring.\n")
return True
return False
@@ -581,10 +575,11 @@ def _setup_browser_login(hermes_host: dict, write_path: Path) -> bool:
"""Loopback OAuth sign-in. Tokens merge into the in-memory cfg so the wizard's final save
keeps them; settings stay wizard-owned (apply_config=False). Returns False on abort."""
from plugins.memory.honcho.oauth_flow import authorize_via_loopback
import webbrowser
def _open(url: str) -> None:
print(f"\n Open this link to authorize (waiting up to 5 minutes):\n\n {url}\n")
_open_in_browser(url)
webbrowser.open(url)
print("\n Starting browser sign-in…")
try:
@@ -593,7 +588,6 @@ def _setup_browser_login(hermes_host: dict, write_path: Path) -> bool:
print(f" OAuth sign-in failed: {e}\n" + _RETRY_HINT)
return False
_apply_grant_to_host(hermes_host, cred)
print(" Authorized — token saved. Let's finish configuring.\n")
return True
@@ -615,11 +609,9 @@ def _setup_cloud_auth(cfg: dict, hermes_host: dict, write_path: Path) -> bool:
default_method = "oauth"
if is_remote or not can_browse:
if device_available:
print(" (no usable local browser detected — device code recommended)")
default_method = "device"
else:
print(" (no usable local browser detected — browser sign-in may need an SSH tunnel to 127.0.0.1:8765)")
default_method = "device" if device_available else "oauth"
print(" (no usable local browser detected — device code recommended)" if device_available else
" (no usable local browser detected — browser sign-in may need an SSH tunnel to 127.0.0.1:8765)")
method = _prompt("oauth, device, or apikey?" if device_available else "OAuth or API key?",
default=default_method).strip().lower()
@@ -630,11 +622,11 @@ def _setup_cloud_auth(cfg: dict, hermes_host: dict, write_path: Path) -> bool:
print(f"\n Current API key: {_mask(cfg.get('apiKey', ''))}")
if new_key := _prompt("Honcho API key (leave blank to keep current)", secret=True):
cfg["apiKey"] = new_key
if not cfg.get("apiKey"):
print("\n No API key configured. Get yours at https://app.honcho.dev\n"
" Run 'hermes honcho setup' again once you have a key.\n")
return False
return True
if cfg.get("apiKey"):
return True
print("\n No API key configured. Get yours at https://app.honcho.dev\n"
" Run 'hermes honcho setup' again once you have a key.\n")
return False
def _menu(header: str, *lines: str) -> None:
@@ -664,10 +656,7 @@ def _setup_tuning(cfg: dict, hermes_host: dict) -> None:
"session -- batch write at session end only",
"N -- write every N turns (e.g. 5)")
new_wf = _prompt("Write frequency", default=str(_pref(hermes_host, cfg, "writeFrequency", "async")))
try:
hermes_host["writeFrequency"] = int(new_wf)
except (ValueError, TypeError):
hermes_host["writeFrequency"] = new_wf if new_wf in {"async", "turn", "session"} else "async"
hermes_host["writeFrequency"] = _first_parsed([new_wf], int, new_wf if new_wf in {"async", "turn", "session"} else "async")
_menu("Recall mode", *(f"{m:<7} -- {desc}" for m, desc in _MODES.items()))
raw_recall = _pref(hermes_host, cfg, "recallMode", "hybrid")
@@ -680,24 +669,18 @@ def _setup_tuning(cfg: dict, hermes_host: dict) -> None:
new_ctx_tokens = _prompt("Context tokens", default=str(current_ctx_tokens) if current_ctx_tokens else "uncapped").strip()
if new_ctx_tokens.lower() in {"none", "uncapped", "no limit"}:
hermes_host.pop("contextTokens", None)
elif new_ctx_tokens:
try:
val = int(new_ctx_tokens)
except (ValueError, TypeError):
val = -1 # non-numeric keeps current
if val >= 0:
hermes_host["contextTokens"] = val
elif new_ctx_tokens and (val := _first_parsed([new_ctx_tokens], int, -1)) >= 0: # non-numeric keeps current
hermes_host["contextTokens"] = val
_menu("Dialectic cadence",
"How often Honcho rebuilds its user model (LLM call on Honcho backend).",
"1 = every turn, 2 = every other turn, 3+ = sparser.",
"Recommended: 1-5.")
new_dialectic = _prompt("Dialectic cadence", default=str(_pref(hermes_host, cfg, "dialecticCadence") or "2"))
try:
if (val := int(new_dialectic)) >= 1:
hermes_host["dialecticCadence"] = val
except (ValueError, TypeError):
if (val := _first_parsed([new_dialectic], int, None)) is None:
hermes_host["dialecticCadence"] = 2
elif val >= 1:
hermes_host["dialecticCadence"] = val
_menu("Dialectic reasoning level",
"Depth Honcho uses when synthesizing user context on auto-injected calls.",
@@ -844,18 +827,13 @@ def cmd_status(args) -> None:
active_path = _config_path()
write_path = _local_config_path()
from plugins.memory.honcho.client import HonchoClientConfig, get_honcho_client
if not cfg: # config file missing — the env-var fallback may still yield a usable config
try:
_env_cfg = HonchoClientConfig.from_global_config(host=_host_key())
cfg = bool(_env_cfg.api_key or _env_cfg.base_url)
except Exception:
cfg = False
if not cfg:
return print(f" No Honcho config found at {active_path}\n Run 'hermes honcho setup' to configure.\n")
not_found = f" No Honcho config found at {active_path}\n Run 'hermes honcho setup' to configure.\n"
try:
hcfg = HonchoClientConfig.from_global_config(host=_host_key())
except Exception as e:
return print(f" Config error: {e}\n")
return print(not_found if not cfg else f" Config error: {e}\n")
if not cfg and not (hcfg.api_key or hcfg.base_url): # file missing and no env-var fallback either
return print(not_found)
# The OAuth access token is also stored under apiKey, so the auth line
# distinguishes a refreshable grant from a static key explicitly.
@@ -912,16 +890,13 @@ def _show_peer_cards(hcfg, client) -> None:
mgr, session_key = _session_manager(hcfg, client)
card = mgr.get_peer_card(session_key)
if card:
print(f"\n User peer card ({len(card)} facts):")
print("\n".join(f" - {fact}" for fact in card[:10]))
print(f"\n User peer card ({len(card)} facts):\n" + "\n".join(f" - {fact}" for fact in card[:10]))
if len(card) > 10:
print(f" ... and {len(card) - 10} more")
ai_text = mgr.get_ai_representation(session_key).get("representation", "")
if ai_text:
print(f"\n AI peer representation:\n {ai_text[:200] + ('...' if len(ai_text) > 200 else '')}")
if not card and not ai_text:
print("\n No peer data yet (accumulates after first conversation)")
print()
print("" if card or ai_text else "\n No peer data yet (accumulates after first conversation)\n")
except Exception as e:
print(f"\n Peer data unavailable: {e}\n")
@@ -991,12 +966,27 @@ def cmd_map(args) -> None:
# ── peer / mode / strategy / tokens ────────────────────────────────────────
def _show_or_set_fields(args, fields: tuple, show) -> None:
"""Shared body of ``peer`` / ``tokens``: with no flags, ``show(block, cfg)`` prints the
current values; otherwise each ``(attr, key, echo, valid)`` flag that is set is written
(an invalid value aborts before saving)."""
cfg = _read_config()
values = {attr: getattr(args, attr, None) for attr, _key, _echo, _valid in fields}
if all(v is None for v in values.values()):
return show(_active_block(cfg), cfg)
for attr, key, echo, valid in fields:
if (value := values[attr]) is None:
continue
if valid is not None and value not in valid: # only --reasoning carries a choice set
return print(f" Invalid reasoning level '{value}'. Options: {', '.join(valid)}")
value = value.strip() if isinstance(value, str) else value
_set_field(cfg, key, value, echo.format(value))
_save(cfg)
def cmd_peer(args) -> None:
"""Show or update peer names and dialectic reasoning level."""
cfg = _read_config()
user_name, ai_name, reasoning = (getattr(args, k, None) for k in ("user", "ai", "reasoning"))
if user_name is None and ai_name is None and reasoning is None:
hermes = _active_block(cfg)
def show(hermes, cfg):
print(f"""
Honcho peers
{RULE}
@@ -1009,17 +999,8 @@ Honcho peers
Dialectic reasoning: {_pref(hermes, cfg, 'dialecticReasoningLevel') or 'low'} ({', '.join(REASONING_LEVELS)})
Dialectic cap: {_pref(hermes, cfg, 'dialecticMaxChars') or 600} chars
""")
return
if user_name is not None:
_set_field(cfg, "peerName", user_name.strip(), f"User peer -> {user_name.strip()}")
if ai_name is not None:
_set_field(cfg, "aiPeer", ai_name.strip(), f"AI peer -> {ai_name.strip()}")
if reasoning is not None:
if reasoning not in REASONING_LEVELS:
return print(f" Invalid reasoning level '{reasoning}'. Options: {', '.join(REASONING_LEVELS)}")
_set_field(cfg, "dialecticReasoningLevel", reasoning, f"Dialectic reasoning level -> {reasoning}")
_save(cfg)
_show_or_set_fields(args, (("user", "peerName", "User peer -> {}", None), ("ai", "aiPeer", "AI peer -> {}", None),
("reasoning", "dialecticReasoningLevel", "Dialectic reasoning level -> {}", REASONING_LEVELS)), show)
def _show_or_set_choice(args, *, attr: str, key: str, noun: str, title: str, choices: dict,
@@ -1054,10 +1035,7 @@ def cmd_strategy(args) -> None:
def cmd_tokens(args) -> None:
"""Show or set token budget settings."""
cfg = _read_config()
context, dialectic = getattr(args, "context", None), getattr(args, "dialectic", None)
if context is None and dialectic is None:
hermes = _active_block(cfg)
def show(hermes, cfg):
print(f"""
Honcho budgets
{RULE}
@@ -1074,13 +1052,8 @@ Honcho budgets
Set with: hermes honcho tokens [--context N] [--dialectic N]
""")
return
if context is not None:
_set_field(cfg, "contextTokens", context, f"context tokens -> {context}")
if dialectic is not None:
_set_field(cfg, "dialecticMaxChars", dialectic, f"dialectic cap -> {dialectic} chars")
_save(cfg)
_show_or_set_fields(args, (("context", "contextTokens", "context tokens -> {}", None),
("dialectic", "dialecticMaxChars", "dialectic cap -> {} chars", None)), show)
# ── identity / migrate ─────────────────────────────────────────────────────
+4 -4
View File
@@ -26,8 +26,8 @@ from hermes_constants import get_hermes_home
# Cache/slot cluster lives in client_cache.py; re-exported because tests and reset
# bookkeeping reach these names through this module.
from plugins.memory.honcho.client_cache import ( # noqa: F401
_DEFAULT_HTTP_TIMEOUT, _apply_fresh_oauth_token, _client_cache_key, _client_slots, _client_slots_lock,
_credential_fingerprint, _honcho_json_timeout_memo, _refresh_cached_oauth, _slot_for,
_DEFAULT_HTTP_TIMEOUT, _client_cache_key, _client_slots, _client_slots_lock, _credential_fingerprint,
_honcho_json_timeout_memo, _refresh_oauth, _slot_for,
)
if TYPE_CHECKING:
@@ -508,7 +508,7 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho:
slot = _slot_for(key)
cached = slot.peek()
if cached is not None:
_refresh_cached_oauth(cached, config, slot)
_refresh_oauth(config, cached, slot)
refreshed = slot.peek()
if refreshed is not None:
return refreshed
@@ -518,7 +518,7 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho:
config = HonchoClientConfig.from_global_config()
# Start with a live access token rather than 401ing an hour in.
_apply_fresh_oauth_token(config)
_refresh_oauth(config)
if not config.api_key and not config.base_url:
raise ValueError("Honcho API key not found. Get your API key at https://app.honcho.dev, "
+12 -18
View File
@@ -169,22 +169,13 @@ def _resolve_timeout_from_sources(config: HonchoClientConfig | None) -> float:
return timeout if timeout is not None else _DEFAULT_HTTP_TIMEOUT
def _apply_fresh_oauth_token(config: HonchoClientConfig) -> None:
"""Refresh a near-expiry OAuth grant and point ``config.api_key`` at it. No-op for static
keys or on failure (the first 401 triggers session.py's forced rotation). Refreshes against
the config's BOUND path: the ambient resolver on daemon threads lands on the default profile."""
try:
from plugins.memory.honcho import oauth
token, _ = oauth.ensure_fresh_token(config.bound_config_path(), config.host)
if token:
config.api_key = token
except Exception:
logger.warning("Honcho OAuth pre-build refresh failed", exc_info=True)
def _refresh_cached_oauth(client: Honcho, config: HonchoClientConfig | None, slot: SingletonSlot | None = None) -> None:
"""Rotate the cached client's Bearer in place when its OAuth token is stale. If the in-place
rotation can't apply (SDK shape change), the slot is reset so the next acquisition rebuilds."""
def _refresh_oauth(config: HonchoClientConfig | None, client: Honcho | None = None, slot: SingletonSlot | None = None) -> None:
"""Refresh a near-expiry OAuth grant. Pre-build (``client=None``): point ``config.api_key`` at the
fresh token so a new client doesn't 401 an hour in. Cached (``client`` given): rotate its Bearer in
place; if the in-place rotation can't apply (SDK shape change) reset ``slot`` so the next acquisition
rebuilds. No-op for static keys or on failure (the first 401 triggers session.py's forced rotation).
Refreshes against the config's BOUND path: the ambient resolver on daemon threads lands on the
default profile."""
from plugins.memory.honcho.client import resolve_active_host, resolve_config_path
try:
@@ -194,7 +185,10 @@ def _refresh_cached_oauth(client: Honcho, config: HonchoClientConfig | None, slo
else:
host, path = resolve_active_host(), resolve_config_path()
token, refreshed = oauth.ensure_fresh_token(path, host)
if refreshed and token and not oauth.apply_token_to_client(client, token) and slot is not None:
if client is None:
if token:
config.api_key = token
elif refreshed and token and not oauth.apply_token_to_client(client, token) and slot is not None:
slot.reset()
except Exception:
logger.warning("Honcho OAuth cached refresh failed", exc_info=True)
logger.warning("Honcho OAuth %s refresh failed", "pre-build" if client is None else "cached", exc_info=True)