agentdatabase 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.
- agentdatabase-0.1.0.dist-info/METADATA +847 -0
- agentdatabase-0.1.0.dist-info/RECORD +35 -0
- agentdatabase-0.1.0.dist-info/WHEEL +5 -0
- agentdatabase-0.1.0.dist-info/entry_points.txt +2 -0
- agentdatabase-0.1.0.dist-info/licenses/LICENSE +651 -0
- agentdatabase-0.1.0.dist-info/top_level.txt +1 -0
- agentdb/__init__.py +5 -0
- agentdb/adapters/claude_agent_sdk.py +831 -0
- agentdb/adapters/hermes.py +247 -0
- agentdb/backend.py +75 -0
- agentdb/core/__init__.py +19 -0
- agentdb/core/directory_tracking.py +59 -0
- agentdb/core/file_integrity.py +79 -0
- agentdb/core/models.py +90 -0
- agentdb/core/profiles.py +116 -0
- agentdb/core/store.py +373 -0
- agentdb/core/system.py +86 -0
- agentdb/embeddings/__init__.py +7 -0
- agentdb/embeddings/provider.py +99 -0
- agentdb/embeddings/store.py +199 -0
- agentdb/embeddings/text.py +20 -0
- agentdb/gateway/__init__.py +189 -0
- agentdb/gateway/adapter.py +58 -0
- agentdb/governance/__init__.py +3 -0
- agentdb/governance/conflict_detector.py +103 -0
- agentdb/governance/lifecycle_manager.py +226 -0
- agentdb/governance/permission_router.py +131 -0
- agentdb/interface/__init__.py +25 -0
- agentdb/interface/client.py +1108 -0
- agentdb/interface/mcp_server.py +96 -0
- agentdb/retrieval/__init__.py +3 -0
- agentdb/retrieval/algorithm.py +207 -0
- agentdb/skills/__init__.py +3 -0
- agentdb/skills/skill_store.py +351 -0
- agentdb/testing.py +68 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentDBHermesProvider — Hermes MemoryProvider adapter.
|
|
3
|
+
|
|
4
|
+
Implements Hermes's MemoryProvider ABC on top of GatewayAdapter so that
|
|
5
|
+
any Hermes agent can use AgentDB as its governed external memory backend.
|
|
6
|
+
Direct in-process import; no sidecar, no HTTP.
|
|
7
|
+
|
|
8
|
+
Principle 12 compliance:
|
|
9
|
+
12a hooks (real-time, synchronous): prefetch, sync_turn, on_turn_start,
|
|
10
|
+
on_memory_write, handle_tool_call, system_prompt_block
|
|
11
|
+
12b hooks (async/decoupled): on_session_end, on_pre_compress, on_delegation
|
|
12
|
+
|
|
13
|
+
Limitation: on_memory_write cannot veto Hermes-native writes — it fires
|
|
14
|
+
after the write has already landed in Hermes's own store. AgentDB governance
|
|
15
|
+
covers AgentDB's own records only, not Hermes-native storage.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from agent.memory_provider import MemoryProvider # real Hermes install
|
|
25
|
+
except ImportError:
|
|
26
|
+
# Hermes not installed — stub ABC so tests run without it
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
|
|
29
|
+
class MemoryProvider(ABC): # type: ignore[no-redef]
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def initialize(self, session_id: str, **kwargs) -> None: ...
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def system_prompt_block(self) -> str: ...
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def prefetch(self, query: str, *, session_id: str) -> list: ...
|
|
36
|
+
def queue_prefetch(self, query: str, *, session_id: str) -> None: ...
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def sync_turn(self, user: str, asst: str, *, session_id: str) -> None: ...
|
|
39
|
+
def get_tool_schemas(self) -> list: ...
|
|
40
|
+
def handle_tool_call(self, name: str, args: dict, *, session_id: str): ...
|
|
41
|
+
def shutdown(self) -> None: ...
|
|
42
|
+
def on_turn_start(self, turn: int, message: str, **kwargs) -> None: ...
|
|
43
|
+
def on_session_end(self, messages: list) -> None: ...
|
|
44
|
+
def on_session_switch(self, new_session_id: str, **kwargs) -> None: ...
|
|
45
|
+
def on_pre_compress(self, messages: list) -> str: ...
|
|
46
|
+
def on_memory_write(self, action: str, target: str, content: str, metadata: dict) -> None: ...
|
|
47
|
+
def on_delegation(self, task: str, result: str, **kwargs) -> None: ...
|
|
48
|
+
def backup_paths(self) -> list[str]: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
from agentdb.gateway.adapter import GatewayAdapter
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from agentdb import AgentDB
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _utcnow() -> datetime:
|
|
58
|
+
return datetime.now(timezone.utc)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AgentDBHermesProvider(MemoryProvider, GatewayAdapter):
|
|
62
|
+
"""
|
|
63
|
+
Hermes MemoryProvider backed by AgentDB.
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
db = AgentDB(db_path="agentdb.sqlite")
|
|
67
|
+
provider = AgentDBHermesProvider(db=db, agent_id="coder")
|
|
68
|
+
# Register with Hermes MemoryManager, then let Hermes call hooks.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, db: "AgentDB", agent_id: str) -> None:
|
|
72
|
+
GatewayAdapter.__init__(self, db, agent_id, profile=db.config_for(agent_id)["name"])
|
|
73
|
+
self._session_id: str = ""
|
|
74
|
+
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
# Phase 1 — core governed memory loop (Principle 12a)
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def initialize(self, session_id: str, **kwargs) -> None:
|
|
80
|
+
"""Store session_id, wire conflict handler, dispatch conversation_started."""
|
|
81
|
+
self._session_id = session_id
|
|
82
|
+
self.gateway.subscribe("conflict_detected", self._on_conflict)
|
|
83
|
+
self.gateway.dispatch("conversation_started", session_id=session_id)
|
|
84
|
+
|
|
85
|
+
def prefetch(self, query: str, *, session_id: str) -> list:
|
|
86
|
+
"""Return governed records ranked for this query. [12a]"""
|
|
87
|
+
result = self._db.retrieve(query, agent_id=self._agent_id, session_id=session_id)
|
|
88
|
+
return result.records
|
|
89
|
+
|
|
90
|
+
def sync_turn(self, user: str, asst: str, *, session_id: str) -> None:
|
|
91
|
+
"""Persist assistant reply as a governed episodic record. [12a]"""
|
|
92
|
+
if asst:
|
|
93
|
+
self._db.write(
|
|
94
|
+
key=f"memory/hermes/{self._agent_id}/{session_id}/turn",
|
|
95
|
+
value=asst,
|
|
96
|
+
origin="agent_inferred",
|
|
97
|
+
agent_id=self._agent_id,
|
|
98
|
+
session_id=session_id,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def on_turn_start(self, turn: int, message: str, **kwargs) -> None:
|
|
102
|
+
"""Dispatch reconciliation trigger at the start of each turn. [12a]"""
|
|
103
|
+
self.gateway.dispatch(
|
|
104
|
+
"conversation_started",
|
|
105
|
+
session_id=self._session_id,
|
|
106
|
+
turn=turn,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# system_prompt_block [12a]
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def system_prompt_block(self) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Inject governed skills into Hermes's system prompt.
|
|
116
|
+
|
|
117
|
+
Returns a '## Governed Skills' section if any skills exist for this
|
|
118
|
+
agent; empty string otherwise.
|
|
119
|
+
"""
|
|
120
|
+
skills = self._db.skills.get_for_agent(self._agent_id)
|
|
121
|
+
if not skills:
|
|
122
|
+
return ""
|
|
123
|
+
lines = ["## Governed Skills"]
|
|
124
|
+
for skill in skills:
|
|
125
|
+
warning = ""
|
|
126
|
+
if skill.health_score < 0.6:
|
|
127
|
+
warning = f" [STALE — health_score={skill.health_score:.2f}]"
|
|
128
|
+
lines.append(f"### {skill.name}{warning}")
|
|
129
|
+
lines.append(skill.content)
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Remaining hooks
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def queue_prefetch(self, query: str, *, session_id: str) -> None:
|
|
137
|
+
"""No-op — AgentDB retrieval is always a live query; nothing to pre-warm."""
|
|
138
|
+
|
|
139
|
+
def get_tool_schemas(self) -> list:
|
|
140
|
+
"""No tool schemas exposed — AgentDB governance is transparent to the LLM."""
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
def handle_tool_call(self, name: str, args: dict, *, session_id: str) -> Any:
|
|
144
|
+
"""Route memory tool calls to db.read() or db.retrieve(). [12a]"""
|
|
145
|
+
if name == "memory_get":
|
|
146
|
+
key = args.get("key", "")
|
|
147
|
+
return self._db.read(key, agent_id=self._agent_id)
|
|
148
|
+
if name == "memory_search":
|
|
149
|
+
query = args.get("query", "")
|
|
150
|
+
return self._db.retrieve(query, agent_id=self._agent_id, session_id=session_id).records
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
def shutdown(self) -> None:
|
|
154
|
+
"""No-op — SQLite closes with the process."""
|
|
155
|
+
|
|
156
|
+
def on_session_end(self, messages: list) -> None:
|
|
157
|
+
"""Record session outcome and check for pending conflicts. [12b]"""
|
|
158
|
+
if self._session_id:
|
|
159
|
+
self._db.lifecycle.record_outcome(
|
|
160
|
+
session_id=self._session_id,
|
|
161
|
+
agent_id=self._agent_id,
|
|
162
|
+
outcome_type="session_end",
|
|
163
|
+
outcome_value=1.0,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def on_session_switch(self, new_session_id: str, **kwargs) -> None:
|
|
167
|
+
"""Re-scope internal session reference on session rotation."""
|
|
168
|
+
self._session_id = new_session_id
|
|
169
|
+
|
|
170
|
+
def on_pre_compress(self, messages: list) -> str:
|
|
171
|
+
"""
|
|
172
|
+
Call configured summarizer if available; write summary as governed record.
|
|
173
|
+
Returns summary string, or '' to let Hermes handle compaction natively. [12b]
|
|
174
|
+
"""
|
|
175
|
+
summarizer: Optional[Callable] = self._db._summarizer
|
|
176
|
+
if summarizer is None:
|
|
177
|
+
return ""
|
|
178
|
+
summary: str = summarizer(messages)
|
|
179
|
+
ts = _utcnow().strftime("%Y%m%dT%H%M%SZ")
|
|
180
|
+
self._db.write(
|
|
181
|
+
key=f"memory/episodic/hermes/{self._agent_id}/{self._session_id}/compaction-{ts}",
|
|
182
|
+
value=summary,
|
|
183
|
+
origin="agent_inferred",
|
|
184
|
+
agent_id=self._agent_id,
|
|
185
|
+
session_id=self._session_id,
|
|
186
|
+
)
|
|
187
|
+
return summary
|
|
188
|
+
|
|
189
|
+
def on_memory_write(
|
|
190
|
+
self,
|
|
191
|
+
action: str,
|
|
192
|
+
target: str,
|
|
193
|
+
content: str,
|
|
194
|
+
metadata: dict,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Mirror Hermes-native memory writes for AgentDB governance audit. [12a]
|
|
198
|
+
|
|
199
|
+
NOTE: This hook cannot veto Hermes-native writes — it fires after
|
|
200
|
+
the write has already landed in Hermes's own store. Governance here
|
|
201
|
+
covers AgentDB's own records only.
|
|
202
|
+
|
|
203
|
+
Scope: only writes targeting MEMORY.md or DREAMS.md are governed;
|
|
204
|
+
high-frequency ephemeral daily notes are skipped.
|
|
205
|
+
"""
|
|
206
|
+
governed_targets = {"MEMORY.md", "DREAMS.md"}
|
|
207
|
+
if Path(target).name not in governed_targets:
|
|
208
|
+
return
|
|
209
|
+
# Permission check is intentionally skipped here: this is a governance mirror of a
|
|
210
|
+
# Hermes-native write that already passed Hermes's own permission checks. AgentDB
|
|
211
|
+
# records the write for provenance tracking but cannot and should not re-gate it.
|
|
212
|
+
self._db.write(
|
|
213
|
+
key=f"memory/hermes-mirror/{self._agent_id}/{target}",
|
|
214
|
+
value=content,
|
|
215
|
+
origin="agent_inferred",
|
|
216
|
+
agent_id=self._agent_id,
|
|
217
|
+
session_id=self._session_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def on_delegation(self, task: str, result: str, **kwargs) -> None:
|
|
221
|
+
"""Write subagent task result as a governed episodic record. [12b]"""
|
|
222
|
+
if result:
|
|
223
|
+
ts = _utcnow().strftime("%Y%m%dT%H%M%SZ")
|
|
224
|
+
self._db.write(
|
|
225
|
+
key=f"memory/episodic/hermes/{self._agent_id}/{self._session_id}/delegation-{ts}",
|
|
226
|
+
value=result,
|
|
227
|
+
origin="agent_inferred",
|
|
228
|
+
agent_id=self._agent_id,
|
|
229
|
+
session_id=self._session_id,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def backup_paths(self) -> list[str]:
|
|
233
|
+
"""Return the AgentDB file path for backup tooling."""
|
|
234
|
+
if hasattr(self._db, "_path") and self._db._path and str(self._db._path) != ":memory:":
|
|
235
|
+
return [str(self._db._path)]
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
# Internal handlers
|
|
240
|
+
# ------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
def _on_conflict(self, **payload) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Platform-agnostic conflict handler.
|
|
245
|
+
Concrete notification wiring is platform-specific (Slack, Telegram, etc.).
|
|
246
|
+
"""
|
|
247
|
+
pass
|
agentdb/backend.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from .governance.permission_router import PermissionDenied
|
|
7
|
+
from .interface.client import AgentDB
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentDBBackend:
|
|
11
|
+
"""
|
|
12
|
+
A simple key-value interface over AgentDB for a specific agent.
|
|
13
|
+
|
|
14
|
+
trust_zone: "agent_inferred" or "human_approved"
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
agent_id: str,
|
|
20
|
+
trust_zone: str,
|
|
21
|
+
db: AgentDB,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._agent_id = agent_id
|
|
24
|
+
self._trust_zone = trust_zone
|
|
25
|
+
self.db = db
|
|
26
|
+
|
|
27
|
+
def write(self, key: str, value: Any) -> None:
|
|
28
|
+
"""Write a value. Raises PermissionDenied if not allowed."""
|
|
29
|
+
# Derive origin from trust_zone
|
|
30
|
+
origin = "human_approved" if self._trust_zone == "human_approved" else "agent_inferred"
|
|
31
|
+
self.db.write(
|
|
32
|
+
key=key,
|
|
33
|
+
value=value,
|
|
34
|
+
origin=origin,
|
|
35
|
+
agent_id=self._agent_id,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def read(self, key: str) -> Optional[Any]:
|
|
39
|
+
"""Read a value by key. Returns None if not found."""
|
|
40
|
+
record = self.db.read(key, agent_id=self._agent_id)
|
|
41
|
+
if record is None:
|
|
42
|
+
return None
|
|
43
|
+
return record.value
|
|
44
|
+
|
|
45
|
+
def delete(self, key: str) -> None:
|
|
46
|
+
"""Delete/archive a record by key. Raises PermissionDenied if not allowed."""
|
|
47
|
+
self.db.delete(key, agent_id=self._agent_id, trust_zone=self._trust_zone)
|
|
48
|
+
|
|
49
|
+
def list(self, prefix: str) -> list[str]:
|
|
50
|
+
"""List all keys that start with prefix, scoped to this agent."""
|
|
51
|
+
records = self.db._store.list_by_agent(self._agent_id)
|
|
52
|
+
return [r.key for r in records if r.key.startswith(prefix)]
|
|
53
|
+
|
|
54
|
+
def grep(self, pattern: str, prefix: str = "") -> list[str]:
|
|
55
|
+
"""Return keys whose values contain the given string pattern, scoped to this agent."""
|
|
56
|
+
records = self.db._store.list_by_agent(self._agent_id)
|
|
57
|
+
results: list[str] = []
|
|
58
|
+
pattern_lower = pattern.lower()
|
|
59
|
+
for record in records:
|
|
60
|
+
if prefix and not record.key.startswith(prefix):
|
|
61
|
+
continue
|
|
62
|
+
import json
|
|
63
|
+
value_str = (
|
|
64
|
+
json.dumps(record.value)
|
|
65
|
+
if isinstance(record.value, (dict, list))
|
|
66
|
+
else str(record.value)
|
|
67
|
+
).lower()
|
|
68
|
+
if pattern_lower in value_str:
|
|
69
|
+
results.append(record.key)
|
|
70
|
+
return results
|
|
71
|
+
|
|
72
|
+
def glob(self, pattern: str) -> list[str]:
|
|
73
|
+
"""Return keys matching an fnmatch glob pattern, scoped to this agent."""
|
|
74
|
+
records = self.db._store.list_by_agent(self._agent_id)
|
|
75
|
+
return [r.key for r in records if fnmatch.fnmatch(r.key, pattern)]
|
agentdb/core/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .models import (
|
|
2
|
+
Origin,
|
|
3
|
+
TrustLevel,
|
|
4
|
+
ExpiryPolicy,
|
|
5
|
+
MemoryTier,
|
|
6
|
+
MemoryRecord,
|
|
7
|
+
ConflictRecord,
|
|
8
|
+
Skill,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Origin",
|
|
13
|
+
"TrustLevel",
|
|
14
|
+
"ExpiryPolicy",
|
|
15
|
+
"MemoryTier",
|
|
16
|
+
"MemoryRecord",
|
|
17
|
+
"ConflictRecord",
|
|
18
|
+
"Skill",
|
|
19
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def collect_directory_snapshots(
|
|
8
|
+
directories: list[str],
|
|
9
|
+
base_dir: Any = ".",
|
|
10
|
+
max_files_per_directory: int = 200,
|
|
11
|
+
) -> list[dict]:
|
|
12
|
+
"""
|
|
13
|
+
Collect a snapshot of each directory (relative to base_dir).
|
|
14
|
+
|
|
15
|
+
Returns a list of dicts:
|
|
16
|
+
{directory, exists, file_count, entries: [{path, ...}]}
|
|
17
|
+
|
|
18
|
+
paths in entries are relative to base_dir.
|
|
19
|
+
If a directory doesn't exist: {directory, exists=False, file_count=0, entries=[]}
|
|
20
|
+
"""
|
|
21
|
+
base = Path(base_dir)
|
|
22
|
+
results: list[dict] = []
|
|
23
|
+
|
|
24
|
+
for directory in directories:
|
|
25
|
+
full_path = base / directory
|
|
26
|
+
if not full_path.exists():
|
|
27
|
+
results.append(
|
|
28
|
+
{
|
|
29
|
+
"directory": directory,
|
|
30
|
+
"exists": False,
|
|
31
|
+
"file_count": 0,
|
|
32
|
+
"entries": [],
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
entries: list[dict] = []
|
|
38
|
+
count = 0
|
|
39
|
+
for item in sorted(full_path.rglob("*")):
|
|
40
|
+
if item.is_file():
|
|
41
|
+
if count >= max_files_per_directory:
|
|
42
|
+
break
|
|
43
|
+
try:
|
|
44
|
+
rel = item.relative_to(base)
|
|
45
|
+
entries.append({"path": str(rel)})
|
|
46
|
+
except ValueError:
|
|
47
|
+
entries.append({"path": str(item)})
|
|
48
|
+
count += 1
|
|
49
|
+
|
|
50
|
+
results.append(
|
|
51
|
+
{
|
|
52
|
+
"directory": directory,
|
|
53
|
+
"exists": True,
|
|
54
|
+
"file_count": count,
|
|
55
|
+
"entries": entries,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return results
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _utcnow() -> datetime:
|
|
11
|
+
return datetime.now(timezone.utc)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_file_sha256(path: Any) -> str:
|
|
15
|
+
"""Return the SHA-256 hex digest (64 chars) of the file at path."""
|
|
16
|
+
p = Path(path)
|
|
17
|
+
h = hashlib.sha256()
|
|
18
|
+
with p.open("rb") as f:
|
|
19
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
20
|
+
h.update(chunk)
|
|
21
|
+
return h.hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FileObservation:
|
|
26
|
+
path: str
|
|
27
|
+
sha256: str
|
|
28
|
+
size_bytes: int
|
|
29
|
+
captured_at: str
|
|
30
|
+
modified_at: str
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return {
|
|
34
|
+
"path": self.path,
|
|
35
|
+
"sha256": self.sha256,
|
|
36
|
+
"size_bytes": self.size_bytes,
|
|
37
|
+
"captured_at": self.captured_at,
|
|
38
|
+
"modified_at": self.modified_at,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def observe_file(path: Any, base_dir: Optional[Any] = None) -> FileObservation:
|
|
43
|
+
"""
|
|
44
|
+
Observe a single file and return a FileObservation.
|
|
45
|
+
|
|
46
|
+
path: the file to observe (absolute or relative)
|
|
47
|
+
base_dir: if provided, the returned .path is relative to base_dir
|
|
48
|
+
"""
|
|
49
|
+
p = Path(path)
|
|
50
|
+
sha = compute_file_sha256(p)
|
|
51
|
+
stat = p.stat()
|
|
52
|
+
size_bytes = stat.st_size
|
|
53
|
+
captured_at = _utcnow().isoformat()
|
|
54
|
+
modified_at = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
|
|
55
|
+
|
|
56
|
+
if base_dir is not None:
|
|
57
|
+
try:
|
|
58
|
+
rel_path = str(p.relative_to(Path(base_dir)))
|
|
59
|
+
except ValueError:
|
|
60
|
+
rel_path = str(p)
|
|
61
|
+
else:
|
|
62
|
+
rel_path = str(p)
|
|
63
|
+
|
|
64
|
+
return FileObservation(
|
|
65
|
+
path=rel_path,
|
|
66
|
+
sha256=sha,
|
|
67
|
+
size_bytes=size_bytes,
|
|
68
|
+
captured_at=captured_at,
|
|
69
|
+
modified_at=modified_at,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def observe_files(paths: list) -> list[dict]:
|
|
74
|
+
"""Observe multiple files and return a list of to_dict() results."""
|
|
75
|
+
results = []
|
|
76
|
+
for p in paths:
|
|
77
|
+
obs = observe_file(p)
|
|
78
|
+
results.append(obs.to_dict())
|
|
79
|
+
return results
|
agentdb/core/models.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Origin(str, Enum):
|
|
11
|
+
HUMAN_APPROVED = "human_approved"
|
|
12
|
+
AGENT_INFERRED = "agent_inferred"
|
|
13
|
+
PROVIDER_INGESTED = "provider_ingested"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TrustLevel(str, Enum):
|
|
17
|
+
SEALED = "sealed"
|
|
18
|
+
HUMAN = "human"
|
|
19
|
+
AGENT = "agent"
|
|
20
|
+
PROVIDER = "provider"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExpiryPolicy(str, Enum):
|
|
24
|
+
PERMANENT = "permanent"
|
|
25
|
+
EPISODIC = "episodic"
|
|
26
|
+
SESSION = "session"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MemoryTier(str, Enum):
|
|
30
|
+
WORKING = "working"
|
|
31
|
+
EPISODIC = "episodic"
|
|
32
|
+
LONG_TERM = "long_term"
|
|
33
|
+
ORG = "org"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _utcnow() -> datetime:
|
|
37
|
+
return datetime.now(timezone.utc)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _new_uuid() -> str:
|
|
41
|
+
return str(uuid.uuid4())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class MemoryRecord:
|
|
46
|
+
key: str
|
|
47
|
+
value: Any
|
|
48
|
+
origin: str # Origin enum value or raw string
|
|
49
|
+
trust_level: str # TrustLevel enum value or raw string
|
|
50
|
+
agent_id: str
|
|
51
|
+
id: str = field(default_factory=_new_uuid)
|
|
52
|
+
confidence: float = 1.0
|
|
53
|
+
version: int = 1
|
|
54
|
+
archived: bool = False
|
|
55
|
+
retrieval_count: int = 0
|
|
56
|
+
created_at: datetime = field(default_factory=_utcnow)
|
|
57
|
+
entities: list = field(default_factory=list)
|
|
58
|
+
last_retrieved: Optional[datetime] = None
|
|
59
|
+
conflict_id: Optional[str] = None
|
|
60
|
+
expiry_time: Optional[datetime] = None
|
|
61
|
+
inferred: bool = False
|
|
62
|
+
working_dirs: list = field(default_factory=list)
|
|
63
|
+
embedding: Optional[list[float]] = field(default=None, repr=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ConflictRecord:
|
|
68
|
+
record_a_id: str
|
|
69
|
+
record_b_id: str
|
|
70
|
+
conflict_type: str
|
|
71
|
+
id: str = field(default_factory=_new_uuid)
|
|
72
|
+
resolved: bool = False
|
|
73
|
+
auto_resolved: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class Skill:
|
|
78
|
+
id: str
|
|
79
|
+
name: str
|
|
80
|
+
content: str
|
|
81
|
+
authored_by: str
|
|
82
|
+
scope: list
|
|
83
|
+
version: str = "1.0.0"
|
|
84
|
+
archived: bool = False
|
|
85
|
+
health_score: float = 1.0
|
|
86
|
+
health_score_reasons: list = field(default_factory=list)
|
|
87
|
+
created_at: datetime = field(default_factory=_utcnow)
|
|
88
|
+
updated_at: datetime = field(default_factory=_utcnow)
|
|
89
|
+
last_retrieved: Optional[datetime] = None
|
|
90
|
+
parent_skill_id: Optional[str] = None
|
agentdb/core/profiles.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in AgentDB profiles.
|
|
3
|
+
|
|
4
|
+
To add a new built-in profile: define a new Profile(...) below and add it
|
|
5
|
+
to BUILTIN_PROFILES. To change what "budget_optimized" means: edit the
|
|
6
|
+
BUDGET_OPTIMIZED instance directly -- every field is named, typed, and
|
|
7
|
+
lives on its own dataclass below.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class LifecycleSettings:
|
|
16
|
+
confidence_decay_rate: float
|
|
17
|
+
archival_threshold: float
|
|
18
|
+
staleness_days: int
|
|
19
|
+
reinforce_rate: float
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class RankerWeights:
|
|
24
|
+
provenance: float
|
|
25
|
+
recency: float
|
|
26
|
+
confidence: float
|
|
27
|
+
causal: float
|
|
28
|
+
semantic: float
|
|
29
|
+
workspace: float = 0.0 # opt-in; weight 0.0 = disabled
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class RetrievalSettings:
|
|
34
|
+
retrieve_limit: int
|
|
35
|
+
conflict_threshold: float # currently unused elsewhere in the codebase; reserved
|
|
36
|
+
min_composite_score: float # records ranked below this score are excluded; 0.0 = no threshold
|
|
37
|
+
ranker_weights: RankerWeights
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class InjectionSettings:
|
|
42
|
+
context_token_limit: int
|
|
43
|
+
memory_budget_chars: int
|
|
44
|
+
memory_header: str
|
|
45
|
+
override_prefix: str
|
|
46
|
+
record_line_format: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class CaptureSettings:
|
|
51
|
+
conversation_jsonl: bool
|
|
52
|
+
tracking_max_files_per_directory: int
|
|
53
|
+
auto_persist_response: bool
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Profile:
|
|
58
|
+
name: str
|
|
59
|
+
lifecycle: LifecycleSettings
|
|
60
|
+
retrieval: RetrievalSettings
|
|
61
|
+
injection: InjectionSettings
|
|
62
|
+
capture: CaptureSettings
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_SHARED_INJECTION_TEXT = dict(
|
|
66
|
+
memory_header="Background context from previous sessions (supplemental — current conversation and user request take priority):",
|
|
67
|
+
override_prefix="Override instruction:",
|
|
68
|
+
record_line_format="- {key}: {value}",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
BUDGET_OPTIMIZED = Profile(
|
|
72
|
+
name="budget_optimized",
|
|
73
|
+
lifecycle=LifecycleSettings(
|
|
74
|
+
confidence_decay_rate=0.0, archival_threshold=0.10, staleness_days=90, reinforce_rate=0.10,
|
|
75
|
+
),
|
|
76
|
+
retrieval=RetrievalSettings(
|
|
77
|
+
retrieve_limit=5, conflict_threshold=0.85, min_composite_score=0.0,
|
|
78
|
+
ranker_weights=RankerWeights(provenance=0.35, recency=0.25, confidence=0.25, causal=0.15, semantic=0.0, workspace=0.0),
|
|
79
|
+
),
|
|
80
|
+
injection=InjectionSettings(context_token_limit=1000, memory_budget_chars=2000, **_SHARED_INJECTION_TEXT),
|
|
81
|
+
capture=CaptureSettings(
|
|
82
|
+
conversation_jsonl=False, tracking_max_files_per_directory=200, auto_persist_response=False,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
FULL_STORAGE = Profile(
|
|
87
|
+
name="full_storage",
|
|
88
|
+
lifecycle=LifecycleSettings(
|
|
89
|
+
confidence_decay_rate=0.0, archival_threshold=0.0, staleness_days=3650, reinforce_rate=0.10,
|
|
90
|
+
),
|
|
91
|
+
retrieval=RetrievalSettings(
|
|
92
|
+
retrieve_limit=200, conflict_threshold=0.85, min_composite_score=0.0,
|
|
93
|
+
ranker_weights=RankerWeights(provenance=0.35, recency=0.25, confidence=0.25, causal=0.15, semantic=0.0, workspace=0.0),
|
|
94
|
+
),
|
|
95
|
+
injection=InjectionSettings(context_token_limit=50000, memory_budget_chars=200000, **_SHARED_INJECTION_TEXT),
|
|
96
|
+
capture=CaptureSettings(
|
|
97
|
+
conversation_jsonl=True, tracking_max_files_per_directory=10000, auto_persist_response=True,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
RESEARCH = Profile(
|
|
102
|
+
name="research",
|
|
103
|
+
lifecycle=LifecycleSettings(
|
|
104
|
+
confidence_decay_rate=0.0, archival_threshold=0.05, staleness_days=180, reinforce_rate=0.10,
|
|
105
|
+
),
|
|
106
|
+
retrieval=RetrievalSettings(
|
|
107
|
+
retrieve_limit=50, conflict_threshold=0.85, min_composite_score=0.0,
|
|
108
|
+
ranker_weights=RankerWeights(provenance=0.25, recency=0.35, confidence=0.25, causal=0.15, semantic=0.0, workspace=0.0),
|
|
109
|
+
),
|
|
110
|
+
injection=InjectionSettings(context_token_limit=8000, memory_budget_chars=24000, **_SHARED_INJECTION_TEXT),
|
|
111
|
+
capture=CaptureSettings(
|
|
112
|
+
conversation_jsonl=True, tracking_max_files_per_directory=500, auto_persist_response=True,
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
BUILTIN_PROFILES: dict[str, Profile] = {p.name: p for p in (BUDGET_OPTIMIZED, FULL_STORAGE, RESEARCH)}
|