@signetai/connector-hermes-agent 0.140.1
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.
- package/bin/install.js +5 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21626 -0
- package/hermes-plugin/README.md +64 -0
- package/hermes-plugin/__init__.py +1053 -0
- package/hermes-plugin/client.py +571 -0
- package/hermes-plugin/plugin.yaml +16 -0
- package/package.json +59 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""Signet daemon HTTP client.
|
|
2
|
+
|
|
3
|
+
Communicates with the Signet daemon on localhost:3850 (default) for
|
|
4
|
+
memory operations: search, store, hooks, and session lifecycle.
|
|
5
|
+
|
|
6
|
+
Configuration resolution:
|
|
7
|
+
1. SIGNET_HOST + SIGNET_PORT env vars
|
|
8
|
+
2. SIGNET_DAEMON_URL env var (full URL override)
|
|
9
|
+
3. Default: http://localhost:3850
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import ipaddress
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import urllib.request
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_DEFAULT_HOST = "localhost"
|
|
26
|
+
_DEFAULT_PORT = 3850
|
|
27
|
+
_TIMEOUT_SECS = 5
|
|
28
|
+
_LONG_TIMEOUT_SECS = 15
|
|
29
|
+
_RECALL_TIMEOUT_SECS = 30
|
|
30
|
+
_TRUSTED_ORIGINS_ENV = "SIGNET_TRUSTED_DAEMON_ORIGINS"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sanitize(value: str) -> str:
|
|
34
|
+
"""Strip leading/trailing whitespace and embedded newlines from env values."""
|
|
35
|
+
return value.strip().replace("\r", "").replace("\n", "")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_base_url(raw: str, source: str) -> str:
|
|
39
|
+
"""Normalize a daemon URL to an origin string."""
|
|
40
|
+
parsed = urllib.parse.urlparse(raw)
|
|
41
|
+
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
42
|
+
raise ValueError(f"{source} must be an http(s) URL")
|
|
43
|
+
if parsed.username or parsed.password:
|
|
44
|
+
raise ValueError(f"{source} must not include username or password")
|
|
45
|
+
if parsed.query or parsed.fragment:
|
|
46
|
+
raise ValueError(f"{source} must not include query strings or fragments")
|
|
47
|
+
if parsed.path not in ("", "/"):
|
|
48
|
+
raise ValueError(f"{source} must point at the daemon origin, not a path")
|
|
49
|
+
return urllib.parse.urlunparse((parsed.scheme, parsed.netloc, "", "", "", ""))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_base_url() -> str:
|
|
53
|
+
"""Resolve the Signet daemon base URL."""
|
|
54
|
+
explicit = _sanitize(os.environ.get("SIGNET_DAEMON_URL", ""))
|
|
55
|
+
if explicit:
|
|
56
|
+
return _normalize_base_url(explicit, "SIGNET_DAEMON_URL")
|
|
57
|
+
host = _sanitize(os.environ.get("SIGNET_HOST", _DEFAULT_HOST))
|
|
58
|
+
port = _sanitize(os.environ.get("SIGNET_PORT", str(_DEFAULT_PORT)))
|
|
59
|
+
return _normalize_base_url(f"http://{host}:{port}", "SIGNET_HOST/SIGNET_PORT")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_loopback_host(host: str) -> bool:
|
|
63
|
+
"""Return true for localhost and loopback IP literals."""
|
|
64
|
+
if host.lower() == "localhost":
|
|
65
|
+
return True
|
|
66
|
+
try:
|
|
67
|
+
return ipaddress.ip_address(host).is_loopback
|
|
68
|
+
except ValueError:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _trusted_daemon_origins() -> List[str]:
|
|
73
|
+
"""Read the exact remote daemon origins trusted to receive SIGNET_TOKEN."""
|
|
74
|
+
raw = _sanitize(os.environ.get(_TRUSTED_ORIGINS_ENV, ""))
|
|
75
|
+
origins: List[str] = []
|
|
76
|
+
for part in raw.split(","):
|
|
77
|
+
candidate = part.strip()
|
|
78
|
+
if not candidate:
|
|
79
|
+
continue
|
|
80
|
+
try:
|
|
81
|
+
origins.append(_normalize_base_url(candidate, _TRUSTED_ORIGINS_ENV))
|
|
82
|
+
except ValueError:
|
|
83
|
+
continue
|
|
84
|
+
return origins
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _should_send_auth_token(base_url: str) -> bool:
|
|
88
|
+
"""Only send bearer tokens to loopback or explicitly trusted daemon origins."""
|
|
89
|
+
parsed = urllib.parse.urlparse(base_url)
|
|
90
|
+
host = parsed.hostname or ""
|
|
91
|
+
return _is_loopback_host(host) or base_url in _trusted_daemon_origins()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _read_json_response(resp) -> Dict[str, Any]:
|
|
95
|
+
"""Read a daemon response, treating empty successful bodies as an empty object."""
|
|
96
|
+
body = resp.read()
|
|
97
|
+
if not body:
|
|
98
|
+
return {}
|
|
99
|
+
return json.loads(body.decode("utf-8"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _safe_score(value: Any) -> float:
|
|
103
|
+
"""Coerce daemon result scores without failing recall on malformed rows."""
|
|
104
|
+
try:
|
|
105
|
+
return float(value or 0.0)
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
return 0.0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SignetClient:
|
|
111
|
+
"""HTTP client for the Signet daemon API."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, agent_id: str = "", harness: str = "hermes-agent"):
|
|
114
|
+
self._base_url = _resolve_base_url()
|
|
115
|
+
self._agent_id = agent_id
|
|
116
|
+
self._harness = harness
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def base_url(self) -> str:
|
|
120
|
+
return self._base_url
|
|
121
|
+
|
|
122
|
+
def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
123
|
+
h: Dict[str, str] = {
|
|
124
|
+
"Content-Type": "application/json",
|
|
125
|
+
"x-signet-runtime-path": "plugin",
|
|
126
|
+
"x-signet-agent-id": self._agent_id,
|
|
127
|
+
"x-signet-actor": "hermes-memory-plugin",
|
|
128
|
+
}
|
|
129
|
+
# Include auth token only for loopback or explicitly trusted origins.
|
|
130
|
+
token = _sanitize(os.environ.get("SIGNET_API_KEY", "")) or _sanitize(os.environ.get("SIGNET_TOKEN", ""))
|
|
131
|
+
if token and _should_send_auth_token(self._base_url):
|
|
132
|
+
h["Authorization"] = f"Bearer {token}"
|
|
133
|
+
if extra:
|
|
134
|
+
h.update(extra)
|
|
135
|
+
return h
|
|
136
|
+
|
|
137
|
+
def _post(
|
|
138
|
+
self,
|
|
139
|
+
path: str,
|
|
140
|
+
body: Dict[str, Any],
|
|
141
|
+
*,
|
|
142
|
+
timeout: float = _TIMEOUT_SECS,
|
|
143
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
144
|
+
) -> Optional[Dict[str, Any]]:
|
|
145
|
+
"""POST JSON to the daemon. Returns parsed response or None on failure."""
|
|
146
|
+
url = f"{self._base_url}{path}"
|
|
147
|
+
data = json.dumps(body).encode("utf-8")
|
|
148
|
+
headers = self._headers(extra_headers)
|
|
149
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
150
|
+
try:
|
|
151
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
152
|
+
return _read_json_response(resp)
|
|
153
|
+
except urllib.error.HTTPError as e:
|
|
154
|
+
body_text = ""
|
|
155
|
+
try:
|
|
156
|
+
body_text = e.read().decode("utf-8", errors="replace")[:200]
|
|
157
|
+
except Exception as read_err:
|
|
158
|
+
logger.debug("Signet POST %s: failed to read error body: %s", path, read_err)
|
|
159
|
+
logger.debug("Signet POST %s returned %d: %s", path, e.code, body_text)
|
|
160
|
+
return None
|
|
161
|
+
except (urllib.error.URLError, OSError, TimeoutError, ValueError) as e:
|
|
162
|
+
logger.debug("Signet POST %s failed: %s", path, e)
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
def _get(
|
|
166
|
+
self,
|
|
167
|
+
path: str,
|
|
168
|
+
*,
|
|
169
|
+
timeout: float = _TIMEOUT_SECS,
|
|
170
|
+
) -> Optional[Dict[str, Any]]:
|
|
171
|
+
"""GET from the daemon. Returns parsed response or None on failure."""
|
|
172
|
+
url = f"{self._base_url}{path}"
|
|
173
|
+
req = urllib.request.Request(url, headers=self._headers(), method="GET")
|
|
174
|
+
try:
|
|
175
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
176
|
+
return _read_json_response(resp)
|
|
177
|
+
except (urllib.error.HTTPError, urllib.error.URLError, OSError, TimeoutError, ValueError) as e:
|
|
178
|
+
logger.debug("Signet GET %s failed: %s", path, e)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _patch(
|
|
182
|
+
self,
|
|
183
|
+
path: str,
|
|
184
|
+
body: Dict[str, Any],
|
|
185
|
+
*,
|
|
186
|
+
timeout: float = _TIMEOUT_SECS,
|
|
187
|
+
) -> Optional[Dict[str, Any]]:
|
|
188
|
+
"""PATCH JSON to the daemon. Returns parsed response or None on failure."""
|
|
189
|
+
url = f"{self._base_url}{path}"
|
|
190
|
+
data = json.dumps(body).encode("utf-8")
|
|
191
|
+
req = urllib.request.Request(url, data=data, headers=self._headers(), method="PATCH")
|
|
192
|
+
try:
|
|
193
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
194
|
+
return _read_json_response(resp)
|
|
195
|
+
except (urllib.error.HTTPError, urllib.error.URLError, OSError, TimeoutError, ValueError) as e:
|
|
196
|
+
logger.debug("Signet PATCH %s failed: %s", path, e)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
def _delete(
|
|
200
|
+
self,
|
|
201
|
+
path: str,
|
|
202
|
+
*,
|
|
203
|
+
timeout: float = _TIMEOUT_SECS,
|
|
204
|
+
) -> Optional[Dict[str, Any]]:
|
|
205
|
+
"""DELETE from the daemon. Returns parsed response or None on failure."""
|
|
206
|
+
url = f"{self._base_url}{path}"
|
|
207
|
+
req = urllib.request.Request(url, headers=self._headers(), method="DELETE")
|
|
208
|
+
try:
|
|
209
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
210
|
+
return _read_json_response(resp)
|
|
211
|
+
except (urllib.error.HTTPError, urllib.error.URLError, OSError, TimeoutError, ValueError) as e:
|
|
212
|
+
logger.debug("Signet DELETE %s failed: %s", path, e)
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# -- Health ---------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def is_available(self) -> bool:
|
|
218
|
+
"""Check if the Signet daemon is reachable. No credentials needed."""
|
|
219
|
+
result = self._get("/health", timeout=2)
|
|
220
|
+
return result is not None
|
|
221
|
+
|
|
222
|
+
# -- Hooks ----------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def session_start(
|
|
225
|
+
self,
|
|
226
|
+
session_key: str,
|
|
227
|
+
*,
|
|
228
|
+
project: str = "",
|
|
229
|
+
) -> Optional[Dict[str, Any]]:
|
|
230
|
+
"""Call session-start hook. Returns identity + memories + inject text."""
|
|
231
|
+
return self._post(
|
|
232
|
+
"/api/hooks/session-start",
|
|
233
|
+
{
|
|
234
|
+
"harness": self._harness,
|
|
235
|
+
"sessionKey": session_key,
|
|
236
|
+
"project": project,
|
|
237
|
+
"agentId": self._agent_id,
|
|
238
|
+
},
|
|
239
|
+
timeout=_LONG_TIMEOUT_SECS,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def user_prompt_submit(
|
|
243
|
+
self,
|
|
244
|
+
session_key: str,
|
|
245
|
+
user_message: str,
|
|
246
|
+
*,
|
|
247
|
+
last_assistant_message: str = "",
|
|
248
|
+
project: str = "",
|
|
249
|
+
) -> Optional[Dict[str, Any]]:
|
|
250
|
+
"""Call user-prompt-submit hook. Returns recall inject text."""
|
|
251
|
+
return self._post(
|
|
252
|
+
"/api/hooks/user-prompt-submit",
|
|
253
|
+
{
|
|
254
|
+
"harness": self._harness,
|
|
255
|
+
"sessionKey": session_key,
|
|
256
|
+
"userMessage": user_message,
|
|
257
|
+
"lastAssistantMessage": last_assistant_message,
|
|
258
|
+
"agentId": self._agent_id,
|
|
259
|
+
"project": project,
|
|
260
|
+
},
|
|
261
|
+
timeout=_RECALL_TIMEOUT_SECS,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def session_end(
|
|
265
|
+
self,
|
|
266
|
+
session_key: str,
|
|
267
|
+
transcript: str,
|
|
268
|
+
*,
|
|
269
|
+
project: str = "",
|
|
270
|
+
reason: str = "",
|
|
271
|
+
) -> Optional[Dict[str, Any]]:
|
|
272
|
+
"""Call session-end hook. Triggers memory extraction from transcript."""
|
|
273
|
+
body: Dict[str, Any] = {
|
|
274
|
+
"harness": self._harness,
|
|
275
|
+
"sessionKey": session_key,
|
|
276
|
+
"transcript": transcript,
|
|
277
|
+
"agentId": self._agent_id,
|
|
278
|
+
"cwd": project,
|
|
279
|
+
}
|
|
280
|
+
if reason:
|
|
281
|
+
body["reason"] = reason
|
|
282
|
+
return self._post(
|
|
283
|
+
"/api/hooks/session-end",
|
|
284
|
+
body,
|
|
285
|
+
timeout=_LONG_TIMEOUT_SECS,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def pre_compaction(
|
|
289
|
+
self,
|
|
290
|
+
session_key: str,
|
|
291
|
+
*,
|
|
292
|
+
session_context: str = "",
|
|
293
|
+
message_count: int = 0,
|
|
294
|
+
) -> Optional[Dict[str, Any]]:
|
|
295
|
+
"""Call pre-compaction hook. Returns summary prompt and guidelines."""
|
|
296
|
+
body: Dict[str, Any] = {
|
|
297
|
+
"harness": self._harness,
|
|
298
|
+
"sessionKey": session_key,
|
|
299
|
+
}
|
|
300
|
+
if session_context:
|
|
301
|
+
body["sessionContext"] = session_context
|
|
302
|
+
if message_count > 0:
|
|
303
|
+
body["messageCount"] = message_count
|
|
304
|
+
return self._post("/api/hooks/pre-compaction", body)
|
|
305
|
+
|
|
306
|
+
def compaction_complete(
|
|
307
|
+
self,
|
|
308
|
+
session_key: str,
|
|
309
|
+
summary: str,
|
|
310
|
+
*,
|
|
311
|
+
project: str = "",
|
|
312
|
+
) -> Optional[Dict[str, Any]]:
|
|
313
|
+
"""Call compaction-complete hook. Saves summary as session memory."""
|
|
314
|
+
return self._post(
|
|
315
|
+
"/api/hooks/compaction-complete",
|
|
316
|
+
{
|
|
317
|
+
"harness": self._harness,
|
|
318
|
+
"sessionKey": session_key,
|
|
319
|
+
"summary": summary,
|
|
320
|
+
"agentId": self._agent_id,
|
|
321
|
+
"project": project,
|
|
322
|
+
},
|
|
323
|
+
timeout=_LONG_TIMEOUT_SECS,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def checkpoint_extract(
|
|
327
|
+
self,
|
|
328
|
+
session_key: str,
|
|
329
|
+
transcript: str,
|
|
330
|
+
*,
|
|
331
|
+
project: str = "",
|
|
332
|
+
) -> Optional[Dict[str, Any]]:
|
|
333
|
+
"""Call checkpoint-extract for long-running sessions.
|
|
334
|
+
|
|
335
|
+
Extracts only the delta since last extraction. Does not
|
|
336
|
+
release the session claim.
|
|
337
|
+
"""
|
|
338
|
+
return self._post(
|
|
339
|
+
"/api/hooks/session-checkpoint-extract",
|
|
340
|
+
{
|
|
341
|
+
"harness": self._harness,
|
|
342
|
+
"sessionKey": session_key,
|
|
343
|
+
"transcript": transcript,
|
|
344
|
+
"agentId": self._agent_id,
|
|
345
|
+
"project": project,
|
|
346
|
+
},
|
|
347
|
+
timeout=_LONG_TIMEOUT_SECS,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# -- Memory API -----------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def remember(
|
|
353
|
+
self,
|
|
354
|
+
content: str,
|
|
355
|
+
*,
|
|
356
|
+
importance: float = 0.5,
|
|
357
|
+
tags: Optional[List[str]] = None,
|
|
358
|
+
memory_type: str = "",
|
|
359
|
+
pinned: Optional[bool] = None,
|
|
360
|
+
project: str = "",
|
|
361
|
+
source_type: str = "",
|
|
362
|
+
source_id: str = "",
|
|
363
|
+
hints: Optional[List[str]] = None,
|
|
364
|
+
transcript: str = "",
|
|
365
|
+
structured: Optional[Dict[str, Any]] = None,
|
|
366
|
+
who: str = "hermes-agent",
|
|
367
|
+
) -> Optional[Dict[str, Any]]:
|
|
368
|
+
"""Store a memory via the daemon API."""
|
|
369
|
+
body: Dict[str, Any] = {
|
|
370
|
+
"content": content,
|
|
371
|
+
"importance": importance,
|
|
372
|
+
"who": who,
|
|
373
|
+
}
|
|
374
|
+
if self._agent_id:
|
|
375
|
+
body["agentId"] = self._agent_id
|
|
376
|
+
if memory_type:
|
|
377
|
+
body["type"] = memory_type
|
|
378
|
+
if tags:
|
|
379
|
+
body["tags"] = tags
|
|
380
|
+
if pinned is not None:
|
|
381
|
+
body["pinned"] = pinned
|
|
382
|
+
if project:
|
|
383
|
+
body["project"] = project
|
|
384
|
+
if source_type:
|
|
385
|
+
body["sourceType"] = source_type
|
|
386
|
+
if source_id:
|
|
387
|
+
body["sourceId"] = source_id
|
|
388
|
+
if hints:
|
|
389
|
+
body["hints"] = hints
|
|
390
|
+
if transcript:
|
|
391
|
+
body["transcript"] = transcript
|
|
392
|
+
if structured:
|
|
393
|
+
body["structured"] = structured
|
|
394
|
+
return self._post("/api/memory/remember", body, timeout=_LONG_TIMEOUT_SECS)
|
|
395
|
+
|
|
396
|
+
def recall(
|
|
397
|
+
self,
|
|
398
|
+
query: str,
|
|
399
|
+
*,
|
|
400
|
+
limit: int = 10,
|
|
401
|
+
project: str = "",
|
|
402
|
+
memory_type: str = "",
|
|
403
|
+
tags: str = "",
|
|
404
|
+
who: str = "",
|
|
405
|
+
pinned: Optional[bool] = None,
|
|
406
|
+
importance_min: Optional[float] = None,
|
|
407
|
+
since: str = "",
|
|
408
|
+
until: str = "",
|
|
409
|
+
keyword_query: str = "",
|
|
410
|
+
score_min: Optional[float] = None,
|
|
411
|
+
aggregate: bool = False,
|
|
412
|
+
aggregate_budget: str = "",
|
|
413
|
+
save_aggregate: Optional[bool] = None,
|
|
414
|
+
agent_scoped: bool = False,
|
|
415
|
+
) -> Optional[Dict[str, Any]]:
|
|
416
|
+
"""Search memories via hybrid recall."""
|
|
417
|
+
body: Dict[str, Any] = {
|
|
418
|
+
"query": query,
|
|
419
|
+
"limit": limit,
|
|
420
|
+
}
|
|
421
|
+
if project:
|
|
422
|
+
body["project"] = project
|
|
423
|
+
if memory_type:
|
|
424
|
+
body["type"] = memory_type
|
|
425
|
+
if tags:
|
|
426
|
+
body["tags"] = tags
|
|
427
|
+
if who:
|
|
428
|
+
body["who"] = who
|
|
429
|
+
if pinned is not None:
|
|
430
|
+
body["pinned"] = pinned
|
|
431
|
+
if importance_min is not None:
|
|
432
|
+
body["importance_min"] = importance_min
|
|
433
|
+
if since:
|
|
434
|
+
body["since"] = since
|
|
435
|
+
if until:
|
|
436
|
+
body["until"] = until
|
|
437
|
+
if keyword_query:
|
|
438
|
+
body["keywordQuery"] = keyword_query
|
|
439
|
+
if aggregate:
|
|
440
|
+
body["aggregate"] = True
|
|
441
|
+
if aggregate_budget in ("small", "medium", "large"):
|
|
442
|
+
body["aggregateBudget"] = aggregate_budget
|
|
443
|
+
if save_aggregate is not None:
|
|
444
|
+
body["saveAggregate"] = save_aggregate
|
|
445
|
+
if agent_scoped and self._agent_id:
|
|
446
|
+
body["agentId"] = self._agent_id
|
|
447
|
+
|
|
448
|
+
result = self._post("/api/memory/recall", body, timeout=_RECALL_TIMEOUT_SECS)
|
|
449
|
+
if (
|
|
450
|
+
result
|
|
451
|
+
and score_min is not None
|
|
452
|
+
and isinstance(result.get("results"), list)
|
|
453
|
+
):
|
|
454
|
+
kept = [
|
|
455
|
+
row for row in result["results"]
|
|
456
|
+
if not isinstance(row, dict) or _safe_score(row.get("score")) >= score_min
|
|
457
|
+
]
|
|
458
|
+
result = dict(result)
|
|
459
|
+
result["results"] = kept
|
|
460
|
+
meta = result.get("meta")
|
|
461
|
+
if isinstance(meta, dict):
|
|
462
|
+
result["meta"] = {**meta, "totalReturned": len(kept), "noHits": len(kept) == 0}
|
|
463
|
+
return result
|
|
464
|
+
|
|
465
|
+
def session_search(
|
|
466
|
+
self,
|
|
467
|
+
query: str,
|
|
468
|
+
*,
|
|
469
|
+
session_key: str = "",
|
|
470
|
+
current_session_key: str = "",
|
|
471
|
+
agent_id: str = "",
|
|
472
|
+
project: str = "",
|
|
473
|
+
limit: int = 10,
|
|
474
|
+
) -> Optional[Dict[str, Any]]:
|
|
475
|
+
"""Search active or completed session transcripts."""
|
|
476
|
+
body: Dict[str, Any] = {
|
|
477
|
+
"query": query,
|
|
478
|
+
"limit": limit,
|
|
479
|
+
}
|
|
480
|
+
if session_key:
|
|
481
|
+
body["sessionKey"] = session_key
|
|
482
|
+
if current_session_key:
|
|
483
|
+
body["currentSessionKey"] = current_session_key
|
|
484
|
+
resolved_agent_id = agent_id or self._agent_id
|
|
485
|
+
if resolved_agent_id:
|
|
486
|
+
body["agentId"] = resolved_agent_id
|
|
487
|
+
if project:
|
|
488
|
+
body["project"] = project
|
|
489
|
+
return self._post("/api/sessions/search", body, timeout=_RECALL_TIMEOUT_SECS)
|
|
490
|
+
|
|
491
|
+
def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]:
|
|
492
|
+
"""Retrieve a single memory by ID."""
|
|
493
|
+
return self._get(f"/api/memory/{urllib.parse.quote(memory_id)}")
|
|
494
|
+
|
|
495
|
+
def list_memories(
|
|
496
|
+
self,
|
|
497
|
+
*,
|
|
498
|
+
limit: int = 100,
|
|
499
|
+
offset: int = 0,
|
|
500
|
+
memory_type: str = "",
|
|
501
|
+
) -> Optional[Dict[str, Any]]:
|
|
502
|
+
"""List memories with optional filters."""
|
|
503
|
+
params = f"?limit={limit}&offset={offset}"
|
|
504
|
+
if memory_type:
|
|
505
|
+
params += f"&type={urllib.parse.quote(memory_type)}"
|
|
506
|
+
return self._get(f"/api/memories{params}")
|
|
507
|
+
|
|
508
|
+
def modify_memory(
|
|
509
|
+
self,
|
|
510
|
+
memory_id: str,
|
|
511
|
+
*,
|
|
512
|
+
content: str = "",
|
|
513
|
+
memory_type: str = "",
|
|
514
|
+
importance: Optional[float] = None,
|
|
515
|
+
tags: str = "",
|
|
516
|
+
pinned: Optional[bool] = None,
|
|
517
|
+
reason: str,
|
|
518
|
+
) -> Optional[Dict[str, Any]]:
|
|
519
|
+
"""Edit an existing memory by ID."""
|
|
520
|
+
body: Dict[str, Any] = {"reason": reason}
|
|
521
|
+
if content:
|
|
522
|
+
body["content"] = content
|
|
523
|
+
if memory_type:
|
|
524
|
+
body["type"] = memory_type
|
|
525
|
+
if importance is not None:
|
|
526
|
+
body["importance"] = importance
|
|
527
|
+
if tags:
|
|
528
|
+
body["tags"] = tags
|
|
529
|
+
if pinned is not None:
|
|
530
|
+
body["pinned"] = pinned
|
|
531
|
+
return self._patch(f"/api/memory/{urllib.parse.quote(memory_id)}", body)
|
|
532
|
+
|
|
533
|
+
def forget_memory(
|
|
534
|
+
self,
|
|
535
|
+
memory_id: str,
|
|
536
|
+
*,
|
|
537
|
+
reason: str,
|
|
538
|
+
) -> Optional[Dict[str, Any]]:
|
|
539
|
+
"""Soft-delete a memory by ID."""
|
|
540
|
+
params = urllib.parse.urlencode({"reason": reason})
|
|
541
|
+
return self._delete(f"/api/memory/{urllib.parse.quote(memory_id)}?{params}")
|
|
542
|
+
|
|
543
|
+
def search(
|
|
544
|
+
self,
|
|
545
|
+
query: str,
|
|
546
|
+
*,
|
|
547
|
+
limit: int = 10,
|
|
548
|
+
memory_type: str = "",
|
|
549
|
+
) -> List[Dict[str, Any]]:
|
|
550
|
+
"""Search memories. Returns list of memory objects."""
|
|
551
|
+
params = f"?q={urllib.parse.quote(query)}&limit={limit}"
|
|
552
|
+
if memory_type:
|
|
553
|
+
params += f"&type={urllib.parse.quote(memory_type)}"
|
|
554
|
+
result = self._get(f"/api/memory/search{params}")
|
|
555
|
+
if result and isinstance(result, dict):
|
|
556
|
+
return result.get("results", result.get("memories", []))
|
|
557
|
+
if isinstance(result, list):
|
|
558
|
+
return result
|
|
559
|
+
return []
|
|
560
|
+
|
|
561
|
+
def feedback(
|
|
562
|
+
self,
|
|
563
|
+
ratings: Dict[str, float],
|
|
564
|
+
*,
|
|
565
|
+
session_key: str = "",
|
|
566
|
+
) -> Optional[Dict[str, Any]]:
|
|
567
|
+
"""Rate memory relevance for predictor training."""
|
|
568
|
+
body: Dict[str, Any] = {"ratings": ratings}
|
|
569
|
+
if session_key:
|
|
570
|
+
body["session_key"] = session_key
|
|
571
|
+
return self._post("/api/memory/feedback", body)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: signet
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: "Signet Memory — persistent cross-session memory with hybrid search, knowledge graph, and predictive recall via the Signet daemon."
|
|
4
|
+
hooks:
|
|
5
|
+
- initialize
|
|
6
|
+
- system_prompt_block
|
|
7
|
+
- queue_prefetch
|
|
8
|
+
- prefetch
|
|
9
|
+
- on_turn_start
|
|
10
|
+
- sync_turn
|
|
11
|
+
- on_memory_write
|
|
12
|
+
- on_pre_compress
|
|
13
|
+
- on_compaction_complete
|
|
14
|
+
- on_session_end
|
|
15
|
+
- on_delegation
|
|
16
|
+
- shutdown
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@signetai/connector-hermes-agent",
|
|
3
|
+
"version": "0.140.1",
|
|
4
|
+
"description": "Signet connector for Hermes Agent — installs Signet as a pluggable memory provider",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"hermes-plugin",
|
|
17
|
+
"bin",
|
|
18
|
+
"!dist/**/*.test.*",
|
|
19
|
+
"!dist/**/__tests__/**"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun build src/index.ts --outdir dist --target node --external better-sqlite3 && bun run build:types",
|
|
23
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
24
|
+
"dev": "bun --watch src/index.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@signetai/connector-base": "0.140.1",
|
|
29
|
+
"@signetai/core": "0.140.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"signet",
|
|
37
|
+
"hermes",
|
|
38
|
+
"hermes-agent",
|
|
39
|
+
"connector",
|
|
40
|
+
"ai-memory",
|
|
41
|
+
"agent-identity",
|
|
42
|
+
"memory-provider"
|
|
43
|
+
],
|
|
44
|
+
"license": "Apache-2.0",
|
|
45
|
+
"bin": {
|
|
46
|
+
"signet-connector-hermes-agent": "bin/install.js"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/Signet-AI/signetai.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/Signet-AI/signetai",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/Signet-AI/signetai/issues"
|
|
58
|
+
}
|
|
59
|
+
}
|