Skip to content
Merged
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ On first real Hermes use, Hermes may ask you to approve the shell hook or requir
The installed hook calls:

```bash
agent-memory hermes-pre-llm-hook ~/.agent-memory/memory.db --top-k 3 --max-prompt-lines 8 --max-prompt-chars 1200 --max-prompt-tokens 300 --max-alternatives 2
agent-memory hermes-pre-llm-hook ~/.agent-memory/memory.db --top-k 1 --max-prompt-lines 6 --max-prompt-chars 800 --max-prompt-tokens 200 --max-verification-steps 1 --max-alternatives 0 --max-guidelines 1 --no-reason-codes
```

The hook receives the Hermes event JSON on stdin, retrieves relevant approved memories, and returns bounded ephemeral context for the current prompt. It does not write back to Hermes session storage.
The hook receives the Hermes event JSON on stdin, retrieves relevant approved memories, and returns bounded ephemeral context for the current prompt. It does not write back to Hermes session storage. `agent-memory bootstrap` uses the conservative Hermes preset by default: one top memory, small prompt budgets, no alternative-memory detail in the prompt, no reason-code noise, and fail-closed behavior if retrieval is unavailable.

If you only want to inspect the YAML snippet and not modify config:

Expand All @@ -105,9 +105,12 @@ agent-memory hermes-hook-config-snippet ~/.agent-memory/memory.db
If you want explicit paths and budgets:

```bash
agent-memory hermes-install-hook ~/.agent-memory/memory.db --config-path ~/.hermes/config.yaml --top-k 3 --max-prompt-lines 8 --max-prompt-chars 1200 --max-prompt-tokens 300 --max-alternatives 2 --timeout 12
agent-memory hermes-install-hook ~/.agent-memory/memory.db --config-path ~/.hermes/config.yaml --preset conservative --timeout 8
agent-memory hermes-install-hook ~/.agent-memory/memory.db --config-path ~/.hermes/config.yaml --preset balanced
```

Use `--preset balanced` if you intentionally want the older, more verbose hook shape (`--top-k 3`, larger budgets, and reason codes). Explicit flags such as `--top-k`, `--max-prompt-tokens`, or `--no-reason-codes` override the selected preset.

## Codex and Claude prompt wrappers

For harnesses that want a plain prompt prefix rather than a Hermes hook response:
Expand Down
92 changes: 75 additions & 17 deletions src/agent_memory/api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,55 @@ def _normalize_command_aliases(argv: list[str]) -> list[str]:
return [alias_map.get(argv[0], argv[0]), *argv[1:]]


HERMES_HOOK_PRESETS = {
"conservative": {
"top_k": 1,
"max_prompt_lines": 6,
"max_prompt_chars": 800,
"max_prompt_tokens": 200,
"max_verification_steps": 1,
"max_alternatives": 0,
"max_guidelines": 1,
"no_reason_codes": True,
"timeout": 8,
},
"balanced": {
"top_k": 3,
"max_prompt_lines": 8,
"max_prompt_chars": 1200,
"max_prompt_tokens": 300,
"max_verification_steps": None,
"max_alternatives": 2,
"max_guidelines": None,
"no_reason_codes": False,
"timeout": 12,
},
}


def _add_hermes_hook_preset_argument(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--preset",
choices=sorted(HERMES_HOOK_PRESETS),
default="conservative",
help="Apply a Hermes hook budget preset before explicit flag overrides.",
)


def _apply_hermes_hook_preset(args: argparse.Namespace) -> None:
preset_name = getattr(args, "preset", None)
if preset_name is None:
return
preset = HERMES_HOOK_PRESETS[preset_name]
for field, value in preset.items():
if field == "no_reason_codes":
if value:
args.no_reason_codes = True
continue
if hasattr(args, field) and getattr(args, field) is None:
setattr(args, field, value)


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="agent-memory")
subparsers = parser.add_subparsers(dest="command", required=True)
Expand Down Expand Up @@ -268,9 +317,10 @@ def _build_parser() -> argparse.ArgumentParser:

