kryth 0.1.0__py3-none-any.whl
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.
Potentially problematic release.
This version of kryth might be problematic. Click here for more details.
- kryth/__init__.py +31 -0
- kryth/agent/__init__.py +1 -0
- kryth/agent/memory/__init__.py +21 -0
- kryth/agent/memory/controllers.py +415 -0
- kryth-0.1.0.dist-info/METADATA +303 -0
- kryth-0.1.0.dist-info/RECORD +9 -0
- kryth-0.1.0.dist-info/WHEEL +5 -0
- kryth-0.1.0.dist-info/licenses/LICENSE +21 -0
- kryth-0.1.0.dist-info/top_level.txt +1 -0
kryth/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Kryth — AI Agent Memory Framework.
|
|
2
|
+
|
|
3
|
+
Kryth provides structured memory layers for LLM-based agents, including
|
|
4
|
+
write, retrieval, duplicate detection, and compression controllers.
|
|
5
|
+
|
|
6
|
+
Typical usage::
|
|
7
|
+
|
|
8
|
+
from kryth.agent.memory.controllers import (
|
|
9
|
+
WriteController,
|
|
10
|
+
RetrievalController,
|
|
11
|
+
DuplicateDetector,
|
|
12
|
+
CompressionController,
|
|
13
|
+
compute_state_hash,
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from kryth.agent.memory.controllers import (
|
|
18
|
+
CompressionController,
|
|
19
|
+
DuplicateDetector,
|
|
20
|
+
RetrievalController,
|
|
21
|
+
WriteController,
|
|
22
|
+
compute_state_hash,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"WriteController",
|
|
27
|
+
"RetrievalController",
|
|
28
|
+
"DuplicateDetector",
|
|
29
|
+
"CompressionController",
|
|
30
|
+
"compute_state_hash",
|
|
31
|
+
]
|
kryth/agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent memory subpackage for Kryth."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Memory controllers subpackage for Kryth.
|
|
2
|
+
|
|
3
|
+
Exports: WriteController, RetrievalController, DuplicateDetector,
|
|
4
|
+
CompressionController, compute_state_hash.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from kryth.agent.memory.controllers import (
|
|
8
|
+
CompressionController,
|
|
9
|
+
DuplicateDetector,
|
|
10
|
+
RetrievalController,
|
|
11
|
+
WriteController,
|
|
12
|
+
compute_state_hash,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"WriteController",
|
|
17
|
+
"RetrievalController",
|
|
18
|
+
"DuplicateDetector",
|
|
19
|
+
"CompressionController",
|
|
20
|
+
"compute_state_hash",
|
|
21
|
+
]
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Memory controllers — Write, Retrieval, DuplicateDetector, Compression.
|
|
2
|
+
|
|
3
|
+
Every tool call that produces knowledge must go through WriteController.
|
|
4
|
+
Every prompt injection must go through RetrievalController.
|
|
5
|
+
Every duplicate check must go through DuplicateDetector.
|
|
6
|
+
Memory growth is managed by CompressionController.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── State hash ──────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
def compute_state_hash(cwd: str = "") -> str:
|
|
23
|
+
"""Compute a robust state hash for duplicate command detection.
|
|
24
|
+
|
|
25
|
+
Includes:
|
|
26
|
+
1. git diff (staged + unstaged)
|
|
27
|
+
2. modified file hashes (up to 20)
|
|
28
|
+
3. dependency lock file hashes
|
|
29
|
+
4. environment hash (selected vars)
|
|
30
|
+
5. working directory
|
|
31
|
+
"""
|
|
32
|
+
hasher = hashlib.sha256()
|
|
33
|
+
cwd = cwd or os.getcwd()
|
|
34
|
+
hasher.update(cwd.encode())
|
|
35
|
+
|
|
36
|
+
# 1. Git diff
|
|
37
|
+
try:
|
|
38
|
+
diff = subprocess.run(
|
|
39
|
+
["git", "diff", "--no-color"],
|
|
40
|
+
capture_output=True, text=True, timeout=5, cwd=cwd,
|
|
41
|
+
)
|
|
42
|
+
hasher.update(diff.stdout.encode()[:50000])
|
|
43
|
+
diff_staged = subprocess.run(
|
|
44
|
+
["git", "diff", "--no-color", "--cached"],
|
|
45
|
+
capture_output=True, text=True, timeout=5, cwd=cwd,
|
|
46
|
+
)
|
|
47
|
+
hasher.update(diff_staged.stdout.encode()[:50000])
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# 2. Modified file hashes (up to 20 files by mtime)
|
|
52
|
+
try:
|
|
53
|
+
lock_files = [
|
|
54
|
+
"requirements.txt", "Pipfile", "Pipfile.lock",
|
|
55
|
+
"pyproject.toml", "poetry.lock",
|
|
56
|
+
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
57
|
+
"Cargo.toml", "Cargo.lock",
|
|
58
|
+
"go.mod", "go.sum",
|
|
59
|
+
"Gemfile", "Gemfile.lock",
|
|
60
|
+
"composer.json", "composer.lock",
|
|
61
|
+
]
|
|
62
|
+
counted = 0
|
|
63
|
+
for fname in lock_files:
|
|
64
|
+
fpath = Path(cwd) / fname
|
|
65
|
+
if fpath.is_file():
|
|
66
|
+
try:
|
|
67
|
+
h = hashlib.sha256(fpath.read_bytes()).hexdigest()[:16]
|
|
68
|
+
hasher.update(f"{fname}:{h}".encode())
|
|
69
|
+
counted += 1
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
# Recent modified files
|
|
73
|
+
for f in sorted(Path(cwd).iterdir(), key=lambda p: p.stat().st_mtime if p.is_file() else 0, reverse=True)[:20]:
|
|
74
|
+
if f.is_file() and f.suffix in (".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java"):
|
|
75
|
+
try:
|
|
76
|
+
h = hashlib.sha256(f.read_bytes()).hexdigest()[:16]
|
|
77
|
+
hasher.update(f"{f.name}:{h}".encode())
|
|
78
|
+
counted += 1
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# 3. Environment hash (selected vars — not secrets)
|
|
85
|
+
for var in ("VIRTUAL_ENV", "CONDA_PREFIX", "NODE_ENV", "PYTHON_VERSION", "NODE_VERSION"):
|
|
86
|
+
val = os.environ.get(var, "")
|
|
87
|
+
if val:
|
|
88
|
+
hasher.update(f"{var}={val}".encode())
|
|
89
|
+
|
|
90
|
+
return hasher.hexdigest()[:16]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── Duplicate Detector ──────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
class DuplicateDetector:
|
|
96
|
+
"""Soft duplicate detection — returns memory summaries, never hard-blocks.
|
|
97
|
+
|
|
98
|
+
Rules:
|
|
99
|
+
- Same file + same hash → return memory summary, allow reread
|
|
100
|
+
- Same command + same cwd + same state_hash → return previous result, allow rerun
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def check_read(self, repo_memory, session_id: int, path: str) -> Optional[dict]:
|
|
104
|
+
"""Check if file was already read. Returns a memory hit dict or None."""
|
|
105
|
+
file_rec = repo_memory.get_file(session_id, path)
|
|
106
|
+
if not file_rec:
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
with open(path, "rb") as f:
|
|
110
|
+
current_hash = hashlib.sha256(f.read()).hexdigest()[:16]
|
|
111
|
+
if current_hash != file_rec.hash:
|
|
112
|
+
return None # file changed, not a duplicate
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
summary = self._build_read_summary(file_rec)
|
|
117
|
+
return {
|
|
118
|
+
"duplicate": True,
|
|
119
|
+
"type": "read",
|
|
120
|
+
"path": path,
|
|
121
|
+
"turn": file_rec.last_read_turn,
|
|
122
|
+
"read_count": file_rec.read_count,
|
|
123
|
+
"summary": summary,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def _build_read_summary(self, rec) -> str:
|
|
127
|
+
"""Build a concise semantic summary of what's already known about a file."""
|
|
128
|
+
parts = [f"📄 {rec.path} ({rec.language}, {rec.lines} lines, read #{rec.read_count})"]
|
|
129
|
+
if rec.purpose:
|
|
130
|
+
parts.append(f" Purpose: {rec.purpose[:80]}")
|
|
131
|
+
if rec.functions:
|
|
132
|
+
parts.append(f" Functions: {', '.join(rec.functions[:8])}")
|
|
133
|
+
if rec.classes:
|
|
134
|
+
parts.append(f" Classes: {', '.join(rec.classes[:5])}")
|
|
135
|
+
if rec.routes:
|
|
136
|
+
parts.append(f" Routes: {', '.join(rec.routes[:8])}")
|
|
137
|
+
if rec.imports:
|
|
138
|
+
parts.append(f" Imports: {', '.join(rec.imports[:6])}")
|
|
139
|
+
return "\n".join(parts)
|
|
140
|
+
|
|
141
|
+
def check_command(
|
|
142
|
+
self, execution_memory, session_id: int, command: str, cwd: str, state_hash: str = ""
|
|
143
|
+
) -> Optional[dict]:
|
|
144
|
+
"""Check if identical command was already run. Returns a memory hit dict or None."""
|
|
145
|
+
dup = execution_memory.find_duplicate(session_id, command, cwd, state_hash)
|
|
146
|
+
if dup is None:
|
|
147
|
+
return None
|
|
148
|
+
return {
|
|
149
|
+
"duplicate": True,
|
|
150
|
+
"type": "command",
|
|
151
|
+
"command": command[:80],
|
|
152
|
+
"exit_code": dup.exit_code,
|
|
153
|
+
"status": "success" if dup.exit_code == 0 else "failed",
|
|
154
|
+
"previous_output": dup.output_summary[:200],
|
|
155
|
+
"run_count": dup.run_count,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── Write Controller ────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
class WriteController:
|
|
162
|
+
"""Single writer for all memory layers.
|
|
163
|
+
|
|
164
|
+
Tool → Memory mapping:
|
|
165
|
+
read_file → RepoMemory
|
|
166
|
+
run_command → ExecutionMemory (with state_hash)
|
|
167
|
+
edit_file/multi_edit → RepoMemory (invalidate) + EpisodicMemory
|
|
168
|
+
run_tests → EpisodicMemory
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def on_tool_result(
|
|
172
|
+
self,
|
|
173
|
+
tool_name: str,
|
|
174
|
+
args: dict,
|
|
175
|
+
result: Any,
|
|
176
|
+
error: bool,
|
|
177
|
+
session_id: int,
|
|
178
|
+
memory_manager,
|
|
179
|
+
turn: int,
|
|
180
|
+
) -> None:
|
|
181
|
+
if error:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if tool_name == "read_file":
|
|
185
|
+
path = args.get("path", "")
|
|
186
|
+
if path and result:
|
|
187
|
+
content = str(result)
|
|
188
|
+
memory_manager.repo.add_file(session_id, path, content, turn)
|
|
189
|
+
memory_manager.working.add_finding(f"Read: {path}")
|
|
190
|
+
|
|
191
|
+
elif tool_name in ("run_command", "shell_exec"):
|
|
192
|
+
command = args.get("command", "") or args.get("cmd", "")
|
|
193
|
+
cwd = args.get("cwd") or os.getcwd()
|
|
194
|
+
exit_code = 0
|
|
195
|
+
output_summary = str(result)[:500] if result else ""
|
|
196
|
+
if "ERROR" in output_summary.upper() or "FAILED" in output_summary.upper():
|
|
197
|
+
exit_code = 1
|
|
198
|
+
env_hash = hashlib.sha256(str(sorted(os.environ.items())).encode()).hexdigest()[:16]
|
|
199
|
+
state_hash = compute_state_hash(cwd)
|
|
200
|
+
memory_manager.execution.record(
|
|
201
|
+
session_id, command, cwd, exit_code, output_summary,
|
|
202
|
+
env_hash=env_hash, state_hash=state_hash,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
elif tool_name in ("edit_file", "multi_edit"):
|
|
206
|
+
path = args.get("path", "")
|
|
207
|
+
if path:
|
|
208
|
+
memory_manager.repo.invalidate_file(session_id, path)
|
|
209
|
+
desc = args.get("oldString", "")[:60] or args.get("newString", "")[:60]
|
|
210
|
+
desc = desc or f"edit {os.path.basename(path)}"
|
|
211
|
+
memory_manager.episodic.add_edit(session_id, path, desc)
|
|
212
|
+
|
|
213
|
+
elif tool_name == "write_file":
|
|
214
|
+
path = args.get("path", "")
|
|
215
|
+
if path:
|
|
216
|
+
memory_manager.repo.invalidate_file(session_id, path)
|
|
217
|
+
memory_manager.episodic.add_edit(session_id, path, "wrote file")
|
|
218
|
+
|
|
219
|
+
elif tool_name in ("run_tests",):
|
|
220
|
+
memory_manager.episodic.add_result(session_id, str(result)[:200] if result else "tests completed")
|
|
221
|
+
|
|
222
|
+
def set_objective(self, memory_manager, session_id: int, objective: str) -> None:
|
|
223
|
+
memory_manager.working.set_objective(objective)
|
|
224
|
+
if session_id:
|
|
225
|
+
try:
|
|
226
|
+
memory_manager.episodic.set_objective(session_id, objective)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ── Retrieval Controller ────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
class RetrievalController:
|
|
234
|
+
"""Retrieve only relevant memory for LLM context injection.
|
|
235
|
+
|
|
236
|
+
Strictly capped at 800 tokens (~3200 chars).
|
|
237
|
+
Priority order:
|
|
238
|
+
1. Current objective
|
|
239
|
+
2. WorkingMemory findings + blockers
|
|
240
|
+
3. Top repo memory hits (ranked by importance)
|
|
241
|
+
4. Recent command results
|
|
242
|
+
5. Critical episodic findings
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
MAX_CHARS = 3200 # ~800 tokens
|
|
246
|
+
|
|
247
|
+
# Keyword → relevance mapping for scoring repo files
|
|
248
|
+
_RELEVANCE_KEYWORDS = {
|
|
249
|
+
"auth": ["login", "auth", "oauth", "session", "token", "password", "user", "permission", "jwt"],
|
|
250
|
+
"api": ["route", "api", "endpoint", "rest", "controller", "handler", "middleware"],
|
|
251
|
+
"db": ["db", "database", "sql", "model", "schema", "migration", "query", "orm"],
|
|
252
|
+
"config": ["config", "setting", "env", "yaml", "json", "toml", "ini"],
|
|
253
|
+
"test": ["test", "spec", "mock", "assert", "pytest", "jest"],
|
|
254
|
+
"frontend": ["component", "react", "vue", "svelte", "template", "css", "html", "ui"],
|
|
255
|
+
"build": ["build", "webpack", "vite", "rollup", "babel", "tsconfig", "make"],
|
|
256
|
+
"deploy": ["deploy", "docker", "ci", "cd", "pipeline", "action", "workflow"],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
def build_prompt_block(
|
|
260
|
+
self,
|
|
261
|
+
memory_manager,
|
|
262
|
+
session_id: int,
|
|
263
|
+
include_long_term: bool = False,
|
|
264
|
+
user_input: str = "",
|
|
265
|
+
) -> str:
|
|
266
|
+
parts = []
|
|
267
|
+
|
|
268
|
+
# 1. Current objective (always included, highest priority)
|
|
269
|
+
wm = memory_manager.working.to_prompt_block(max_chars=600)
|
|
270
|
+
if wm:
|
|
271
|
+
parts.append(wm)
|
|
272
|
+
|
|
273
|
+
remaining = self.MAX_CHARS - sum(len(p) + 2 for p in parts)
|
|
274
|
+
|
|
275
|
+
# 2. Repo memory — ranked by importance + relevance
|
|
276
|
+
repo = self._get_repo_block(memory_manager, session_id, user_input, max_chars=min(remaining, 1200))
|
|
277
|
+
if repo:
|
|
278
|
+
parts.append(repo)
|
|
279
|
+
remaining = self.MAX_CHARS - sum(len(p) + 2 for p in parts)
|
|
280
|
+
|
|
281
|
+
# 3. Recent command results (compact)
|
|
282
|
+
exec_block = memory_manager.execution.get_prompt_block(session_id)
|
|
283
|
+
if exec_block:
|
|
284
|
+
if len(exec_block) + 2 > remaining:
|
|
285
|
+
exec_lines = exec_block.splitlines()
|
|
286
|
+
exec_block = "\n".join(exec_lines[:4]) # keep only header + 3 results
|
|
287
|
+
if len(exec_block) + 2 > remaining:
|
|
288
|
+
exec_block = exec_block[:remaining - 2]
|
|
289
|
+
if exec_block:
|
|
290
|
+
parts.append(exec_block)
|
|
291
|
+
remaining = self.MAX_CHARS - sum(len(p) + 2 for p in parts)
|
|
292
|
+
|
|
293
|
+
# 4. Episodic findings (compact)
|
|
294
|
+
ep = memory_manager.episodic.get_prompt_block(max_chars=min(remaining, 800))
|
|
295
|
+
if ep:
|
|
296
|
+
parts.append(ep)
|
|
297
|
+
remaining = self.MAX_CHARS - sum(len(p) + 2 for p in parts)
|
|
298
|
+
|
|
299
|
+
# 5. Long-term memory (if requested, lowest priority)
|
|
300
|
+
if include_long_term and remaining > 200:
|
|
301
|
+
ltm = memory_manager.long_term.to_prompt_block(max_chars=min(remaining, 400))
|
|
302
|
+
if ltm:
|
|
303
|
+
parts.append(ltm)
|
|
304
|
+
|
|
305
|
+
block = "\n\n".join(parts)
|
|
306
|
+
if len(block) > self.MAX_CHARS:
|
|
307
|
+
block = block[:self.MAX_CHARS] + "\n... (truncated)"
|
|
308
|
+
return block
|
|
309
|
+
|
|
310
|
+
def _get_repo_block(
|
|
311
|
+
self, memory_manager, session_id: int, user_input: str, max_chars: int
|
|
312
|
+
) -> str:
|
|
313
|
+
"""Retrieve repo memory ranked by importance + relevance."""
|
|
314
|
+
mem = memory_manager.repo._get(session_id)
|
|
315
|
+
if not mem.files:
|
|
316
|
+
return ""
|
|
317
|
+
|
|
318
|
+
# Score files by relevance
|
|
319
|
+
query_tokens = set(re.findall(r'\w+', user_input.lower())) if user_input else set()
|
|
320
|
+
scored = []
|
|
321
|
+
for path, rec in mem.files.items():
|
|
322
|
+
relevance = self._compute_relevance(rec, query_tokens) if query_tokens else 0.5
|
|
323
|
+
score = 0.7 * rec.importance_score + 0.3 * relevance
|
|
324
|
+
scored.append((score, path, rec))
|
|
325
|
+
|
|
326
|
+
# Sort by score descending
|
|
327
|
+
scored.sort(key=lambda x: -x[0])
|
|
328
|
+
|
|
329
|
+
parts = ["KNOWN REPO STATE"]
|
|
330
|
+
if mem.framework:
|
|
331
|
+
parts.append(f"Framework: {mem.framework}")
|
|
332
|
+
|
|
333
|
+
# Top files by score
|
|
334
|
+
file_lines = []
|
|
335
|
+
for score, path, rec in scored[:8]:
|
|
336
|
+
desc = f" {path} ({rec.language}, {rec.lines} lines, imp={score:.2f})"
|
|
337
|
+
if rec.purpose:
|
|
338
|
+
desc += f" — {rec.purpose[:50]}"
|
|
339
|
+
file_lines.append(desc)
|
|
340
|
+
if file_lines:
|
|
341
|
+
parts.append(f"Files ({len(mem.files)}):\n" + "\n".join(file_lines))
|
|
342
|
+
|
|
343
|
+
# Key symbols from top files
|
|
344
|
+
sym_lines = []
|
|
345
|
+
for score, path, rec in scored[:5]:
|
|
346
|
+
syms = (rec.functions + rec.classes)[:6]
|
|
347
|
+
if syms:
|
|
348
|
+
sym_lines.append(f" {path}: {', '.join(syms)}")
|
|
349
|
+
if sym_lines:
|
|
350
|
+
parts.append("Key symbols:\n" + "\n".join(sym_lines))
|
|
351
|
+
|
|
352
|
+
if mem.entrypoints:
|
|
353
|
+
parts.append("Entry points:\n " + "\n ".join(mem.entrypoints[:3]))
|
|
354
|
+
|
|
355
|
+
if mem.routes:
|
|
356
|
+
parts.append("Routes:\n " + ", ".join(mem.routes[:10]))
|
|
357
|
+
|
|
358
|
+
block = "\n\n".join(parts)
|
|
359
|
+
if len(block) > max_chars:
|
|
360
|
+
return block[:max_chars] + "\n... (truncated)"
|
|
361
|
+
return block
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
def _compute_relevance(rec, query_tokens: set) -> float:
|
|
365
|
+
"""Score how relevant a file record is to the user's query tokens."""
|
|
366
|
+
if not query_tokens:
|
|
367
|
+
return 0.0
|
|
368
|
+
text = f"{rec.path} {rec.purpose} {' '.join(rec.functions)} {' '.join(rec.classes)} {' '.join(rec.imports)}".lower()
|
|
369
|
+
matches = sum(1 for t in query_tokens if t in text or any(t in w for w in text.split()))
|
|
370
|
+
return min(1.0, matches / max(1, len(query_tokens)))
|
|
371
|
+
|
|
372
|
+
def get_priority_ranking(
|
|
373
|
+
self,
|
|
374
|
+
memory_manager,
|
|
375
|
+
session_id: int,
|
|
376
|
+
user_input: str = "",
|
|
377
|
+
) -> dict[str, str]:
|
|
378
|
+
return {
|
|
379
|
+
"objective": memory_manager.working.objective,
|
|
380
|
+
"repo_files": str(len(memory_manager.repo._get(session_id).files)),
|
|
381
|
+
"recent_commands": str(len(memory_manager.execution._get(session_id).commands)),
|
|
382
|
+
"findings": str(len(memory_manager.episodic._get(session_id).root_causes)),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ── Compression Controller ──────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
class CompressionController:
|
|
389
|
+
"""Prevent uncontrolled memory growth."""
|
|
390
|
+
|
|
391
|
+
def compress_episodic(self, episodic_memory, session_id: int) -> None:
|
|
392
|
+
episode = episodic_memory._get(session_id)
|
|
393
|
+
if len(episode.root_causes) > 10:
|
|
394
|
+
episode.root_causes = episode.root_causes[-10:]
|
|
395
|
+
if len(episode.hypotheses) > 10:
|
|
396
|
+
episode.hypotheses = episode.hypotheses[-10:]
|
|
397
|
+
if len(episode.edits) > 20:
|
|
398
|
+
episode.edits = episode.edits[-20:]
|
|
399
|
+
if len(episode.decisions) > 20:
|
|
400
|
+
episode.decisions = episode.decisions[-20:]
|
|
401
|
+
if len(episode.successful_strategies) > 10:
|
|
402
|
+
episode.successful_strategies = episode.successful_strategies[-10:]
|
|
403
|
+
if len(episode.failed_strategies) > 10:
|
|
404
|
+
episode.failed_strategies = episode.failed_strategies[-10:]
|
|
405
|
+
|
|
406
|
+
def compress_long_term(self, long_term_memory) -> int:
|
|
407
|
+
return long_term_memory.compress(keep_top=500)
|
|
408
|
+
|
|
409
|
+
def compress_execution(self, execution_memory, session_id: int) -> None:
|
|
410
|
+
mem = execution_memory._get(session_id)
|
|
411
|
+
if len(mem.commands) > mem.max_commands:
|
|
412
|
+
mem.commands = mem.commands[-mem.max_commands:]
|
|
413
|
+
keys = list(mem._index.keys())
|
|
414
|
+
for k in keys[:max(0, len(keys) - mem.max_commands)]:
|
|
415
|
+
mem._index.pop(k, None)
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kryth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI Agent Memory Framework — structured memory layers for LLM-based agents
|
|
5
|
+
Author: Kryth Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai,agent,memory,llm,framework
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
24
|
+
Requires-Dist: ruff>=0.7; extra == "dev"
|
|
25
|
+
Requires-Dist: pre-commit>=4.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
<br/>
|
|
30
|
+
<h1>🧠 Kryth</h1>
|
|
31
|
+
<p><strong>AI Agent Memory Framework — Structured memory layers for LLM-based agents</strong></p>
|
|
32
|
+
|
|
33
|
+
<br/>
|
|
34
|
+
|
|
35
|
+
<!-- Badges -->
|
|
36
|
+
<p>
|
|
37
|
+
<a href="https://pypi.org/project/kryth/">
|
|
38
|
+
<img src="https://img.shields.io/pypi/v/kryth?style=for-the-badge&logo=pypi&logoColor=white&color=blueviolet" alt="PyPI Version"/>
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://pypi.org/project/kryth/">
|
|
41
|
+
<img src="https://img.shields.io/pypi/dm/kryth?style=for-the-badge&logo=python&logoColor=white&color=blue" alt="PyPI Downloads"/>
|
|
42
|
+
</a>
|
|
43
|
+
<a href="LICENSE">
|
|
44
|
+
<img src="https://img.shields.io/badge/license-MIT-green.svg?style=for-the-badge" alt="MIT License"/>
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://github.com/navadeep0508/KRYTH-OS">
|
|
47
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.10+"/>
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://github.com/navadeep0508/KRYTH-OS/actions">
|
|
50
|
+
<img src="https://img.shields.io/badge/code%20style-ruff-261230?style=for-the-badge&logo=ruff&logoColor=white" alt="Ruff"/>
|
|
51
|
+
</a>
|
|
52
|
+
<a href="CONTRIBUTING.md">
|
|
53
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge&logo=github" alt="PRs Welcome"/>
|
|
54
|
+
</a>
|
|
55
|
+
</p>
|
|
56
|
+
|
|
57
|
+
<br/>
|
|
58
|
+
|
|
59
|
+
<!-- Quick links -->
|
|
60
|
+
<p>
|
|
61
|
+
<a href="#-quick-start">🚀 Quick Start</a> •
|
|
62
|
+
<a href="#-features">✨ Features</a> •
|
|
63
|
+
<a href="#-architecture">🏗️ Architecture</a> •
|
|
64
|
+
<a href="#-installation">📦 Installation</a> •
|
|
65
|
+
<a href="#-contributing">🤝 Contributing</a>
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<br/>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 💡 Why Kryth?
|
|
74
|
+
|
|
75
|
+
LLM-based agents need **memory** — but not just any memory. They need structured, token-efficient, deduplicated memory that fits inside a context window. Kryth gives you:
|
|
76
|
+
|
|
77
|
+
| Without Kryth | With Kryth |
|
|
78
|
+
|---|---|
|
|
79
|
+
| ❌ Bloated, unstructured context | ✅ Token-capped retrieval (~800 tokens) |
|
|
80
|
+
| ❌ Duplicate reads and redundant commands | ✅ Smart duplicate detection & summarization |
|
|
81
|
+
| ❌ Runaway memory growth | ✅ Automatic compression with configurable limits |
|
|
82
|
+
| ❌ Ad-hoc memory management | ✅ Clean, modular controller architecture |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 🚀 Quick Start
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from kryth import WriteController, RetrievalController, compute_state_hash
|
|
90
|
+
|
|
91
|
+
# Compute a unique state hash for your project
|
|
92
|
+
state_hash = compute_state_hash(cwd="/path/to/project")
|
|
93
|
+
|
|
94
|
+
# Route knowledge from tool calls into memory
|
|
95
|
+
write_ctrl = WriteController()
|
|
96
|
+
write_ctrl.on_tool_result(
|
|
97
|
+
tool_name="read_file",
|
|
98
|
+
args={"path": "main.py"},
|
|
99
|
+
result="file contents...",
|
|
100
|
+
error=False,
|
|
101
|
+
session_id=1,
|
|
102
|
+
memory_manager=memory_manager, # Your MemoryManager instance
|
|
103
|
+
turn=3,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Retrieve only the most relevant memories for LLM context
|
|
107
|
+
retrieval_ctrl = RetrievalController()
|
|
108
|
+
context_block = retrieval_ctrl.build_prompt_block(
|
|
109
|
+
memory_manager=memory_manager,
|
|
110
|
+
session_id=1,
|
|
111
|
+
user_input="What does the auth module do?",
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
> 💡 **Tip:** Kryth is designed to integrate with your existing `MemoryManager`. See the [full documentation](#) for setup details.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## ✨ Features
|
|
120
|
+
|
|
121
|
+
<table>
|
|
122
|
+
<tr>
|
|
123
|
+
<td width="50%">
|
|
124
|
+
<h3>🧩 <strong>Modular Architecture</strong></h3>
|
|
125
|
+
<p>Each memory concern — writing, retrieval, deduplication, compression — is isolated in its own controller. Mix and match what you need.</p>
|
|
126
|
+
</td>
|
|
127
|
+
<td width="50%">
|
|
128
|
+
<h3>📏 <strong>Token-Aware Retrieval</strong></h3>
|
|
129
|
+
<p>Context injection is <strong>always capped at ~800 tokens</strong>, ensuring your LLM stays focused and never exceeds context limits.</p>
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
<tr>
|
|
133
|
+
<td width="50%">
|
|
134
|
+
<h3>🎯 <strong>Relevance Ranking</strong></h3>
|
|
135
|
+
<p>Memories are scored and ranked by importance × query relevance, so only the most pertinent information makes it into the prompt.</p>
|
|
136
|
+
</td>
|
|
137
|
+
<td width="50%">
|
|
138
|
+
<h3>🔍 <strong>Smart Duplicate Detection</strong></h3>
|
|
139
|
+
<p>Detects duplicate file reads and command executions — then returns a <em>summary</em> instead of hard-blocking. Never repeat work.</p>
|
|
140
|
+
</td>
|
|
141
|
+
</tr>
|
|
142
|
+
<tr>
|
|
143
|
+
<td width="50%">
|
|
144
|
+
<h3>🧹 <strong>Automatic Compression</strong></h3>
|
|
145
|
+
<p>Prevents unbounded memory growth with configurable limits per memory layer. Set it and forget it.</p>
|
|
146
|
+
</td>
|
|
147
|
+
<td width="50%">
|
|
148
|
+
<h3>🔐 <strong>State Hashing</strong></h3>
|
|
149
|
+
<p>Robust duplicate command detection using git diff, file hashes, and environment variables. Know when state has genuinely changed.</p>
|
|
150
|
+
</td>
|
|
151
|
+
</tr>
|
|
152
|
+
</table>
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 🏗️ Architecture
|
|
157
|
+
|
|
158
|
+
Kryth organizes memory into a clean, layered architecture:
|
|
159
|
+
|
|
160
|
+
```mermaid
|
|
161
|
+
flowchart TB
|
|
162
|
+
subgraph Input["📥 Inputs"]
|
|
163
|
+
TC[Tool Calls]
|
|
164
|
+
UI[User Input]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
subgraph Controllers["🎮 Controllers"]
|
|
168
|
+
WC[WriteController]
|
|
169
|
+
RD[DuplicateDetector]
|
|
170
|
+
RC[RetrievalController]
|
|
171
|
+
CC[CompressionController]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
subgraph Memory["💾 Memory Layers"]
|
|
175
|
+
RM[RepoMemory<br/>File contents, symbols]
|
|
176
|
+
EM[ExecutionMemory<br/>Command history]
|
|
177
|
+
EPM[EpisodicMemory<br/>Edits, decisions]
|
|
178
|
+
WM[WorkingMemory<br/>Objective, blockers]
|
|
179
|
+
LTM[LongTermMemory<br/>Summarized insights]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
TC --> WC
|
|
183
|
+
WC --> RM
|
|
184
|
+
WC --> EM
|
|
185
|
+
WC --> EPM
|
|
186
|
+
WC --> WM
|
|
187
|
+
WC --> RD
|
|
188
|
+
RD -.->|Soft dedup| TC
|
|
189
|
+
|
|
190
|
+
UI --> RC
|
|
191
|
+
RC --> RM
|
|
192
|
+
RC --> EM
|
|
193
|
+
RC --> EPM
|
|
194
|
+
RC --> WM
|
|
195
|
+
RC -.->|Optional| LTM
|
|
196
|
+
|
|
197
|
+
CC --> EPM
|
|
198
|
+
CC --> LTM
|
|
199
|
+
CC --> EM
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Memory Layers
|
|
203
|
+
|
|
204
|
+
| Layer | Purpose | Written By |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| **RepoMemory** | File contents, structure, symbols | `read_file` |
|
|
207
|
+
| **ExecutionMemory** | Command history and results | `run_command` |
|
|
208
|
+
| **EpisodicMemory** | Edits, decisions, findings | `edit_file`, `write_file` |
|
|
209
|
+
| **WorkingMemory** | Current objective and blockers | `set_objective` |
|
|
210
|
+
| **LongTermMemory** | Summarized insights | Compression |
|
|
211
|
+
|
|
212
|
+
### Retrieval Priority
|
|
213
|
+
|
|
214
|
+
The `RetrievalController` builds context blocks in **strict priority order**, ensuring the most critical information is always included:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
🥇 Current objective (always included)
|
|
218
|
+
🥈 Working memory findings + blockers
|
|
219
|
+
🥉 Top repo memory hits (ranked by importance × relevance)
|
|
220
|
+
Recent command execution results
|
|
221
|
+
Critical episodic findings
|
|
222
|
+
Long-term memory summaries (optional)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 📦 Installation
|
|
228
|
+
|
|
229
|
+
**Stable release** (recommended):
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install kryth
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Development version**:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
git clone https://github.com/navadeep0508/KRYTH-OS.git
|
|
239
|
+
cd kryth
|
|
240
|
+
pip install -e ".[dev]"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 🧪 Development
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Set up environment
|
|
249
|
+
python -m venv venv
|
|
250
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
251
|
+
pip install -e ".[dev]"
|
|
252
|
+
|
|
253
|
+
# Install pre-commit hooks
|
|
254
|
+
pre-commit install
|
|
255
|
+
|
|
256
|
+
# Run tests
|
|
257
|
+
pytest
|
|
258
|
+
|
|
259
|
+
# Code quality
|
|
260
|
+
ruff check src/
|
|
261
|
+
mypy src/
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 🗺️ Roadmap
|
|
267
|
+
|
|
268
|
+
- [x] Core memory controllers (Write, Retrieve, Deduplicate, Compress)
|
|
269
|
+
- [x] Token-capped context retrieval
|
|
270
|
+
- [ ] Integration with popular agent frameworks (LangChain, CrewAI)
|
|
271
|
+
- [ ] Persistent storage backends (SQLite, PostgreSQL, Redis)
|
|
272
|
+
- [ ] Async-first API
|
|
273
|
+
- [ ] Interactive memory dashboard
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 🤝 Contributing
|
|
278
|
+
|
|
279
|
+
Contributions are what make the open source community such an amazing place! Any contributions you make are **greatly appreciated**.
|
|
280
|
+
|
|
281
|
+
- 🐛 Found a bug? [Open an issue](https://github.com/navadeep0508/KRYTH-OS/issues)
|
|
282
|
+
- 💡 Have an idea? [Start a discussion](https://github.com/navadeep0508/KRYTH-OS/discussions)
|
|
283
|
+
- 🔧 Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 📄 License
|
|
288
|
+
|
|
289
|
+
Distributed under the **MIT License**. See [`LICENSE`](LICENSE) for more information.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
<div align="center">
|
|
294
|
+
<p>
|
|
295
|
+
Made with ❤️ by <a href="https://github.com/navadeep0508">navadeep0508</a>
|
|
296
|
+
</p>
|
|
297
|
+
<p>
|
|
298
|
+
<a href="https://github.com/navadeep0508/KRYTH-OS">GitHub</a> •
|
|
299
|
+
<a href="https://pypi.org/project/kryth/">PyPI</a> •
|
|
300
|
+
<a href="CHANGELOG.md">Changelog</a> •
|
|
301
|
+
<a href="CODE_OF_CONDUCT.md">Code of Conduct</a>
|
|
302
|
+
</p>
|
|
303
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
kryth/__init__.py,sha256=2_ni1dmz-5K6y0f6gKEkJHySLn50iXNv1o8RzkFetZc,711
|
|
2
|
+
kryth/agent/__init__.py,sha256=xbT9pJFwCWRps32v6y1s0VAUAEzpEqKKdz2wlc2008Y,41
|
|
3
|
+
kryth/agent/memory/__init__.py,sha256=moq3lqQheZvNTkzByYrWuUyORnl22VY9yPewCr39VkA,471
|
|
4
|
+
kryth/agent/memory/controllers.py,sha256=w9JQEsghyGK53LmS9NuFctrSnhs198piXhvyhqEwHwE,16685
|
|
5
|
+
kryth-0.1.0.dist-info/licenses/LICENSE,sha256=NAbUpPHgo4xtuaN2PlRZWE6-cSxhMaTybd4hc--PjGs,1062
|
|
6
|
+
kryth-0.1.0.dist-info/METADATA,sha256=_Fde4EWZ7L7PEBN1A7jBijC2KRaA0ShUrHgm2ht28G0,9323
|
|
7
|
+
kryth-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
kryth-0.1.0.dist-info/top_level.txt,sha256=8gUYy5gbkp2v_rDrvdeipvrt5u7eJwjstnnRPXVRrgw,6
|
|
9
|
+
kryth-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kryth
|
|
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 @@
|
|
|
1
|
+
kryth
|