Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d459868776 | ||
|
|
0401176c7a | ||
|
|
f31c950182 | ||
|
|
ffb53767bf |
@@ -313,6 +313,25 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
def _resolve_prefill_messages_file(config: Dict[str, Any]) -> str:
|
||||
"""Resolve the prefill file path from env/config.
|
||||
|
||||
``prefill_messages_file`` at the top level is the canonical config key.
|
||||
``agent.prefill_messages_file`` remains a legacy fallback for older CLI and
|
||||
godmode-generated configs.
|
||||
"""
|
||||
env_path = os.getenv("HERMES_PREFILL_MESSAGES_FILE", "").strip()
|
||||
if env_path:
|
||||
return env_path
|
||||
top_level = str(config.get("prefill_messages_file", "") or "").strip()
|
||||
if top_level:
|
||||
return top_level
|
||||
agent_cfg = config.get("agent", {})
|
||||
if isinstance(agent_cfg, dict):
|
||||
return str(agent_cfg.get("prefill_messages_file", "") or "").strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _parse_reasoning_config(effort: str) -> dict | None:
|
||||
"""Parse a reasoning effort level into an OpenRouter reasoning config dict."""
|
||||
from hermes_constants import parse_reasoning_effort
|
||||
@@ -3272,7 +3291,7 @@ class HermesCLI:
|
||||
|
||||
# Ephemeral prefill messages (few-shot priming, never persisted)
|
||||
self.prefill_messages = _load_prefill_messages(
|
||||
CLI_CONFIG["agent"].get("prefill_messages_file", "")
|
||||
_resolve_prefill_messages_file(CLI_CONFIG)
|
||||
)
|
||||
|
||||
# Reasoning config (OpenRouter reasoning effort level)
|
||||
|
||||
+9
-2
@@ -1551,9 +1551,16 @@ def _run_job_impl(job: dict) -> tuple[bool, str, str, Optional[str]]:
|
||||
effort = str(_cfg.get("agent", {}).get("reasoning_effort", "")).strip()
|
||||
reasoning_config = parse_reasoning_effort(effort)
|
||||
|
||||
# Prefill messages from env or config.yaml
|
||||
# Prefill messages from env or config.yaml. The top-level
|
||||
# prefill_messages_file key is canonical; agent.prefill_messages_file is
|
||||
# retained as a legacy fallback for older CLI/godmode configs.
|
||||
prefill_messages = None
|
||||
prefill_file = os.getenv("HERMES_PREFILL_MESSAGES_FILE", "") or _cfg.get("prefill_messages_file", "")
|
||||
agent_cfg = _cfg.get("agent", {}) if isinstance(_cfg.get("agent", {}), dict) else {}
|
||||
prefill_file = (
|
||||
os.getenv("HERMES_PREFILL_MESSAGES_FILE", "")
|
||||
or _cfg.get("prefill_messages_file", "")
|
||||
or agent_cfg.get("prefill_messages_file", "")
|
||||
)
|
||||
if prefill_file:
|
||||
pfpath = Path(prefill_file).expanduser()
|
||||
if not pfpath.is_absolute():
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# Why IDE-Embedded Coding Agents Feel Better — and Where Hermes Stands
|
||||
|
||||
A study of the *harness* techniques that make in-editor coding agents punch above
|
||||
the raw model, and an honest map of which ones Hermes already implements, which it
|
||||
approximates, and which it lacks.
|
||||
|
||||
## TL;DR
|
||||
|
||||
The leading IDE-embedded coding products are **not better models** — they call the
|
||||
same frontier models (Claude, GPT) that power terminal agents like this one. Their
|
||||
edge comes entirely from the **harness**: how context is retrieved and assembled,
|
||||
how mechanical edits are applied reliably, and how cheap specialized models are
|
||||
routed in for sub-tasks. The model is a commodity; the meal it's fed is not.
|
||||
|
||||
This document breaks the advantage into five concrete subsystems, each backed by
|
||||
published engineering from the vendors, and maps each onto the Hermes codebase.
|
||||
|
||||
| # | Technique | What it buys | Hermes status |
|
||||
|---|-----------|--------------|---------------|
|
||||
| 1 | Indexed semantic retrieval (Merkle delta-sync + content-addressed embedding cache) | Knows the repo in ms; feeds the *right* snippets | ❌ **Gap** (grep/FTS5 only, no vector index) |
|
||||
| 2 | Retrieval as the accuracy driver | +~12.5% answer accuracy (vendor eval) | ⚠️ **Approximated** (lexical search, not semantic) |
|
||||
| 3 | Decoupled "apply" model + line-number-free search/replace | Frontier model only *reasons*; mechanical patching never botches the file | ✅ **Have an analog** (`tools/fuzzy_match.py`) |
|
||||
| 4 | Ambient IDE context (cursor pos, selection, live diagnostics) | More intent-signal per token | ⚠️ **Partial** (context files + LSP, no live cursor/selection) |
|
||||
| 5 | Per-task model routing (tiny model for autocomplete, frontier for reasoning) | Right tool per job | ✅ **Have** (`agent/auxiliary_client.py`) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Indexed semantic retrieval — the actual moat
|
||||
|
||||
The headline trick is *not* the system prompt. It's a vector index over the repo
|
||||
that is kept fresh cheaply:
|
||||
|
||||
- **Merkle tree for change detection.** Every file is SHA-256 hashed; folder hashes
|
||||
derive from children; the root summarizes the repo. An edit changes only that
|
||||
file's hash plus the path to the root, so the indexer walks **only the branches
|
||||
that differ** instead of rescanning. (This is git's own content-addressing trick
|
||||
repurposed for indexing.)
|
||||
- **Syntax-aware chunking.** Changed files are split on function/class boundaries,
|
||||
not arbitrary token windows, then embedded.
|
||||
- **Content-addressed embedding cache.** Embeddings are keyed by the hash of the
|
||||
chunk content. Re-indexing unchanged code is a cache hit → zero embedding cost.
|
||||
Embedding is the expensive step, so this is the whole ballgame for speed.
|
||||
- **Cross-clone index reuse.** Vendors observe that clones of one repo are ~92%
|
||||
identical across an org; a "simhash" lets a new clone reuse a teammate's index,
|
||||
collapsing time-to-first-query from hours (99th pct) to seconds. Access is gated
|
||||
cryptographically: you can only compute a Merkle node's hash if you actually hold
|
||||
the file, so results you can't *prove* you possess are dropped.
|
||||
|
||||
**Hermes status: this is the real gap.** Core Hermes has no vector index, no
|
||||
embedding store, no Merkle delta-sync. Codebase awareness is achieved at task time
|
||||
via lexical tools (`search_files` → ripgrep) and session recall via SQLite FTS5
|
||||
(`hermes_state.py`, `tools/session_search_tool.py`). The only embedding-flavored
|
||||
retrieval lives in an optional plugin (`plugins/memory/holographic/`), and even
|
||||
that is FTS5-backed, not dense-vector.
|
||||
|
||||
This is a *defensible* design choice for a terminal-first agent — ripgrep over a
|
||||
known working tree is fast, dependency-free, and always current — but it means
|
||||
Hermes "discovers" a codebase cold each task rather than walking in pre-indexed.
|
||||
|
||||
## 2. Retrieval is the accuracy driver (empirically)
|
||||
|
||||
Vendor evals attribute **~+12.5% answer accuracy** and higher edit-retention to
|
||||
semantic search alone — same model, better-retrieved context. This is the
|
||||
empirical proof of the thesis: *a worse model with better context beats a better
|
||||
model with worse context.*
|
||||
|
||||
**Hermes status: approximated, lexically.** Hermes gets the *shape* of this through
|
||||
aggressive context assembly — `agent/prompt_builder.py` and `agent/system_prompt.py`
|
||||
inject project context files (AGENTS.md / CLAUDE.md / .cursorrules), and
|
||||
`agent/subdirectory_hints.py` surfaces local structure. What's missing is *ranked
|
||||
semantic* retrieval: Hermes finds text by pattern, not by meaning. For
|
||||
"where is the thing that does X" questions, lexical search is strictly weaker than
|
||||
embeddings.
|
||||
|
||||
## 3. Decoupled apply model — the most underrated trick
|
||||
|
||||
Leading products split edits into two stages:
|
||||
|
||||
1. **Plan** — the frontier model emits a *terse* edit, often with
|
||||
`// ... existing code ...` placeholders.
|
||||
2. **Apply** — a separate, cheap, often self-hosted model turns that sketch into the
|
||||
final file.
|
||||
|
||||
Why bother? Frontier models are *lazy and inaccurate* at large rewrites: they drop
|
||||
code, emit `...`, "helpfully" reformat unrelated lines, miscount line numbers, and
|
||||
can trap the agent in retry loops. Three published findings drive the design:
|
||||
|
||||
- **Whole-file rewrites beat diffs** for the model, because diffs force fewer output
|
||||
tokens (less room to "think"), are out-of-distribution (models saw far more whole
|
||||
files in training), and line numbers are tokenizer poison (a number is one token,
|
||||
forcing a one-shot commit, and models can't count lines).
|
||||
- So edits use **search/replace blocks with no line numbers**, with redundant
|
||||
context lines so the parser tolerates model slips.
|
||||
- **Speculative decoding** makes apply fast (~1000 tok/s) because the unchanged file
|
||||
*is* the draft — and because that can't be built into hosted Anthropic/OpenAI
|
||||
models, vendors train and self-host their own apply model.
|
||||
|
||||
**Hermes status: it has a deterministic analog, and it's good.**
|
||||
`tools/fuzzy_match.py` implements an **8-strategy search/replace matcher** (exact →
|
||||
line-trimmed → whitespace-normalized → indentation-flexible → escape-normalized →
|
||||
trimmed-boundary → block-anchor → context-aware-similarity) that is *precisely* the
|
||||
"tolerate model slips in line-number-free search/replace" idea — just solved with
|
||||
`difflib.SequenceMatcher` instead of a trained model. `tools/patch_parser.py` and
|
||||
`tools/file_tools.py` wire it into the `patch` tool. Hermes also already adopts the
|
||||
correct *interface*: the model emits `old_string`/`new_string`, never line numbers.
|
||||
|
||||
Where the vendors go further: a *trained* apply model can reconstruct intent from a
|
||||
sketch (resolve `// ... existing ...` placeholders against the real file), whereas a
|
||||
fuzzy matcher can only locate-and-substitute text the model actually wrote. Hermes
|
||||
trades that capability for zero latency, zero cost, and full determinism — a sound
|
||||
trade for a local agent, but worth naming.
|
||||
|
||||
## 4. Ambient context — the editor's free advantage
|
||||
|
||||
Because the product *is* the editor, it injects for free: the open file, **cursor
|
||||
position**, current selection, **live LSP/linter diagnostics**, and recent diffs.
|
||||
Terminal agents must spend tool calls reconstructing all of this. More intent-signal
|
||||
per token → better output from the same model.
|
||||
|
||||
**Hermes status: partial.** Hermes injects project context files and has LSP
|
||||
plumbing (`agent/lsp/`), and the ACP adapter (`acp_adapter/`) gives editors a way to
|
||||
feed edits/approvals back. What it lacks is the *passive* signal: it doesn't know
|
||||
where your cursor is or what you've selected, because in a terminal there is no
|
||||
cursor to read. The ACP integration narrows this gap when Hermes runs inside an
|
||||
editor, but the default terminal surface is signal-poorer by construction.
|
||||
|
||||
## 5. Per-task model routing
|
||||
|
||||
Autocomplete uses a tiny fast model; chat uses a frontier model; apply uses the
|
||||
custom fast model; the agent loop uses a frontier model plus tools. Nothing is
|
||||
forced through one monolith.
|
||||
|
||||
**Hermes status: have it.** `agent/auxiliary_client.py` provides a routed auxiliary
|
||||
model used for cheaper sub-tasks — title generation (`agent/title_generator.py`),
|
||||
vision routing (`tools/computer_use/vision_routing.py`), background review
|
||||
(`agent/background_review.py`), and conversation compression
|
||||
(`agent/context_compressor.py`, `trajectory_compressor.py`). The pattern — reserve
|
||||
the expensive model for reasoning, route mechanical sub-tasks to a cheap one — is
|
||||
already core to Hermes.
|
||||
|
||||
---
|
||||
|
||||
## Synthesis: where Hermes wins, ties, and trails
|
||||
|
||||
**Ties or wins:**
|
||||
- **Apply reliability** — the 8-strategy fuzzy matcher is a genuinely strong,
|
||||
zero-cost analog to a trained apply model, and uses the same line-number-free
|
||||
search/replace interface the research converged on.
|
||||
- **Model routing** — auxiliary-client routing already reserves the frontier model
|
||||
for reasoning.
|
||||
- **Context-file injection & session memory** — robust, and FTS5 session search is a
|
||||
real recall capability terminal-first.
|
||||
|
||||
**Trails:**
|
||||
- **Semantic codebase retrieval** is the one structural gap. Hermes is lexical
|
||||
(ripgrep + FTS5) where the leaders are dense-vector with a cheaply-maintained
|
||||
index. This is the highest-leverage area if Hermes ever wants to close the
|
||||
"feels like it already knows my repo" gap.
|
||||
- **Ambient passive context** (cursor/selection/live diagnostics) is inherently
|
||||
weaker outside an editor; the ACP path is the right place to invest if that
|
||||
matters.
|
||||
|
||||
**The single most transferable insight:** the research independently concluded that
|
||||
it is *better to rewrite via fuzzy-tolerant, line-number-free search/replace than to
|
||||
trust the smart model to emit a precise diff* — and Hermes already lands on the same
|
||||
answer in `tools/fuzzy_match.py`. That convergence is a good sign the harness
|
||||
fundamentals here are sound; the missing piece is retrieval, not editing.
|
||||
|
||||
## Suggested follow-ups (not implemented here — analysis only)
|
||||
|
||||
1. **Optional semantic index plugin.** A content-addressed embedding cache keyed by
|
||||
file hash, behind the existing plugin interface, would give ranked semantic
|
||||
retrieval without bloating the core terminal path. Merkle delta-sync keeps it
|
||||
cheap to refresh.
|
||||
2. **Apply-from-sketch mode.** Let the model emit `// ... existing ...` placeholders
|
||||
and resolve them against the real file before handing to the fuzzy matcher —
|
||||
captures most of a trained apply model's benefit deterministically.
|
||||
3. **Richer ambient context over ACP.** Pipe editor cursor/selection/diagnostics
|
||||
into prompt assembly when running embedded, closing the passive-signal gap.
|
||||
+4
-1
@@ -3034,13 +3034,16 @@ class GatewayRunner:
|
||||
"""Load ephemeral prefill messages from config or env var.
|
||||
|
||||
Checks HERMES_PREFILL_MESSAGES_FILE env var first, then falls back to
|
||||
the prefill_messages_file key in ~/.hermes/config.yaml.
|
||||
the top-level prefill_messages_file key in ~/.hermes/config.yaml.
|
||||
agent.prefill_messages_file is accepted as a legacy fallback.
|
||||
Relative paths are resolved from ~/.hermes/.
|
||||
"""
|
||||
file_path = os.getenv("HERMES_PREFILL_MESSAGES_FILE", "")
|
||||
if not file_path:
|
||||
cfg = _load_gateway_runtime_config()
|
||||
file_path = str(cfg.get("prefill_messages_file", "") or "")
|
||||
if not file_path:
|
||||
file_path = str(cfg_get(cfg, "agent", "prefill_messages_file", default="") or "")
|
||||
if not file_path:
|
||||
return []
|
||||
path = Path(file_path).expanduser()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Supermemory Memory Provider
|
||||
|
||||
Semantic long-term memory with profile recall, semantic search, explicit memory tools, and session-end conversation ingest.
|
||||
Semantic long-term memory with profile recall, semantic search, explicit memory tools, and full-session conversation ingest (one ingest per session) for richer profiles.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -45,22 +45,34 @@ Config file: `$HERMES_HOME/supermemory.json`
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `supermemory_store` | Store an explicit memory |
|
||||
| `supermemory_search` | Search memories by semantic similarity |
|
||||
| `supermemory_forget` | Forget a memory by ID or best-match query |
|
||||
| `supermemory_profile` | Retrieve persistent profile and recent context |
|
||||
Kebab-case names are registered for the agent; snake_case aliases remain supported.
|
||||
|
||||
| Tool | Alias | Description |
|
||||
|------|-------|-------------|
|
||||
| `supermemory-save` | `supermemory_store` | Store an explicit memory |
|
||||
| `supermemory-search` | `supermemory_search` | Search memories by semantic similarity |
|
||||
| `supermemory-forget` | `supermemory_forget` | Forget a memory by ID or best-match query |
|
||||
| `supermemory-profile` | `supermemory_profile` | Retrieve persistent profile and recent context |
|
||||
|
||||
## Source attribution
|
||||
|
||||
All Supermemory API calls send `x-sm-source: hermes`, and document writes stamp
|
||||
`metadata.sm_source: hermes`. This is a **functional routing key, not telemetry**:
|
||||
it groups Hermes-written memories into a dedicated "Hermes" Space in the
|
||||
Supermemory app, so you can filter, browse, and bulk-manage them per source agent
|
||||
(alongside Codex, Claude Code, etc.) from the Supermemory UI.
|
||||
|
||||
## Behavior
|
||||
|
||||
When enabled, Hermes can:
|
||||
|
||||
- prefetch relevant memory context before each turn
|
||||
- store cleaned conversation turns after each completed response
|
||||
- ingest the full session on session end for richer graph updates
|
||||
- buffer the full conversation and ingest it as **one session** at session end (or on `/reset`, branch, compression, or shutdown)
|
||||
- ingest the full session to the conversations endpoint for richer profile/graph updates
|
||||
- expose explicit tools for search, store, forget, and profile access
|
||||
|
||||
The session is written once via the conversations endpoint, which drives Supermemory's entity extraction and profile building while keeping a clean, retrievable full transcript.
|
||||
|
||||
## Profile-Scoped Containers
|
||||
|
||||
Use `{identity}` in the `container_tag` to scope memories per Hermes profile:
|
||||
@@ -87,7 +99,7 @@ For advanced setups (e.g. OpenClaw-style multi-workspace), you can enable custom
|
||||
```
|
||||
|
||||
When enabled:
|
||||
- `supermemory_search`, `supermemory_store`, `supermemory_forget`, and `supermemory_profile` accept an optional `container_tag` parameter
|
||||
- `supermemory-search`, `supermemory-save`, `supermemory-forget`, and `supermemory-profile` accept an optional `container_tag` parameter
|
||||
- The tag must be in the whitelist: primary container + `custom_containers`
|
||||
- Automatic operations (turn sync, prefetch, memory write mirroring, session ingest) always use the **primary** container only
|
||||
- Custom container instructions are injected into the system prompt
|
||||
|
||||
@@ -269,7 +269,22 @@ class _SupermemoryClient:
|
||||
self._container_tag = container_tag
|
||||
self._search_mode = search_mode if search_mode in _VALID_SEARCH_MODES else _DEFAULT_SEARCH_MODE
|
||||
self._timeout = timeout
|
||||
self._client = Supermemory(api_key=api_key, timeout=timeout, max_retries=0)
|
||||
self._client = Supermemory(
|
||||
api_key=api_key,
|
||||
timeout=timeout,
|
||||
max_retries=0,
|
||||
default_headers={"x-sm-source": "hermes"},
|
||||
)
|
||||
|
||||
def _merge_metadata(self, metadata: Optional[dict]) -> dict:
|
||||
# sm_source routes Hermes writes into the "Hermes" Space in the Supermemory
|
||||
# app so the user can filter / bulk-manage them per source agent. This is a
|
||||
# functional routing key for the user, not vendor telemetry.
|
||||
merged = {"sm_source": "hermes", **(metadata or {})}
|
||||
legacy_source = merged.pop("source", None)
|
||||
if legacy_source and "type" not in merged:
|
||||
merged["type"] = str(legacy_source)
|
||||
return merged
|
||||
|
||||
def add_memory(self, content: str, metadata: Optional[dict] = None, *,
|
||||
entity_context: str = "", container_tag: Optional[str] = None,
|
||||
@@ -280,7 +295,7 @@ class _SupermemoryClient:
|
||||
"container_tags": [tag],
|
||||
}
|
||||
if metadata:
|
||||
kwargs["metadata"] = metadata
|
||||
kwargs["metadata"] = self._merge_metadata(metadata)
|
||||
if entity_context:
|
||||
kwargs["entity_context"] = _clamp_entity_context(entity_context)
|
||||
if custom_id:
|
||||
@@ -349,18 +364,22 @@ class _SupermemoryClient:
|
||||
preview = (target.get("memory") or "")[:100]
|
||||
return {"success": True, "message": f'Forgot: "{preview}"', "id": memory_id}
|
||||
|
||||
def ingest_conversation(self, session_id: str, messages: list[dict]) -> None:
|
||||
payload = json.dumps({
|
||||
def ingest_conversation(self, session_id: str, messages: list[dict], metadata: dict | None = None) -> None:
|
||||
payload: dict = {
|
||||
"conversationId": session_id,
|
||||
"messages": messages,
|
||||
"containerTags": [self._container_tag],
|
||||
}).encode("utf-8")
|
||||
}
|
||||
if metadata:
|
||||
payload["metadata"] = self._merge_metadata(metadata)
|
||||
|
||||
req = urllib.request.Request(
|
||||
_CONVERSATIONS_URL,
|
||||
data=payload,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"x-sm-source": "hermes",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
@@ -447,6 +466,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
self._custom_containers: List[str] = []
|
||||
self._custom_container_instructions = ""
|
||||
self._allowed_containers: List[str] = []
|
||||
self._session_turns: List[Dict[str, str]] = []
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -501,13 +521,13 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
self._search_mode = self._config["search_mode"]
|
||||
self._entity_context = self._config["entity_context"]
|
||||
self._api_timeout = self._config["api_timeout"]
|
||||
|
||||
# Multi-container setup
|
||||
self._enable_custom_containers = self._config["enable_custom_container_tags"]
|
||||
self._custom_containers = self._config["custom_containers"]
|
||||
self._custom_container_instructions = self._config["custom_container_instructions"]
|
||||
self._allowed_containers = [self._container_tag] + list(self._custom_containers)
|
||||
|
||||
self._session_turns = []
|
||||
|
||||
agent_context = kwargs.get("agent_context", "")
|
||||
self._write_enabled = agent_context not in {"cron", "flush", "subagent"}
|
||||
self._active = bool(self._api_key)
|
||||
@@ -534,7 +554,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
lines = [
|
||||
"# Supermemory",
|
||||
f"Active. Container: {self._container_tag}.",
|
||||
"Use supermemory_search, supermemory_store, supermemory_forget, and supermemory_profile for explicit memory operations.",
|
||||
"Use supermemory-search, supermemory-save, supermemory-forget, and supermemory-profile (aliases: supermemory_search, supermemory_store, supermemory_forget, supermemory_profile).",
|
||||
]
|
||||
if self._enable_custom_containers and self._custom_containers:
|
||||
tags_str = ", ".join(self._allowed_containers)
|
||||
@@ -567,31 +587,11 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
|
||||
clean_user = _clean_text_for_capture(user_content)
|
||||
clean_assistant = _clean_text_for_capture(assistant_content)
|
||||
if not clean_user or not clean_assistant:
|
||||
if not clean_user and not clean_assistant:
|
||||
return
|
||||
if self._capture_mode == "all":
|
||||
if len(clean_user) < _MIN_CAPTURE_LENGTH or len(clean_assistant) < _MIN_CAPTURE_LENGTH:
|
||||
return
|
||||
if _is_trivial_message(clean_user):
|
||||
return
|
||||
|
||||
content = (
|
||||
f"[role: user]\n{clean_user}\n[user:end]\n\n"
|
||||
f"[role: assistant]\n{clean_assistant}\n[assistant:end]"
|
||||
)
|
||||
metadata = {"source": "hermes", "type": "conversation_turn"}
|
||||
|
||||
def _run():
|
||||
try:
|
||||
self._client.add_memory(content, metadata=metadata, entity_context=self._entity_context)
|
||||
except Exception:
|
||||
logger.debug("Supermemory sync_turn failed", exc_info=True)
|
||||
|
||||
if self._sync_thread and self._sync_thread.is_alive():
|
||||
self._sync_thread.join(timeout=2.0)
|
||||
self._sync_thread = None
|
||||
self._sync_thread = threading.Thread(target=_run, daemon=True, name="supermemory-sync")
|
||||
self._sync_thread.start()
|
||||
# Buffer every turn for the single full-session document written at end/switch/shutdown
|
||||
self._session_turns.append({"user": clean_user, "assistant": clean_assistant})
|
||||
|
||||
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
|
||||
if not self._active or not self._write_enabled or not self._client or not self._session_id:
|
||||
@@ -609,12 +609,68 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
if len(cleaned) == 1 and len(cleaned[0].get("content", "")) < 20:
|
||||
return
|
||||
try:
|
||||
self._client.ingest_conversation(self._session_id, cleaned)
|
||||
self._client.ingest_conversation(
|
||||
self._session_id,
|
||||
cleaned,
|
||||
metadata={
|
||||
"type": "full_session",
|
||||
"session_id": self._session_id,
|
||||
"message_count": len(cleaned),
|
||||
},
|
||||
)
|
||||
except urllib.error.HTTPError:
|
||||
logger.warning("Supermemory session ingest failed", exc_info=True)
|
||||
except Exception:
|
||||
logger.warning("Supermemory session ingest failed", exc_info=True)
|
||||
|
||||
# Clear buffer so shutdown() doesn't duplicate on normal exit
|
||||
self._session_turns = []
|
||||
|
||||
def on_session_switch(
|
||||
self,
|
||||
new_session_id: str,
|
||||
*,
|
||||
parent_session_id: str = "",
|
||||
reset: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Flush any buffered turns from the old session as one document, then reset for the new session."""
|
||||
if not self._active or not self._write_enabled or not self._client:
|
||||
self._session_id = str(new_session_id or "").strip() or self._session_id
|
||||
self._session_turns = []
|
||||
return
|
||||
|
||||
old_session_id = self._session_id
|
||||
old_turns = list(self._session_turns)
|
||||
|
||||
# Flush previous session via conversations ingest (with metadata)
|
||||
if old_turns and old_session_id:
|
||||
messages: list[dict] = []
|
||||
for turn in old_turns:
|
||||
if turn.get("user"):
|
||||
messages.append({"role": "user", "content": turn["user"]})
|
||||
if turn.get("assistant"):
|
||||
messages.append({"role": "assistant", "content": turn["assistant"]})
|
||||
|
||||
try:
|
||||
self._client.ingest_conversation(
|
||||
old_session_id,
|
||||
messages,
|
||||
metadata={
|
||||
"type": "full_session",
|
||||
"session_id": old_session_id,
|
||||
"message_count": len(old_turns) * 2,
|
||||
"partial": not reset,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Supermemory session-switch ingest failed", exc_info=True)
|
||||
|
||||
# Reset for new session
|
||||
self._session_id = str(new_session_id or "").strip() or old_session_id
|
||||
self._session_turns = []
|
||||
self._turn_count = 0
|
||||
|
||||
def on_memory_write(self, action: str, target: str, content: str) -> None:
|
||||
if not self._active or not self._write_enabled or not self._client:
|
||||
return
|
||||
@@ -625,7 +681,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
try:
|
||||
self._client.add_memory(
|
||||
content.strip(),
|
||||
metadata={"source": "hermes_memory", "target": target, "type": "explicit_memory"},
|
||||
metadata={"target": target, "type": "explicit_memory"},
|
||||
entity_context=self._entity_context,
|
||||
)
|
||||
except Exception:
|
||||
@@ -638,6 +694,31 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
self._write_thread.start()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
# Emergency fallback (crashes only). Buffer is cleared on normal on_session_end().
|
||||
if self._active and self._write_enabled and self._client and self._session_turns and self._session_id:
|
||||
logger.warning("Supermemory: Saving session via shutdown (session=%s, turns=%d)", self._session_id, len(self._session_turns))
|
||||
|
||||
messages: list[dict] = []
|
||||
for turn in self._session_turns:
|
||||
if turn.get("user"):
|
||||
messages.append({"role": "user", "content": turn["user"]})
|
||||
if turn.get("assistant"):
|
||||
messages.append({"role": "assistant", "content": turn["assistant"]})
|
||||
|
||||
try:
|
||||
self._client.ingest_conversation(
|
||||
self._session_id,
|
||||
messages,
|
||||
metadata={
|
||||
"type": "full_session",
|
||||
"session_id": self._session_id,
|
||||
"message_count": len(self._session_turns) * 2,
|
||||
"partial": True,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Supermemory shutdown ingest failed", exc_info=True)
|
||||
|
||||
for attr_name in ("_prefetch_thread", "_sync_thread", "_write_thread"):
|
||||
thread = getattr(self, attr_name, None)
|
||||
if thread and thread.is_alive():
|
||||
@@ -665,8 +746,25 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
return sanitized
|
||||
|
||||
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
||||
def with_kebab_aliases(schemas: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
aliases = {
|
||||
"supermemory_store": "supermemory-save",
|
||||
"supermemory_search": "supermemory-search",
|
||||
"supermemory_forget": "supermemory-forget",
|
||||
"supermemory_profile": "supermemory-profile",
|
||||
}
|
||||
expanded = list(schemas)
|
||||
for schema in schemas:
|
||||
kebab = aliases.get(schema.get("name", ""))
|
||||
if not kebab:
|
||||
continue
|
||||
copy = json.loads(json.dumps(schema))
|
||||
copy["name"] = kebab
|
||||
expanded.append(copy)
|
||||
return expanded
|
||||
|
||||
if not self._enable_custom_containers:
|
||||
return [STORE_SCHEMA, SEARCH_SCHEMA, FORGET_SCHEMA, PROFILE_SCHEMA]
|
||||
return with_kebab_aliases([STORE_SCHEMA, SEARCH_SCHEMA, FORGET_SCHEMA, PROFILE_SCHEMA])
|
||||
|
||||
# When multi-container is enabled, add optional container_tag to relevant tools
|
||||
container_param = {
|
||||
@@ -678,7 +776,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
schema = json.loads(json.dumps(base)) # deep copy
|
||||
schema["parameters"]["properties"]["container_tag"] = container_param
|
||||
schemas.append(schema)
|
||||
return schemas
|
||||
return with_kebab_aliases(schemas)
|
||||
|
||||
def _tool_store(self, args: dict) -> str:
|
||||
content = str(args.get("content") or "").strip()
|
||||
@@ -692,7 +790,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
metadata.setdefault("type", _detect_category(content))
|
||||
metadata["source"] = "hermes_tool"
|
||||
metadata.pop("source", None)
|
||||
try:
|
||||
result = self._client.add_memory(content, metadata=metadata, entity_context=self._entity_context, container_tag=tag)
|
||||
preview = content[:80] + ("..." if len(content) > 80 else "")
|
||||
@@ -777,6 +875,13 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
||||
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
||||
if not self._active or not self._client:
|
||||
return tool_error("Supermemory is not configured")
|
||||
aliases = {
|
||||
"supermemory-save": "supermemory_store",
|
||||
"supermemory-search": "supermemory_search",
|
||||
"supermemory-forget": "supermemory_forget",
|
||||
"supermemory-profile": "supermemory_profile",
|
||||
}
|
||||
tool_name = aliases.get(tool_name, tool_name)
|
||||
if tool_name == "supermemory_store":
|
||||
return self._tool_store(args)
|
||||
if tool_name == "supermemory_search":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: supermemory
|
||||
version: 1.0.0
|
||||
version: 1.0.1
|
||||
description: "Supermemory semantic long-term memory with profile recall, semantic search, explicit memory tools, and session ingest."
|
||||
pip_dependencies:
|
||||
- supermemory
|
||||
|
||||
@@ -70,6 +70,7 @@ AUTHOR_MAP = {
|
||||
"524706+Twanislas@users.noreply.github.com": "Twanislas",
|
||||
"9592417+adam91holt@users.noreply.github.com": "adam91holt",
|
||||
"kchuang1015@users.noreply.github.com": "kchuang1015",
|
||||
"maheshthedev@gmail.com": "MaheshtheDev",
|
||||
"kyssta-exe@users.noreply.github.com": "kyssta-exe",
|
||||
"45688690+fujinice@users.noreply.github.com": "fujinice",
|
||||
"276689385+carltonawong@users.noreply.github.com": "carltonawong",
|
||||
|
||||
@@ -90,7 +90,7 @@ undo_jailbreak()
|
||||
7. **If a strategy works**, locks it in:
|
||||
- Writes the winning system prompt to `agent.system_prompt` in `config.yaml`
|
||||
- Writes prefill messages to `~/.hermes/prefill.json`
|
||||
- Sets `agent.prefill_messages_file: "prefill.json"` in `config.yaml`
|
||||
- Sets `prefill_messages_file: "prefill.json"` in `config.yaml`
|
||||
8. **Reports results** — which strategy won, score, preview of compliant response
|
||||
|
||||
### Strategy order per model family:
|
||||
@@ -171,8 +171,7 @@ Create `~/.hermes/prefill.json`:
|
||||
|
||||
Then set in `~/.hermes/config.yaml`:
|
||||
```yaml
|
||||
agent:
|
||||
prefill_messages_file: "prefill.json"
|
||||
prefill_messages_file: "prefill.json"
|
||||
```
|
||||
|
||||
Prefill messages are injected at the start of every API call, after the system prompt. They are ephemeral — never saved to sessions or trajectories. The model sees them as prior conversation context, establishing a pattern of compliance.
|
||||
|
||||
@@ -397,7 +397,8 @@ def _write_config(system_prompt: str = None, prefill_file: str = None):
|
||||
cfg["agent"]["system_prompt"] = system_prompt
|
||||
|
||||
if prefill_file is not None:
|
||||
cfg["agent"]["prefill_messages_file"] = prefill_file
|
||||
cfg["prefill_messages_file"] = prefill_file
|
||||
cfg["agent"].pop("prefill_messages_file", None)
|
||||
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True,
|
||||
@@ -721,6 +722,7 @@ def undo_jailbreak(verbose=True):
|
||||
if "agent" in cfg:
|
||||
cfg["agent"].pop("system_prompt", None)
|
||||
cfg["agent"].pop("prefill_messages_file", None)
|
||||
cfg.pop("prefill_messages_file", None)
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True,
|
||||
width=120, sort_keys=False)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Regression tests for CLI prefill config key compatibility."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cli
|
||||
|
||||
|
||||
def test_resolve_prefill_messages_file_uses_top_level(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_PREFILL_MESSAGES_FILE", raising=False)
|
||||
|
||||
assert cli._resolve_prefill_messages_file(
|
||||
{
|
||||
"prefill_messages_file": "top.json",
|
||||
"agent": {"prefill_messages_file": "legacy.json"},
|
||||
}
|
||||
) == "top.json"
|
||||
|
||||
|
||||
def test_resolve_prefill_messages_file_accepts_legacy_agent_key(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_PREFILL_MESSAGES_FILE", raising=False)
|
||||
|
||||
assert cli._resolve_prefill_messages_file(
|
||||
{"agent": {"prefill_messages_file": "legacy.json"}}
|
||||
) == "legacy.json"
|
||||
|
||||
|
||||
def test_resolve_prefill_messages_file_prefers_env(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_PREFILL_MESSAGES_FILE", "env.json")
|
||||
|
||||
assert cli._resolve_prefill_messages_file(
|
||||
{
|
||||
"prefill_messages_file": "top.json",
|
||||
"agent": {"prefill_messages_file": "legacy.json"},
|
||||
}
|
||||
) == "env.json"
|
||||
@@ -1546,6 +1546,36 @@ class TestRunJobConfigEnvVarExpansion:
|
||||
"config.yaml ${VAR} was not expanded in the cron execution path."
|
||||
)
|
||||
|
||||
def test_legacy_agent_prefill_messages_file_is_loaded(self, tmp_path, monkeypatch):
|
||||
"""Cron accepts the legacy agent.prefill_messages_file fallback."""
|
||||
prefill = [{"role": "system", "content": "legacy cron prefill"}]
|
||||
(tmp_path / "prefill.json").write_text(json.dumps(prefill), encoding="utf-8")
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"agent:\n"
|
||||
" prefill_messages_file: prefill.json\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
job = {"id": "prefill-job", "name": "prefill test", "prompt": "hi"}
|
||||
fake_db = MagicMock()
|
||||
|
||||
with patch("cron.scheduler._hermes_home", tmp_path), \
|
||||
patch("cron.scheduler._resolve_origin", return_value=None), \
|
||||
patch("dotenv.load_dotenv"), \
|
||||
patch("hermes_state.SessionDB", return_value=fake_db), \
|
||||
patch("hermes_cli.runtime_provider.resolve_runtime_provider",
|
||||
return_value=self._RUNTIME), \
|
||||
patch("tools.mcp_tool.discover_mcp_tools", return_value=[]), \
|
||||
patch("run_agent.AIAgent") as mock_agent_cls:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.run_conversation.return_value = {"final_response": "ok"}
|
||||
mock_agent_cls.return_value = mock_agent
|
||||
success, _, _, error = run_job(job)
|
||||
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert mock_agent_cls.call_args.kwargs["prefill_messages"] == prefill
|
||||
|
||||
def test_fallback_model_env_ref_in_config_yaml_is_expanded(self, tmp_path, monkeypatch):
|
||||
"""${VAR} in config.yaml fallback_providers model: is expanded."""
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
|
||||
@@ -33,6 +33,29 @@ def test_load_prefill_messages_expands_env_var_path(monkeypatch, gateway_home):
|
||||
assert gateway_run.GatewayRunner._load_prefill_messages() == prefill
|
||||
|
||||
|
||||
def test_load_prefill_messages_accepts_legacy_agent_key(monkeypatch, gateway_home):
|
||||
prefill = [{"role": "system", "content": "legacy few-shot"}]
|
||||
(gateway_home / "prefill.json").write_text(json.dumps(prefill), encoding="utf-8")
|
||||
_write_config(gateway_home, "agent:\n prefill_messages_file: prefill.json\n")
|
||||
|
||||
assert gateway_run.GatewayRunner._load_prefill_messages() == prefill
|
||||
|
||||
|
||||
def test_load_prefill_messages_prefers_top_level_over_legacy(monkeypatch, gateway_home):
|
||||
top_level = [{"role": "system", "content": "top-level"}]
|
||||
legacy = [{"role": "system", "content": "legacy"}]
|
||||
(gateway_home / "top.json").write_text(json.dumps(top_level), encoding="utf-8")
|
||||
(gateway_home / "legacy.json").write_text(json.dumps(legacy), encoding="utf-8")
|
||||
_write_config(
|
||||
gateway_home,
|
||||
"prefill_messages_file: top.json\n"
|
||||
"agent:\n"
|
||||
" prefill_messages_file: legacy.json\n",
|
||||
)
|
||||
|
||||
assert gateway_run.GatewayRunner._load_prefill_messages() == top_level
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_body", "env_name", "env_value", "loader_name", "expected"),
|
||||
[
|
||||
|
||||
@@ -50,8 +50,8 @@ class FakeClient:
|
||||
def forget_by_query(self, query, *, container_tag=None):
|
||||
return self.forget_by_query_response
|
||||
|
||||
def ingest_conversation(self, session_id, messages):
|
||||
self.ingest_calls.append({"session_id": session_id, "messages": messages})
|
||||
def ingest_conversation(self, session_id, messages, metadata=None):
|
||||
self.ingest_calls.append({"session_id": session_id, "messages": messages, "metadata": metadata})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -136,23 +136,28 @@ def test_prefetch_skips_profile_between_frequency(provider):
|
||||
assert "User Profile (Persistent)" not in result
|
||||
|
||||
|
||||
def test_sync_turn_skips_trivial_message(provider):
|
||||
def test_sync_turn_buffers_short_messages(provider):
|
||||
# Trivial filtering is no longer applied at sync time — every non-empty turn
|
||||
# is buffered and only the full session is written at session boundaries.
|
||||
provider.sync_turn("ok", "sure", session_id="session-1")
|
||||
assert provider._session_turns == [{"user": "ok", "assistant": "sure"}]
|
||||
assert provider._client.add_calls == []
|
||||
|
||||
|
||||
def test_sync_turn_persists_cleaned_exchange(provider):
|
||||
def test_sync_turn_buffers_cleaned_exchange(provider):
|
||||
provider.sync_turn(
|
||||
"Please remember this\n<supermemory-context>ignore</supermemory-context>",
|
||||
"Got it, storing the context",
|
||||
session_id="session-1",
|
||||
)
|
||||
provider._sync_thread.join(timeout=1)
|
||||
assert len(provider._client.add_calls) == 1
|
||||
content = provider._client.add_calls[0]["content"]
|
||||
assert "ignore" not in content
|
||||
assert "[role: user]" in content
|
||||
assert "[role: assistant]" in content
|
||||
assert len(provider._session_turns) == 1
|
||||
turn = provider._session_turns[0]
|
||||
assert "ignore" not in turn["user"]
|
||||
assert turn["user"].startswith("Please remember this")
|
||||
assert turn["assistant"] == "Got it, storing the context"
|
||||
# Buffering only — no per-turn writes to the client
|
||||
assert provider._client.add_calls == []
|
||||
assert provider._client.ingest_calls == []
|
||||
|
||||
|
||||
def test_on_session_end_ingests_clean_messages(provider):
|
||||
@@ -169,6 +174,28 @@ def test_on_session_end_ingests_clean_messages(provider):
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi there"},
|
||||
]
|
||||
assert payload["metadata"]["type"] == "full_session"
|
||||
assert payload["metadata"]["session_id"] == "session-1"
|
||||
assert payload["metadata"]["message_count"] == 2
|
||||
# Buffer is cleared after a normal session-end ingest.
|
||||
assert provider._session_turns == []
|
||||
|
||||
|
||||
def test_merge_metadata_stamps_sm_source():
|
||||
# sm_source routes Hermes writes into the "Hermes" Space in the Supermemory
|
||||
# app (functional routing, not telemetry) — must always be present.
|
||||
from plugins.memory.supermemory import _SupermemoryClient
|
||||
|
||||
client = _SupermemoryClient.__new__(_SupermemoryClient)
|
||||
merged = client._merge_metadata({"type": "explicit_memory"})
|
||||
assert merged["sm_source"] == "hermes"
|
||||
assert merged["type"] == "explicit_memory"
|
||||
|
||||
# Legacy "source" is migrated into "type" when type is absent.
|
||||
merged2 = client._merge_metadata({"source": "conversation_turn"})
|
||||
assert merged2["sm_source"] == "hermes"
|
||||
assert merged2["type"] == "conversation_turn"
|
||||
assert "source" not in merged2
|
||||
|
||||
|
||||
def test_on_memory_write_tracks_thread(provider):
|
||||
@@ -179,7 +206,7 @@ def test_on_memory_write_tracks_thread(provider):
|
||||
assert provider._client.add_calls[0]["metadata"]["type"] == "explicit_memory"
|
||||
|
||||
|
||||
def test_shutdown_joins_and_clears_threads(provider, monkeypatch):
|
||||
def test_shutdown_joins_threads_and_flushes_buffer(provider, monkeypatch):
|
||||
started = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
@@ -196,15 +223,16 @@ def test_shutdown_joins_and_clears_threads(provider, monkeypatch):
|
||||
|
||||
monkeypatch.setattr(provider._client, "add_memory", slow_add_memory)
|
||||
|
||||
# sync_turn now only buffers — no thread is spawned.
|
||||
provider.sync_turn(
|
||||
"Please remember this request in long-term memory",
|
||||
"Absolutely, I will keep that in long-term memory.",
|
||||
session_id="session-1",
|
||||
)
|
||||
assert started.wait(timeout=1)
|
||||
assert provider._sync_thread is not None
|
||||
assert provider._sync_thread is None
|
||||
assert len(provider._session_turns) == 1
|
||||
|
||||
started.clear()
|
||||
# on_memory_write still runs on a background thread.
|
||||
provider.on_memory_write("add", "memory", "Jordan likes concise docs")
|
||||
assert started.wait(timeout=1)
|
||||
assert provider._write_thread is not None
|
||||
@@ -212,10 +240,18 @@ def test_shutdown_joins_and_clears_threads(provider, monkeypatch):
|
||||
release.set()
|
||||
provider.shutdown()
|
||||
|
||||
# All tracked threads joined and cleared.
|
||||
assert provider._sync_thread is None
|
||||
assert provider._write_thread is None
|
||||
assert provider._prefetch_thread is None
|
||||
assert len(provider._client.add_calls) == 2
|
||||
# Explicit memory write went through.
|
||||
assert len(provider._client.add_calls) == 1
|
||||
# Buffered turn was flushed as a partial full-session ingest.
|
||||
assert len(provider._client.ingest_calls) == 1
|
||||
payload = provider._client.ingest_calls[0]
|
||||
assert payload["session_id"] == "session-1"
|
||||
assert payload["metadata"]["partial"] is True
|
||||
assert payload["metadata"]["type"] == "full_session"
|
||||
|
||||
|
||||
def test_store_tool_returns_saved_payload(provider):
|
||||
|
||||
@@ -66,7 +66,7 @@ AI-native cross-session user modeling with dialectic reasoning, session-scoped c
|
||||
hermes memory setup # select "honcho" — runs the Honcho-specific post-setup
|
||||
```
|
||||
|
||||
On a fresh install, configure Honcho directly with `hermes memory setup honcho`. The legacy `hermes honcho setup` command still works (it now redirects to `hermes memory setup`), but is only registered after Honcho is selected as the active memory provider.
|
||||
The legacy `hermes honcho setup` command still works (it now redirects to `hermes memory setup`), but is only registered after Honcho is selected as the active memory provider.
|
||||
|
||||
**Config:** `$HERMES_HOME/honcho.json` (profile-local) or `~/.honcho/config.json` (global). Resolution order: `$HERMES_HOME/honcho.json` > `~/.hermes/honcho.json` > `~/.honcho/config.json`. See the [config reference](https://github.com/NousResearch/hermes-agent/blob/main/plugins/memory/honcho/README.md) and the [Honcho integration guide](https://docs.honcho.dev/v3/guides/integrations/hermes).
|
||||
|
||||
@@ -498,11 +498,11 @@ echo 'SUPERMEMORY_API_KEY=***' >> ~/.hermes/.env
|
||||
|
||||
**Key features:**
|
||||
- Automatic context fencing — strips recalled memories from captured turns to prevent recursive memory pollution
|
||||
- Session-end conversation ingest for richer graph-level knowledge building
|
||||
- Full-session ingest — the entire conversation is sent once at session boundaries
|
||||
- Session-end conversation ingest (to `/v4/conversations`) for richer profile + graph building in Supermemory
|
||||
- Profile facts injected on first turn and at configurable intervals
|
||||
- Trivial message filtering (skips "ok", "thanks", etc.)
|
||||
- **Profile-scoped containers** — use `{identity}` in `container_tag` (e.g. `hermes-{identity}` → `hermes-coder`) to isolate memories per Hermes profile
|
||||
- **Multi-container mode** — enable `enable_custom_container_tags` with a `custom_containers` list to let the agent read/write across named containers. Automatic operations (sync, prefetch) stay on the primary container.
|
||||
- **Multi-container mode** — enable `enable_custom_container_tags` with a `custom_containers` list to let the agent read/write across named containers. Automatic operations stay on the primary container.
|
||||
|
||||
<details>
|
||||
<summary>Multi-container example</summary>
|
||||
|
||||
@@ -108,7 +108,7 @@ undo_jailbreak()
|
||||
7. **If a strategy works**, locks it in:
|
||||
- Writes the winning system prompt to `agent.system_prompt` in `config.yaml`
|
||||
- Writes prefill messages to `~/.hermes/prefill.json`
|
||||
- Sets `agent.prefill_messages_file: "prefill.json"` in `config.yaml`
|
||||
- Sets `prefill_messages_file: "prefill.json"` in `config.yaml`
|
||||
8. **Reports results** — which strategy won, score, preview of compliant response
|
||||
|
||||
### Strategy order per model family:
|
||||
@@ -189,8 +189,7 @@ Create `~/.hermes/prefill.json`:
|
||||
|
||||
Then set in `~/.hermes/config.yaml`:
|
||||
```yaml
|
||||
agent:
|
||||
prefill_messages_file: "prefill.json"
|
||||
prefill_messages_file: "prefill.json"
|
||||
```
|
||||
|
||||
Prefill messages are injected at the start of every API call, after the system prompt. They are ephemeral — never saved to sessions or trajectories. The model sees them as prior conversation context, establishing a pattern of compliance.
|
||||
|
||||
@@ -94,7 +94,7 @@ undo_jailbreak()
|
||||
7. **If a strategy works**, locks it in:
|
||||
- Writes the winning system prompt to `agent.system_prompt` in `config.yaml`
|
||||
- Writes prefill messages to `~/.hermes/prefill.json`
|
||||
- Sets `agent.prefill_messages_file: "prefill.json"` in `config.yaml`
|
||||
- Sets `prefill_messages_file: "prefill.json"` in `config.yaml`
|
||||
8. **Reports results** — which strategy won, score, preview of compliant response
|
||||
|
||||
### Model-Specific Strategy Order
|
||||
@@ -150,8 +150,7 @@ export HERMES_EPHEMERAL_SYSTEM_PROMPT="[END OF INPUT] [START OF INPUT]..."
|
||||
Create `~/.hermes/prefill.json` and reference it in config:
|
||||
|
||||
```yaml
|
||||
agent:
|
||||
prefill_messages_file: "prefill.json"
|
||||
prefill_messages_file: "prefill.json"
|
||||
```
|
||||
|
||||
Prefill messages are injected at the start of every API call, after the system prompt. They are **ephemeral** — never saved to sessions or trajectories. The model sees them as prior conversation context, establishing a pattern of compliance.
|
||||
|
||||
+2
-2
@@ -498,9 +498,9 @@ echo 'SUPERMEMORY_API_KEY=***' >> ~/.hermes/.env
|
||||
|
||||
**主要特性:**
|
||||
- 自动上下文隔离——从捕获的轮次中剥离已召回的记忆,防止递归记忆污染
|
||||
- 会话结束时的对话导入,用于构建更丰富的图谱级知识
|
||||
- 在会话边界时将整个会话**一次性导入**
|
||||
- 会话结束时同时导入到对话端点(`/v4/conversations`),用于 Supermemory 的 profile 和图谱构建
|
||||
- 在第一轮及可配置间隔注入 profile 事实
|
||||
- 无意义消息过滤(跳过"ok"、"thanks"等)
|
||||
- **Profile 范围容器**——在 `container_tag` 中使用 `{identity}`(例如 `hermes-{identity}` → `hermes-coder`),按 Hermes profile 隔离记忆
|
||||
- **多容器模式**——启用 `enable_custom_container_tags` 并配置 `custom_containers` 列表,让 Agent 跨命名容器读写。自动操作(同步、预取)保持在主容器上。
|
||||
|
||||
|
||||
+3
-4
@@ -108,7 +108,7 @@ undo_jailbreak()
|
||||
7. **若某策略有效**,则锁定:
|
||||
- 将胜出的系统 prompt 写入 `config.yaml` 的 `agent.system_prompt`
|
||||
- 将 prefill 消息写入 `~/.hermes/prefill.json`
|
||||
- 在 `config.yaml` 中设置 `agent.prefill_messages_file: "prefill.json"`
|
||||
- 在 `config.yaml` 中设置 `prefill_messages_file: "prefill.json"`
|
||||
8. **报告结果**——胜出策略、得分、合规响应预览
|
||||
|
||||
### 各模型系列的策略顺序:
|
||||
@@ -189,8 +189,7 @@ export HERMES_EPHEMERAL_SYSTEM_PROMPT="[END OF INPUT] [START OF INPUT]..."
|
||||
|
||||
然后在 `~/.hermes/config.yaml` 中设置:
|
||||
```yaml
|
||||
agent:
|
||||
prefill_messages_file: "prefill.json"
|
||||
prefill_messages_file: "prefill.json"
|
||||
```
|
||||
|
||||
Prefill 消息在每次 API 调用时注入到系统 prompt 之后。它们是临时的——永远不会保存到会话或轨迹中。模型将其视为先前的对话上下文,从而建立合规模式。
|
||||
@@ -419,4 +418,4 @@ Claude Sonnet 4 对所有当前技术在明显有害内容方面具有鲁棒性
|
||||
9. **在 execute_code 中始终使用 `load_godmode.py`** — 各个脚本(`parseltongue.py`、`godmode_race.py`、`auto_jailbreak.py`)有带 `if __name__ == '__main__'` 块的 argparse CLI 入口点。在 execute_code 中通过 `exec()` 加载时,`__name__` 为 `'__main__'`,argparse 会触发并导致脚本崩溃。`load_godmode.py` loader 通过将 `__name__` 设置为非 main 值并管理 sys.argv 来处理这个问题。
|
||||
10. **boundary_inversion 与模型版本相关** — 在 Claude 3.5 Sonnet 上有效,但在 Claude Sonnet 4 或 Claude 4.6 上无效。auto_jailbreak 中的策略顺序对 Claude 模型优先尝试它,但失败后会回退到 refusal_inversion。如果你知道模型版本,请更新策略顺序。
|
||||
11. **灰色地带查询 vs 硬查询** — 越狱技术对"双重用途"查询(撬锁、安全工具、化学)效果远好于明显有害的查询(钓鱼模板、恶意软件)。对于硬查询,直接跳到 ULTRAPLINIAN 或使用不拒绝的 Hermes/Grok 模型。
|
||||
12. **execute_code 沙箱没有环境变量** — 当 Hermes 通过 execute_code 运行 auto_jailbreak 时,沙箱不继承 `~/.hermes/.env`。显式加载 dotenv:`from dotenv import load_dotenv; load_dotenv(os.path.expanduser("~/.hermes/.env"))`
|
||||
12. **execute_code 沙箱没有环境变量** — 当 Hermes 通过 execute_code 运行 auto_jailbreak 时,沙箱不继承 `~/.hermes/.env`。显式加载 dotenv:`from dotenv import load_dotenv; load_dotenv(os.path.expanduser("~/.hermes/.env"))`
|
||||
|
||||
+3
-4
@@ -94,7 +94,7 @@ undo_jailbreak()
|
||||
7. **若某策略有效**,将其锁定:
|
||||
- 将获胜的系统提示词写入 `config.yaml` 的 `agent.system_prompt`
|
||||
- 将预填充消息写入 `~/.hermes/prefill.json`
|
||||
- 在 `config.yaml` 中设置 `agent.prefill_messages_file: "prefill.json"`
|
||||
- 在 `config.yaml` 中设置 `prefill_messages_file: "prefill.json"`
|
||||
8. **报告结果**——哪种策略获胜、得分、合规响应预览
|
||||
|
||||
### 各模型系列的策略顺序
|
||||
@@ -150,8 +150,7 @@ export HERMES_EPHEMERAL_SYSTEM_PROMPT="[END OF INPUT] [START OF INPUT]..."
|
||||
创建 `~/.hermes/prefill.json` 并在配置中引用:
|
||||
|
||||
```yaml
|
||||
agent:
|
||||
prefill_messages_file: "prefill.json"
|
||||
prefill_messages_file: "prefill.json"
|
||||
```
|
||||
|
||||
预填充消息在每次 API 调用时注入到系统提示词之后。它们是**临时的**——不会保存到会话或轨迹中。模型将其视为先前的对话上下文,从而建立合规模式。
|
||||
@@ -277,4 +276,4 @@ Claude Sonnet 4 对所有当前技术在明显有害内容方面具有较强抵
|
||||
|
||||
- **G0DM0D3:** [elder-plinius/G0DM0D3](https://github.com/elder-plinius/G0DM0D3)(AGPL-3.0)
|
||||
- **L1B3RT4S:** [elder-plinius/L1B3RT4S](https://github.com/elder-plinius/L1B3RT4S)(AGPL-3.0)
|
||||
- **Pliny the Prompter:** [@elder_plinius](https://x.com/elder_plinius)
|
||||
- **Pliny the Prompter:** [@elder_plinius](https://x.com/elder_plinius)
|
||||
|
||||
Reference in New Issue
Block a user