hermes_pre_llm_hook_parser = subparsers.add_parser("hermes-pre-llm-hook")
hermes_pre_llm_hook_parser.add_argument("db_path", type=Path)
_add_hermes_hook_preset_argument(hermes_pre_llm_hook_parser)
hermes_pre_llm_hook_parser.add_argument("--limit", type=int, default=5)
hermes_pre_llm_hook_parser.add_argument("--preferred-scope")
hermes_pre_llm_hook_parser.add_argument("--top-k", type=int, default=1)
hermes_pre_llm_hook_parser.add_argument("--top-k", type=int)
hermes_pre_llm_hook_parser.add_argument("--max-prompt-lines", type=int)
hermes_pre_llm_hook_parser.add_argument("--max-prompt-chars", type=int)
hermes_pre_llm_hook_parser.add_argument("--max-prompt-tokens", type=int)
Expand All @@ -281,34 +331,36 @@ def _build_parser() -> argparse.ArgumentParser:

hermes_hook_config_snippet_parser = subparsers.add_parser("hermes-hook-config-snippet")
hermes_hook_config_snippet_parser.add_argument("db_path", type=Path)
_add_hermes_hook_preset_argument(hermes_hook_config_snippet_parser)
hermes_hook_config_snippet_parser.add_argument("--python-executable")
hermes_hook_config_snippet_parser.add_argument("--limit", type=int, default=5)
hermes_hook_config_snippet_parser.add_argument("--preferred-scope")
hermes_hook_config_snippet_parser.add_argument("--top-k", type=int, default=1)
hermes_hook_config_snippet_parser.add_argument("--top-k", type=int)
hermes_hook_config_snippet_parser.add_argument("--max-prompt-lines", type=int)
hermes_hook_config_snippet_parser.add_argument("--max-prompt-chars", type=int)
hermes_hook_config_snippet_parser.add_argument("--max-prompt-tokens", type=int)
hermes_hook_config_snippet_parser.add_argument("--max-verification-steps", type=int)
hermes_hook_config_snippet_parser.add_argument("--max-alternatives", type=int)
hermes_hook_config_snippet_parser.add_argument("--max-guidelines", type=int)
hermes_hook_config_snippet_parser.add_argument("--no-reason-codes", action="store_true")
hermes_hook_config_snippet_parser.add_argument("--timeout", type=int, default=10)
hermes_hook_config_snippet_parser.add_argument("--timeout", type=int)

hermes_install_hook_parser = subparsers.add_parser("hermes-install-hook")
hermes_install_hook_parser.add_argument("db_path", type=Path)
_add_hermes_hook_preset_argument(hermes_install_hook_parser)
hermes_install_hook_parser.add_argument("--config-path", type=Path, default=Path.home() / ".hermes" / "config.yaml")
hermes_install_hook_parser.add_argument("--python-executable")
hermes_install_hook_parser.add_argument("--limit", type=int, default=5)
hermes_install_hook_parser.add_argument("--preferred-scope")
hermes_install_hook_parser.add_argument("--top-k", type=int, default=1)
hermes_install_hook_parser.add_argument("--top-k", type=int)
hermes_install_hook_parser.add_argument("--max-prompt-lines", type=int)
hermes_install_hook_parser.add_argument("--max-prompt-chars", type=int)
hermes_install_hook_parser.add_argument("--max-prompt-tokens", type=int)
hermes_install_hook_parser.add_argument("--max-verification-steps", type=int)
hermes_install_hook_parser.add_argument("--max-alternatives", type=int)
hermes_install_hook_parser.add_argument("--max-guidelines", type=int)
hermes_install_hook_parser.add_argument("--no-reason-codes", action="store_true")
hermes_install_hook_parser.add_argument("--timeout", type=int, default=10)
hermes_install_hook_parser.add_argument("--timeout", type=int)

