Download plugins/memory/byterover/__init__.py from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/plugins/memory/byterover/__init__.py
- Command line
-
hf download hf://SaylorTwift/hermes-agent/plugins/memory/byterover/__init__.py
-
curl -L -o __init__.py https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/plugins/memory/byterover/__init__.py
13.7 kB
| """ByteRover memory plugin — MemoryProvider interface. | |
| Persistent memory via the ByteRover CLI (``brv``): hierarchical context tree with tiered retrieval | |
| (fuzzy text → LLM-driven search), local-first with optional cloud sync (BRV_API_KEY). Requires the | |
| ``brv`` CLI (npm install -g byterover-cli, or byterover.dev/install.sh). Working directory is | |
| $HERMES_HOME/byterover/ (profile-scoped); ``memory.byterover.auto_extract: false`` disables curate hooks. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import os | |
| import shutil | |
| import subprocess | |
| import threading | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional | |
| from agent.memory_provider import MemoryProvider, spawn_context_thread | |
| from tools.registry import tool_error | |
| logger = logging.getLogger(__name__) | |
| _QUERY_TIMEOUT = 10 # brv query — should be fast | |
| _CURATE_TIMEOUT = 120 # brv curate — may involve LLM processing | |
| _MIN_QUERY_LEN = 10 # noise filters | |
| _MIN_OUTPUT_LEN = 20 | |
| _BOOL_WORDS = {**dict.fromkeys(("1", "true", "yes", "on"), True), **dict.fromkeys(("0", "false", "no", "off"), False)} | |
| def _coerce_bool(value: Any, default: bool = False) -> bool: | |
| """bool/number -> truthiness; common true/false words parsed; anything else -> default.""" | |
| if isinstance(value, (bool, int, float)): | |
| return bool(value) | |
| return _BOOL_WORDS.get(value.strip().lower(), default) if isinstance(value, str) else default | |
| def _load_plugin_config() -> Dict[str, Any]: | |
| """Read ``memory.byterover``; fall back to legacy ``memory.provider_config`` (early docs used it).""" | |
| try: | |
| from hermes_cli.config import load_config | |
| memory_config = load_config().get("memory", {}) | |
| except Exception: | |
| return {} | |
| for key in ("byterover", "provider_config") if isinstance(memory_config, dict) else (): | |
| block = memory_config.get(key, {}) | |
| if isinstance(block, dict) and (block or key == "provider_config"): | |
| return dict(block) | |
| return {} | |
| def _get_brv_cwd() -> Path: | |
| """Profile-scoped working directory for the brv context tree.""" | |
| from hermes_constants import get_hermes_home | |
| return get_hermes_home() / "byterover" | |
| # brv binary resolution (cached, thread-safe): None = unresolved, "" = resolved-missing | |
| _brv_path_lock = threading.Lock() | |
| _cached_brv_path: Optional[str] = None | |
| def _resolve_brv_path() -> Optional[str]: | |
| """Find the brv binary on PATH or well-known install locations (resolved once; lookup is outside the lock).""" | |
| global _cached_brv_path | |
| with _brv_path_lock: | |
| if _cached_brv_path is not None: | |
| return _cached_brv_path or None | |
| candidates = (Path.home() / ".brv-cli/bin/brv", Path("/usr/local/bin/brv"), Path.home() / ".npm-global/bin/brv") | |
| found = shutil.which("brv") or next((str(c) for c in candidates if c.exists()), None) | |
| with _brv_path_lock: | |
| if _cached_brv_path is None: | |
| _cached_brv_path = found or "" | |
| return _cached_brv_path or None | |
| def _brv_child_env(brv_path: str) -> Dict[str, str]: | |
| """Env for the ``brv`` child. Under gateway.multiplex_profiles ``os.environ`` holds the | |
| LAUNCH profile's ``.env``; the served profile's ``BRV_*`` (cloud identity) live only in its | |
| secret scope, so they are resolved through the scope — a miss means no cloud key, never | |
| another profile's — and the launch profile's ``.env`` residue is stripped. Outside multiplex | |
| the process env IS this profile's own and is passed through unchanged.""" | |
| from agent.secret_scope import UnscopedSecretError, get_secret, is_multiplex_active | |
| from hermes_constants import get_hermes_home_override | |
| from tools.environments.local import build_subprocess_env, strip_launch_profile_env | |
| env = build_subprocess_env(scrub_secrets=False) | |
| if is_multiplex_active(): | |
| env = strip_launch_profile_env(env, get_hermes_home_override()) | |
| for key in [k for k in env if k.startswith("BRV_")]: | |
| env.pop(key, None) | |
| try: | |
| api_key = get_secret("BRV_API_KEY", "") | |
| except UnscopedSecretError: | |
| api_key = "" | |
| if api_key: | |
| env["BRV_API_KEY"] = api_key | |
| env["PATH"] = str(Path(brv_path).parent) + os.pathsep + env.get("PATH", "") | |
| return env | |
| def _run_brv(args: List[str], timeout: int = _QUERY_TIMEOUT, cwd: str = None) -> dict: | |
| """Run a brv CLI command. Returns {success, output, error}.""" | |
| global _cached_brv_path | |
| brv_path = _resolve_brv_path() | |
| if not brv_path: | |
| return {"success": False, "error": "brv CLI not found. Install: npm install -g byterover-cli"} | |
| effective_cwd = cwd or str(_get_brv_cwd()) | |
| Path(effective_cwd).mkdir(parents=True, exist_ok=True) | |
| env = _brv_child_env(brv_path) | |
| try: | |
| result = subprocess.run( | |
| [brv_path] + args, capture_output=True, text=True, encoding='utf-8', errors='replace', | |
| timeout=timeout, cwd=effective_cwd, env=env, stdin=subprocess.DEVNULL, | |
| ) | |
| except subprocess.TimeoutExpired: | |
| return {"success": False, "error": f"brv timed out after {timeout}s"} | |
| except FileNotFoundError: | |
| with _brv_path_lock: | |
| _cached_brv_path = None # binary vanished; re-resolve next call | |
| return {"success": False, "error": "brv CLI not found"} | |
| except Exception as e: | |
| return {"success": False, "error": str(e)} | |
| stdout, stderr = result.stdout.strip(), result.stderr.strip() | |
| if result.returncode == 0: | |
| return {"success": True, "output": stdout} | |
| return {"success": False, "error": stderr or stdout or f"brv exited {result.returncode}"} | |
| def _schema(name: str, description: str, arg: str = "", arg_desc: str = "") -> dict: | |
| props = {arg: {"type": "string", "description": arg_desc}} if arg else {} | |
| return {"name": name, "description": description, "parameters": {"type": "object", "properties": props, "required": [arg] if arg else []}} | |
| QUERY_SCHEMA = _schema( | |
| "brv_query", "Search ByteRover's persistent knowledge tree for relevant context. Returns memories, project knowledge, " | |
| "architectural decisions, and patterns from previous sessions. Use for any question where past context would help.", | |
| "query", "What to search for.") | |
| CURATE_SCHEMA = _schema( | |
| "brv_curate", "Store important information in ByteRover's persistent knowledge tree. Use for architectural decisions, bug fixes, " | |
| "user preferences, project patterns — anything worth remembering across sessions. ByteRover's LLM automatically " | |
| "categorizes and organizes the memory.", "content", "The information to remember.") | |
| STATUS_SCHEMA = _schema("brv_status", "Check ByteRover status — CLI version, context tree stats, cloud sync state.") | |
| class ByteRoverMemoryProvider(MemoryProvider): | |
| """ByteRover persistent memory via the brv CLI.""" | |
| def __init__(self, config: Optional[Dict[str, Any]] = None): | |
| self._config = dict(config) if config is not None else _load_plugin_config() | |
| self._auto_extract = _coerce_bool(self._config.get("auto_extract"), True) | |
| self._cwd, self._session_id, self._turn_count = "", "", 0 | |
| self._sync_thread: Optional[threading.Thread] = None | |
| def name(self) -> str: | |
| return "byterover" | |
| def is_available(self) -> bool: | |
| """Check if brv CLI is installed. No network calls.""" | |
| return _resolve_brv_path() is not None | |
| def get_config_schema(self): | |
| return [ | |
| {"key": "api_key", "description": "ByteRover API key (optional, for cloud sync)", "secret": True, | |
| "env_var": "BRV_API_KEY", "url": "https://app.byterover.dev"}, | |
| {"key": "auto_extract", "description": "Automatically curate completed turns and compression/memory hooks", | |
| "default": "true", "choices": ["true", "false"]}, | |
| ] | |
| def initialize(self, session_id: str, **kwargs) -> None: | |
| self._cwd, self._session_id, self._turn_count = str(_get_brv_cwd()), session_id, 0 | |
| Path(self._cwd).mkdir(parents=True, exist_ok=True) | |
| def system_prompt_block(self) -> str: | |
| if not _resolve_brv_path(): | |
| return "" | |
| return ("# ByteRover Memory\nActive. Persistent knowledge tree with hierarchical context.\n" | |
| "Use brv_query to search past knowledge, brv_curate to store important facts, brv_status to check state.") | |
| def _query(self, query: str) -> dict: | |
| return _run_brv(["query", "--", query.strip()[:5000]], timeout=_QUERY_TIMEOUT, cwd=self._cwd) | |
| def _curate(self, content: str) -> dict: | |
| return _run_brv(["curate", "--", content], timeout=_CURATE_TIMEOUT, cwd=self._cwd) | |
| def _curate_in_background(self, content: str, *, name: str, what: str, on_done: str = "") -> threading.Thread: | |
| """Spawn a daemon thread that curates ``content``; failures are logged at debug, never raised.""" | |
| def _work(): | |
| try: | |
| self._curate(content) | |
| if on_done: | |
| logger.info(on_done) | |
| except Exception as e: | |
| logger.debug("ByteRover %s failed: %s", what, e) | |
| t = spawn_context_thread(_work, name=name) | |
| t.start() | |
| return t | |
| def _auto_extract_enabled(self, what: str) -> bool: | |
| if not self._auto_extract: | |
| logger.debug("ByteRover %s skipped (auto_extract disabled)", what) | |
| return self._auto_extract | |
| def prefetch(self, query: str, *, session_id: str = "") -> str: | |
| """Run brv query synchronously (blocks up to _QUERY_TIMEOUT) so context is ready before the first LLM call.""" | |
| if not query or len(query.strip()) < _MIN_QUERY_LEN: | |
| return "" | |
| result = self._query(query) | |
| output = (result.get("output") or "").strip() if result["success"] else "" | |
| return f"## ByteRover Context\n{output}" if len(output) > _MIN_OUTPUT_LEN else "" | |
| def queue_prefetch(self, query: str, *, session_id: str = "") -> None: | |
| """No-op: prefetch() runs synchronously at turn start.""" | |
| def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None: | |
| """Curate the conversation turn in background (non-blocking); only substantive turns.""" | |
| self._turn_count += 1 | |
| if not self._auto_extract_enabled("sync_turn") or len(user_content.strip()) < _MIN_QUERY_LEN: | |
| return | |
| if self._sync_thread and self._sync_thread.is_alive(): # wait for the previous sync so curates don't pile up | |
| self._sync_thread.join(timeout=5.0) | |
| self._sync_thread = self._curate_in_background(f"User: {user_content[:2000]}\nAssistant: {assistant_content[:2000]}", | |
| name="brv-sync", what="sync") | |
| def on_memory_write(self, action: str, target: str, content: str) -> None: | |
| """Mirror built-in memory writes to ByteRover.""" | |
| if self._auto_extract_enabled("memory mirror") and action in {"add", "replace"} and content: | |
| label = "User profile" if target == "user" else "Agent memory" | |
| self._curate_in_background(f"[{label}] {content}", name="brv-memwrite", what="memory mirror") | |
| def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str: | |
| """Extract insights from the last 10 user/assistant messages before compression discards them.""" | |
| if not self._auto_extract_enabled("pre-compression flush") or not messages: | |
| return "" | |
| parts = [f"{msg.get('role', '')}: {msg.get('content', '')[:500]}" for msg in messages[-10:] | |
| if msg.get("role", "") in {"user", "assistant"} and isinstance(msg.get("content", ""), str) and msg.get("content", "").strip()] | |
| if parts: | |
| self._curate_in_background("[Pre-compression context]\n" + "\n".join(parts), name="brv-flush", what="pre-compression flush", | |
| on_done=f"ByteRover pre-compression flush: {len(parts)} messages") | |
| return "" | |
| def get_tool_schemas(self) -> List[Dict[str, Any]]: | |
| return [QUERY_SCHEMA, CURATE_SCHEMA, STATUS_SCHEMA] | |
| def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: | |
| if tool_name not in _TOOLS: | |
| return tool_error(f"Unknown tool: {tool_name}") | |
| arg, fail_msg, run, on_ok = _TOOLS[tool_name] | |
| value = args.get(arg, "") if arg else None | |
| if arg and not value: | |
| return tool_error(f"{arg} is required") | |
| result = run(self, value) | |
| return json.dumps(on_ok(result.get("output", ""))) if result["success"] else tool_error(result.get("error", fail_msg)) | |
| def shutdown(self) -> None: | |
| if self._sync_thread and self._sync_thread.is_alive(): | |
| self._sync_thread.join(timeout=10.0) | |
| def _format_query_output(output: str) -> dict: | |
| output = output.strip() | |
| if len(output) < _MIN_OUTPUT_LEN: | |
| return {"result": "No relevant memories found."} | |
| return {"result": output[:8000] + "\n\n[... truncated]" if len(output) > 8000 else output} | |
| # tool name -> (required arg or None, failure message, run(provider, arg_value) -> brv result, on_ok(output) -> JSON payload) | |
| _TOOLS = { | |
| "brv_query": ("query", "Query failed", lambda p, q: p._query(q), _format_query_output), | |
| "brv_curate": ("content", "Curate failed", lambda p, c: p._curate(c), lambda _: {"result": "Memory curated successfully."}), | |
| "brv_status": (None, "Status check failed", lambda p, _: _run_brv(["status"], timeout=15, cwd=p._cwd), lambda out: {"status": out}), | |
| } | |
| def register(ctx) -> None: | |
| """Register ByteRover as a memory provider plugin.""" | |
| ctx.register_memory_provider(ByteRoverMemoryProvider()) | |