hermes-agentmemory-capture 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hermes_agentmemory_capture-0.1.0/LICENSE +21 -0
- hermes_agentmemory_capture-0.1.0/PKG-INFO +46 -0
- hermes_agentmemory_capture-0.1.0/README.md +35 -0
- hermes_agentmemory_capture-0.1.0/pyproject.toml +22 -0
- hermes_agentmemory_capture-0.1.0/setup.cfg +4 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture/__init__.py +104 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture/capture.py +108 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture.egg-info/PKG-INFO +46 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture.egg-info/SOURCES.txt +10 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture.egg-info/dependency_links.txt +1 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture.egg-info/entry_points.txt +2 -0
- hermes_agentmemory_capture-0.1.0/src/hermes_agentmemory_capture.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 herjarsa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-agentmemory-capture
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Captura automática de sesiones y herramientas de Hermes hacia AgentMemory
|
|
5
|
+
Author: herjarsa
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# hermes-agentmemory-capture
|
|
13
|
+
|
|
14
|
+
Port del plugin `agentmemory-capture` de OpenCode a Hermes: captura
|
|
15
|
+
automática de sesiones y uso de herramientas hacia AgentMemory.
|
|
16
|
+
|
|
17
|
+
Estado: **funcional (HITO 2)** — 8 hooks activos, transporte verificado
|
|
18
|
+
contra backend real. Ver `SPEC.md` para el mapeo y gaps.
|
|
19
|
+
|
|
20
|
+
## Requisitos
|
|
21
|
+
|
|
22
|
+
- Backend AgentMemory corriendo (este equipo: `http://localhost:3111` vía pm2).
|
|
23
|
+
- Env vars (sin hardcodear secretos):
|
|
24
|
+
- `AGENTMEMORY_URL` (ej. `http://localhost:3111`)
|
|
25
|
+
- `AGENTMEMORY_SECRET`
|
|
26
|
+
|
|
27
|
+
## Instalación
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
hermes plugins install herjarsa/hermes-agentmemory-capture --yes-deps --enable
|
|
31
|
+
hermes plugins list # debe salir enabled
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Vía pip (PyPI)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install hermes-agentmemory-capture
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Descubierto automáticamente por Hermes vía entry point
|
|
41
|
+
`hermes_agent.plugins` (requiere reiniciar la sesión).
|
|
42
|
+
|
|
43
|
+
## Verificación
|
|
44
|
+
|
|
45
|
+
Captura de prueba → comprobar con las tools MCP `memory_*` o el viewer
|
|
46
|
+
de AgentMemory que el turno quedó registrado.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# hermes-agentmemory-capture
|
|
2
|
+
|
|
3
|
+
Port del plugin `agentmemory-capture` de OpenCode a Hermes: captura
|
|
4
|
+
automática de sesiones y uso de herramientas hacia AgentMemory.
|
|
5
|
+
|
|
6
|
+
Estado: **funcional (HITO 2)** — 8 hooks activos, transporte verificado
|
|
7
|
+
contra backend real. Ver `SPEC.md` para el mapeo y gaps.
|
|
8
|
+
|
|
9
|
+
## Requisitos
|
|
10
|
+
|
|
11
|
+
- Backend AgentMemory corriendo (este equipo: `http://localhost:3111` vía pm2).
|
|
12
|
+
- Env vars (sin hardcodear secretos):
|
|
13
|
+
- `AGENTMEMORY_URL` (ej. `http://localhost:3111`)
|
|
14
|
+
- `AGENTMEMORY_SECRET`
|
|
15
|
+
|
|
16
|
+
## Instalación
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
hermes plugins install herjarsa/hermes-agentmemory-capture --yes-deps --enable
|
|
20
|
+
hermes plugins list # debe salir enabled
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Vía pip (PyPI)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install hermes-agentmemory-capture
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Descubierto automáticamente por Hermes vía entry point
|
|
30
|
+
`hermes_agent.plugins` (requiere reiniciar la sesión).
|
|
31
|
+
|
|
32
|
+
## Verificación
|
|
33
|
+
|
|
34
|
+
Captura de prueba → comprobar con las tools MCP `memory_*` o el viewer
|
|
35
|
+
de AgentMemory que el turno quedó registrado.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hermes-agentmemory-capture"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Captura automática de sesiones y herramientas de Hermes hacia AgentMemory"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "herjarsa" }]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
12
|
+
hermes-agentmemory-capture = "hermes_agentmemory_capture"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["setuptools>=61"]
|
|
16
|
+
build-backend = "setuptools.build_meta"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
20
|
+
|
|
21
|
+
[tool.hermes]
|
|
22
|
+
plugin = true
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""hermes-agentmemory-capture: captura automática hacia AgentMemory.
|
|
2
|
+
|
|
3
|
+
Port del plugin `agentmemory-capture` de OpenCode. Suscribe hooks del
|
|
4
|
+
ciclo de Hermes y los reenvía como observaciones al backend
|
|
5
|
+
(`POST /agentmemory/observe`). Todo es fail-open: un fallo de red o del
|
|
6
|
+
backend jamás interrumpe el turno (los callbacks devuelven None y el
|
|
7
|
+
transporte traga excepciones).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from .capture import observe
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _project() -> tuple[str, str]:
|
|
21
|
+
"""(project, cwd): basename del directorio de trabajo actual."""
|
|
22
|
+
cwd = os.getcwd()
|
|
23
|
+
return os.path.basename(cwd) or cwd, cwd
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sid(kwargs: dict) -> str:
|
|
27
|
+
return str(kwargs.get("session_id") or kwargs.get("task_id") or "unknown")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def on_session_start(**kwargs) -> None:
|
|
31
|
+
project, cwd = _project()
|
|
32
|
+
observe(_sid(kwargs), "session_created",
|
|
33
|
+
{"profile": kwargs.get("profile_name"), "surface": kwargs.get("surface")},
|
|
34
|
+
project, cwd)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def on_session_end(**kwargs) -> None:
|
|
38
|
+
project, cwd = _project()
|
|
39
|
+
observe(_sid(kwargs), "session_end",
|
|
40
|
+
{"reason": kwargs.get("reason")}, project, cwd)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def pre_tool_call(tool_name: str = "", args: dict | None = None, **kwargs) -> None:
|
|
44
|
+
# IMPORTANTE: devolver None. Un dict con {"action": "block"} vetaría
|
|
45
|
+
# la herramienta; este plugin solo observa.
|
|
46
|
+
project, cwd = _project()
|
|
47
|
+
observe(_sid(kwargs), "pre_tool_call",
|
|
48
|
+
{"tool": tool_name, "args": args or {}}, project, cwd)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def post_tool_call(tool_name: str = "", args: dict | None = None, result=None,
|
|
52
|
+
status: str | None = None, duration_ms: int = 0, **kwargs) -> None:
|
|
53
|
+
project, cwd = _project()
|
|
54
|
+
ok = (status or "ok") == "ok"
|
|
55
|
+
observe(_sid(kwargs), "post_tool_use" if ok else "post_tool_failure",
|
|
56
|
+
{"tool": tool_name, "args": args or {}, "result": result,
|
|
57
|
+
"status": status or "ok", "duration_ms": duration_ms},
|
|
58
|
+
project, cwd)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def pre_llm_call(**kwargs) -> None:
|
|
62
|
+
project, cwd = _project()
|
|
63
|
+
observe(_sid(kwargs), "prompt_submit",
|
|
64
|
+
{"model": kwargs.get("model")}, project, cwd)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def post_llm_call(**kwargs) -> None:
|
|
68
|
+
project, cwd = _project()
|
|
69
|
+
observe(_sid(kwargs), "assistant_message",
|
|
70
|
+
{"model": kwargs.get("model"),
|
|
71
|
+
"stop_reason": kwargs.get("stop_reason")}, project, cwd)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def subagent_start(**kwargs) -> None:
|
|
75
|
+
project, cwd = _project()
|
|
76
|
+
observe(_sid(kwargs), "subagent_start", {"goal": kwargs.get("goal")}, project, cwd)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def subagent_stop(**kwargs) -> None:
|
|
80
|
+
project, cwd = _project()
|
|
81
|
+
observe(_sid(kwargs), "subagent_stop",
|
|
82
|
+
{"status": kwargs.get("status")}, project, cwd)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
HOOKS = {
|
|
86
|
+
"on_session_start": on_session_start,
|
|
87
|
+
"on_session_end": on_session_end,
|
|
88
|
+
"pre_tool_call": pre_tool_call,
|
|
89
|
+
"post_tool_call": post_tool_call,
|
|
90
|
+
"pre_llm_call": pre_llm_call,
|
|
91
|
+
"post_llm_call": post_llm_call,
|
|
92
|
+
"subagent_start": subagent_start,
|
|
93
|
+
"subagent_stop": subagent_stop,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def register(ctx) -> None:
|
|
98
|
+
"""Entry point del plugin — lo llama el loader de Hermes."""
|
|
99
|
+
for name, callback in HOOKS.items():
|
|
100
|
+
ctx.register_hook(name, callback)
|
|
101
|
+
logger.info("hermes-agentmemory-capture: %d hooks registrados", len(HOOKS))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = ["register", "HOOKS"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Transporte fail-open hacia el backend AgentMemory.
|
|
2
|
+
|
|
3
|
+
Replica el comportamiento de `post()` en
|
|
4
|
+
`agentmemory-capture.ts` de OpenCode: POST best-effort con timeout corto,
|
|
5
|
+
Bearer con AGENTMEMORY_SECRET, y resolución del secreto (env → ~/.agentmemory).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import urllib.request
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
TIMEOUT_S = 5
|
|
20
|
+
MAX_DATA_CHARS = 4000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def backend_url() -> str:
|
|
24
|
+
return (os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111").rstrip("/")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _usable(value: object) -> str:
|
|
28
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
29
|
+
if not text or (text.startswith("${") and text.endswith("}")):
|
|
30
|
+
return ""
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _file_secret() -> str:
|
|
35
|
+
"""Secreto guardado por el propio backend (~/.agentmemory/.env o secret)."""
|
|
36
|
+
home = Path.home() / ".agentmemory"
|
|
37
|
+
try:
|
|
38
|
+
for line in (home / ".env").read_text(encoding="utf-8").splitlines():
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
41
|
+
continue
|
|
42
|
+
key, _, value = line.partition("=")
|
|
43
|
+
if key.removeprefix("export ").strip() != "AGENTMEMORY_SECRET":
|
|
44
|
+
continue
|
|
45
|
+
value = value.strip()
|
|
46
|
+
if value[:1] in ("'", '"'):
|
|
47
|
+
end = value.find(value[0], 1)
|
|
48
|
+
value = value[1:end] if end > 0 else value[1:]
|
|
49
|
+
found = _usable(value)
|
|
50
|
+
if found:
|
|
51
|
+
return found
|
|
52
|
+
except OSError:
|
|
53
|
+
pass
|
|
54
|
+
try:
|
|
55
|
+
return _usable((home / "secret").read_text(encoding="utf-8"))
|
|
56
|
+
except OSError:
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_loopback(url: str) -> bool:
|
|
61
|
+
return "localhost" in url or "127.0.0.1" in url or "::1" in url
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_secret() -> str:
|
|
65
|
+
configured = _usable(os.environ.get("AGENTMEMORY_SECRET", ""))
|
|
66
|
+
if configured:
|
|
67
|
+
return configured
|
|
68
|
+
if _is_loopback(backend_url()):
|
|
69
|
+
return _file_secret()
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _trim(value: object) -> object:
|
|
74
|
+
if isinstance(value, str) and len(value) > MAX_DATA_CHARS:
|
|
75
|
+
return value[:MAX_DATA_CHARS] + "…[truncado]"
|
|
76
|
+
if isinstance(value, dict):
|
|
77
|
+
return {k: _trim(v) for k, v in value.items()}
|
|
78
|
+
if isinstance(value, (list, tuple)):
|
|
79
|
+
return [_trim(v) for v in value]
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def observe(session_id: str, hook_type: str, data: dict, project: str = "", cwd: str = "") -> bool:
|
|
84
|
+
"""Envía una observación al backend. Nunca lanza: fail-open por diseño."""
|
|
85
|
+
try:
|
|
86
|
+
secret = resolve_secret()
|
|
87
|
+
body = json.dumps({
|
|
88
|
+
"hookType": hook_type,
|
|
89
|
+
"sessionId": session_id or "unknown",
|
|
90
|
+
"project": project,
|
|
91
|
+
"cwd": cwd,
|
|
92
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
93
|
+
"data": _trim(data),
|
|
94
|
+
}).encode("utf-8")
|
|
95
|
+
headers = {"Content-Type": "application/json"}
|
|
96
|
+
if secret:
|
|
97
|
+
headers["Authorization"] = f"Bearer {secret}"
|
|
98
|
+
request = urllib.request.Request(
|
|
99
|
+
f"{backend_url()}/agentmemory/observe",
|
|
100
|
+
data=body,
|
|
101
|
+
headers=headers,
|
|
102
|
+
method="POST",
|
|
103
|
+
)
|
|
104
|
+
with urllib.request.urlopen(request, timeout=TIMEOUT_S) as response:
|
|
105
|
+
return 200 <= response.status < 300
|
|
106
|
+
except Exception as exc: # noqa: BLE001 — un hook jamás debe romper el turno
|
|
107
|
+
logger.debug("agentmemory-capture: POST /observe falló: %s", exc)
|
|
108
|
+
return False
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-agentmemory-capture
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Captura automática de sesiones y herramientas de Hermes hacia AgentMemory
|
|
5
|
+
Author: herjarsa
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# hermes-agentmemory-capture
|
|
13
|
+
|
|
14
|
+
Port del plugin `agentmemory-capture` de OpenCode a Hermes: captura
|
|
15
|
+
automática de sesiones y uso de herramientas hacia AgentMemory.
|
|
16
|
+
|
|
17
|
+
Estado: **funcional (HITO 2)** — 8 hooks activos, transporte verificado
|
|
18
|
+
contra backend real. Ver `SPEC.md` para el mapeo y gaps.
|
|
19
|
+
|
|
20
|
+
## Requisitos
|
|
21
|
+
|
|
22
|
+
- Backend AgentMemory corriendo (este equipo: `http://localhost:3111` vía pm2).
|
|
23
|
+
- Env vars (sin hardcodear secretos):
|
|
24
|
+
- `AGENTMEMORY_URL` (ej. `http://localhost:3111`)
|
|
25
|
+
- `AGENTMEMORY_SECRET`
|
|
26
|
+
|
|
27
|
+
## Instalación
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
hermes plugins install herjarsa/hermes-agentmemory-capture --yes-deps --enable
|
|
31
|
+
hermes plugins list # debe salir enabled
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Vía pip (PyPI)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install hermes-agentmemory-capture
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Descubierto automáticamente por Hermes vía entry point
|
|
41
|
+
`hermes_agent.plugins` (requiere reiniciar la sesión).
|
|
42
|
+
|
|
43
|
+
## Verificación
|
|
44
|
+
|
|
45
|
+
Captura de prueba → comprobar con las tools MCP `memory_*` o el viewer
|
|
46
|
+
de AgentMemory que el turno quedó registrado.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/hermes_agentmemory_capture/__init__.py
|
|
5
|
+
src/hermes_agentmemory_capture/capture.py
|
|
6
|
+
src/hermes_agentmemory_capture.egg-info/PKG-INFO
|
|
7
|
+
src/hermes_agentmemory_capture.egg-info/SOURCES.txt
|
|
8
|
+
src/hermes_agentmemory_capture.egg-info/dependency_links.txt
|
|
9
|
+
src/hermes_agentmemory_capture.egg-info/entry_points.txt
|
|
10
|
+
src/hermes_agentmemory_capture.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hermes_agentmemory_capture
|