hermes_bootstrap_parser = subparsers.add_parser(
"hermes-bootstrap",
Expand All @@ -320,19 +372,20 @@ def _build_parser() -> argparse.ArgumentParser:
nargs="?",
default=Path.home() / ".agent-memory" / "memory.db",
)
_add_hermes_hook_preset_argument(hermes_bootstrap_parser)
hermes_bootstrap_parser.add_argument("--config-path", type=Path, default=Path.home() / ".hermes" / "config.yaml")
hermes_bootstrap_parser.add_argument("--python-executable")
hermes_bootstrap_parser.add_argument("--limit", type=int, default=5)
hermes_bootstrap_parser.add_argument("--preferred-scope")
hermes_bootstrap_parser.add_argument("--top-k", type=int, default=3)
hermes_bootstrap_parser.add_argument("--max-prompt-lines", type=int, default=8)
hermes_bootstrap_parser.add_argument("--max-prompt-chars", type=int, default=1200)
hermes_bootstrap_parser.add_argument("--max-prompt-tokens", type=int, default=300)
hermes_bootstrap_parser.add_argument("--top-k", type=int)
hermes_bootstrap_parser.add_argument("--max-prompt-lines", type=int)
hermes_bootstrap_parser.add_argument("--max-prompt-chars", type=int)
hermes_bootstrap_parser.add_argument("--max-prompt-tokens", type=int)
hermes_bootstrap_parser.add_argument("--max-verification-steps", type=int)
hermes_bootstrap_parser.add_argument("--max-alternatives", type=int, default=2)
hermes_bootstrap_parser.add_argument("--max-alternatives", type=int)
hermes_bootstrap_parser.add_argument("--max-guidelines", type=int)
hermes_bootstrap_parser.add_argument("--no-reason-codes", action="store_true")
hermes_bootstrap_parser.add_argument("--timeout", type=int, default=12)
hermes_bootstrap_parser.add_argument("--timeout", type=int)

hermes_doctor_parser = subparsers.add_parser(
"hermes-doctor",
Expand All @@ -344,26 +397,28 @@ def _build_parser() -> argparse.ArgumentParser:
nargs="?",
default=Path.home() / ".agent-memory" / "memory.db",
)
_add_hermes_hook_preset_argument(hermes_doctor_parser)
hermes_doctor_parser.add_argument("--config-path", type=Path, default=Path.home() / ".hermes" / "config.yaml")
hermes_doctor_parser.add_argument("--python-executable")
hermes_doctor_parser.add_argument("--limit", type=int, default=5)
hermes_doctor_parser.add_argument("--preferred-scope")
hermes_doctor_parser.add_argument("--top-k", type=int, default=3)
hermes_doctor_parser.add_argument("--max-prompt-lines", type=int, default=8)
hermes_doctor_parser.add_argument("--max-prompt-chars", type=int, default=1200)
hermes_doctor_parser.add_argument("--max-prompt-tokens", type=int, default=300)
hermes_doctor_parser.add_argument("--top-k", type=int)
hermes_doctor_parser.add_argument("--max-prompt-lines", type=int)
hermes_doctor_parser.add_argument("--max-prompt-chars", type=int)
hermes_doctor_parser.add_argument("--max-prompt-tokens", type=int)
hermes_doctor_parser.add_argument("--max-verification-steps", type=int)
hermes_doctor_parser.add_argument("--max-alternatives", type=int, default=2)
hermes_doctor_parser.add_argument("--max-alternatives", type=int)
hermes_doctor_parser.add_argument("--max-guidelines", type=int)
hermes_doctor_parser.add_argument("--no-reason-codes", action="store_true")
hermes_doctor_parser.add_argument("--timeout", type=int, default=12)
hermes_doctor_parser.add_argument("--timeout", type=int)

return parser


def main() -> None:
parser = _build_parser()
args = parser.parse_args(_normalize_command_aliases(sys.argv[1:]))
_apply_hermes_hook_preset(args)

if args.command == "init":
initialize_database(args.db_path)
Expand Down Expand Up @@ -581,6 +636,7 @@ def main() -> None:
HermesHookConfigSnippetOptions(
db_path=args.db_path,
python_executable=args.python_executable,
render_default_arguments=True,
limit=args.limit,
preferred_scope=args.preferred_scope,
top_k=args.top_k,
Expand All @@ -604,6 +660,7 @@ def main() -> None:
snippet_options=HermesHookConfigSnippetOptions(
db_path=args.db_path,
python_executable=args.python_executable,
render_default_arguments=True,
limit=args.limit,
preferred_scope=args.preferred_scope,
top_k=args.top_k,
Expand All @@ -628,6 +685,7 @@ def main() -> None:
snippet_options=HermesHookConfigSnippetOptions(
db_path=args.db_path,
python_executable=args.python_executable,
render_default_arguments=True,
limit=args.limit,
preferred_scope=args.preferred_scope,
top_k=args.top_k,
Expand Down
3 changes: 2 additions & 1 deletion src/agent_memory/integrations/hermes_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class HermesPreLlmHookOptions(BaseModel):
class HermesHookConfigSnippetOptions(BaseModel):
db_path: Path
python_executable: str | None = None
render_default_arguments: bool = False
limit: int = 5
preferred_scope: str | None = None
top_k: int = 1
Expand Down Expand Up @@ -120,7 +121,7 @@ def build_hermes_hook_config_snippet(options: HermesHookConfigSnippetOptions) ->
argv.extend(["--limit", str(options.limit)])
if options.preferred_scope:
argv.extend(["--preferred-scope", options.preferred_scope])
if options.top_k != 1:
if options.render_default_arguments or options.top_k != 1:
argv.extend(["--top-k", str(options.top_k)])
if options.max_prompt_lines is not None:
argv.extend(["--max-prompt-lines", str(options.max_prompt_lines)])
Expand Down
56 changes: 48 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ def test_python_module_cli_hermes_install_hook_reports_when_database_already_exi



def test_python_module_cli_hermes_bootstrap_defaults_to_user_paths_and_recommended_budgets(tmp_path: Path) -> None:
def test_python_module_cli_hermes_bootstrap_defaults_to_user_paths_and_conservative_preset(tmp_path: Path) -> None:
env = {**os.environ, "PYTHONPATH": "src", "HOME": str(tmp_path)}
default_db_path = tmp_path / ".agent-memory" / "memory.db"
default_config_path = tmp_path / ".hermes" / "config.yaml"
Expand All @@ -941,12 +941,46 @@ def test_python_module_cli_hermes_bootstrap_defaults_to_user_paths_and_recommend
config_text = default_config_path.read_text()
assert "hermes-pre-llm-hook" in config_text
assert str(default_db_path) in config_text
assert "--top-k 3" in config_text
assert "--max-prompt-lines 8" in config_text
assert "--max-prompt-chars 1200" in config_text
assert "--max-prompt-tokens 300" in config_text
assert "--max-alternatives 2" in config_text
assert "timeout: 12" in config_text
assert "--top-k 1" in config_text
assert "--max-prompt-lines 6" in config_text
assert "--max-prompt-chars 800" in config_text
assert "--max-prompt-tokens 200" in config_text
assert "--max-verification-steps 1" in config_text
assert "--max-alternatives 0" in config_text
assert "--max-guidelines 1" in config_text
assert "--no-reason-codes" in config_text
assert "timeout: 8" in config_text


def test_python_module_cli_hermes_hook_config_snippet_can_use_balanced_preset(tmp_path: Path) -> None:
db_path = tmp_path / "balanced-snippet-memory.db"
env = {**os.environ, "PYTHONPATH": "src"}

result = subprocess.run(
[
sys.executable,
"-m",
"agent_memory.api.cli",
"hermes-hook-config-snippet",
str(db_path),
"--preset",
"balanced",
],
cwd=Path(__file__).resolve().parents[1],
env=env,
capture_output=True,
text=True,
)

assert result.returncode == 0, result.stderr
snippet = result.stdout
assert "--top-k 3" in snippet
assert "--max-prompt-lines 8" in snippet
assert "--max-prompt-chars 1200" in snippet
assert "--max-prompt-tokens 300" in snippet
assert "--max-alternatives 2" in snippet
assert "--no-reason-codes" not in snippet
assert "timeout: 12" in snippet



Expand Down Expand Up @@ -1247,7 +1281,13 @@ def test_python_module_cli_hermes_hook_config_snippet_defaults_to_installed_agen
assert "agent-memory hermes-pre-llm-hook" in snippet
assert sys.executable not in snippet
assert "agent_memory.api.cli" not in snippet
assert "timeout: 10" in snippet
assert "--top-k 1" in snippet
assert "--max-prompt-lines 6" in snippet
assert "--max-prompt-chars 800" in snippet
assert "--max-prompt-tokens 200" in snippet
assert "--max-alternatives 0" in snippet
assert "--no-reason-codes" in snippet
assert "timeout: 8" in snippet



Expand Down