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,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime, timezone # noqa: F401 — timezone used in run_sweep
|
|
5
|
+
|
|
6
|
+
from agentdb.core.models import Origin
|
|
7
|
+
from agentdb.core.store import MemoryStore
|
|
8
|
+
from agentdb.core.system import SystemStats
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _utcnow() -> datetime:
|
|
12
|
+
return datetime.now(timezone.utc)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _origin_value(origin) -> str:
|
|
16
|
+
"""Return the string value of an origin, whether it's an Origin enum or a raw string."""
|
|
17
|
+
if isinstance(origin, Origin):
|
|
18
|
+
return origin.value
|
|
19
|
+
return str(origin)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LifecycleManager:
|
|
23
|
+
CONFIDENCE_DECAY_RATE: float = 0.0 # off by default; set > 0 to enable
|
|
24
|
+
ARCHIVAL_THRESHOLD: float = 0.10 # archive if confidence falls below this
|
|
25
|
+
STALENESS_DAYS: int = 90 # archive if not retrieved for this many days
|
|
26
|
+
REINFORCE_RATE: float = 0.10 # confidence boost per reinforce call
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
store: MemoryStore,
|
|
31
|
+
system: SystemStats,
|
|
32
|
+
session_record_ids: dict | None = None,
|
|
33
|
+
embedding_store=None,
|
|
34
|
+
skill_store=None,
|
|
35
|
+
confidence_decay_rate: float | None = None,
|
|
36
|
+
archival_threshold: float | None = None,
|
|
37
|
+
staleness_days: int | None = None,
|
|
38
|
+
reinforce_rate: float | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._store = store
|
|
41
|
+
self._system = system
|
|
42
|
+
self._session_record_ids: dict[str, list[str]] = (
|
|
43
|
+
session_record_ids if session_record_ids is not None else {}
|
|
44
|
+
)
|
|
45
|
+
self._embedding_store = embedding_store
|
|
46
|
+
self._skill_store = skill_store
|
|
47
|
+
cls = type(self)
|
|
48
|
+
self.CONFIDENCE_DECAY_RATE = confidence_decay_rate if confidence_decay_rate is not None else cls.CONFIDENCE_DECAY_RATE
|
|
49
|
+
self.ARCHIVAL_THRESHOLD = archival_threshold if archival_threshold is not None else cls.ARCHIVAL_THRESHOLD
|
|
50
|
+
self.STALENESS_DAYS = staleness_days if staleness_days is not None else cls.STALENESS_DAYS
|
|
51
|
+
self.REINFORCE_RATE = reinforce_rate if reinforce_rate is not None else cls.REINFORCE_RATE
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# Sweep
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def run_sweep(self) -> dict:
|
|
58
|
+
"""
|
|
59
|
+
On-demand lifecycle sweep.
|
|
60
|
+
|
|
61
|
+
1. If CONFIDENCE_DECAY_RATE > 0: apply decay to agent_inferred +
|
|
62
|
+
provider_ingested records (human_approved is immune).
|
|
63
|
+
2. Archive records whose confidence is below ARCHIVAL_THRESHOLD.
|
|
64
|
+
3. Archive records not retrieved for more than STALENESS_DAYS.
|
|
65
|
+
|
|
66
|
+
Returns {"decayed": N, "archived": N}.
|
|
67
|
+
human_approved records are immune to all lifecycle operations.
|
|
68
|
+
"""
|
|
69
|
+
now = _utcnow()
|
|
70
|
+
decayed = 0
|
|
71
|
+
archived_ids: set[str] = set()
|
|
72
|
+
|
|
73
|
+
records = self._store.list_all()
|
|
74
|
+
|
|
75
|
+
# --- Step 0: archive expired records (expiry_time has passed) ---
|
|
76
|
+
for record in records:
|
|
77
|
+
if _origin_value(record.origin) == Origin.HUMAN_APPROVED.value:
|
|
78
|
+
continue # human_approved records never expire
|
|
79
|
+
if record.expiry_time is not None:
|
|
80
|
+
expiry = record.expiry_time
|
|
81
|
+
if expiry.tzinfo is None:
|
|
82
|
+
expiry = expiry.replace(tzinfo=timezone.utc)
|
|
83
|
+
if expiry <= now:
|
|
84
|
+
self._store.archive_record(record.id)
|
|
85
|
+
archived_ids.add(record.id)
|
|
86
|
+
|
|
87
|
+
records = self._store.list_all()
|
|
88
|
+
|
|
89
|
+
# --- Step 1: optional decay ---
|
|
90
|
+
if self.CONFIDENCE_DECAY_RATE > 0:
|
|
91
|
+
for record in records:
|
|
92
|
+
origin = _origin_value(record.origin)
|
|
93
|
+
if origin == Origin.HUMAN_APPROVED.value:
|
|
94
|
+
continue
|
|
95
|
+
new_conf = max(0.0, record.confidence - self.CONFIDENCE_DECAY_RATE)
|
|
96
|
+
if new_conf != record.confidence:
|
|
97
|
+
self._store.update_confidence(record.id, new_conf)
|
|
98
|
+
decayed += 1
|
|
99
|
+
|
|
100
|
+
# Re-read after potential mutations so archival thresholds are current
|
|
101
|
+
records = self._store.list_all()
|
|
102
|
+
|
|
103
|
+
# --- Step 2: archive below threshold ---
|
|
104
|
+
for record in records:
|
|
105
|
+
if record.id in archived_ids:
|
|
106
|
+
continue
|
|
107
|
+
origin = _origin_value(record.origin)
|
|
108
|
+
if origin == Origin.HUMAN_APPROVED.value:
|
|
109
|
+
continue
|
|
110
|
+
if record.confidence < self.ARCHIVAL_THRESHOLD:
|
|
111
|
+
self._store.archive_record(record.id)
|
|
112
|
+
archived_ids.add(record.id)
|
|
113
|
+
|
|
114
|
+
# --- Step 3: archive stale records ---
|
|
115
|
+
records = self._store.list_all()
|
|
116
|
+
for record in records:
|
|
117
|
+
if record.id in archived_ids:
|
|
118
|
+
continue
|
|
119
|
+
origin = _origin_value(record.origin)
|
|
120
|
+
if origin == Origin.HUMAN_APPROVED.value:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Use last_retrieved if available, else created_at
|
|
124
|
+
reference_time = record.last_retrieved or record.created_at
|
|
125
|
+
# Ensure timezone-aware for comparison
|
|
126
|
+
if reference_time.tzinfo is None:
|
|
127
|
+
reference_time = reference_time.replace(tzinfo=timezone.utc)
|
|
128
|
+
days_since = (now - reference_time).days
|
|
129
|
+
if days_since >= self.STALENESS_DAYS:
|
|
130
|
+
self._store.archive_record(record.id)
|
|
131
|
+
archived_ids.add(record.id)
|
|
132
|
+
|
|
133
|
+
# Embedding orphan cleanup
|
|
134
|
+
if self._embedding_store is not None:
|
|
135
|
+
active_memory_ids = [r.id for r in self._store.list_all()]
|
|
136
|
+
self._embedding_store.prune_orphans("memories", active_memory_ids)
|
|
137
|
+
|
|
138
|
+
if self._skill_store is not None:
|
|
139
|
+
active_skill_ids = self._skill_store.list_all_active_ids()
|
|
140
|
+
self._embedding_store.prune_orphans("skills", active_skill_ids)
|
|
141
|
+
|
|
142
|
+
return {"decayed": decayed, "archived": len(archived_ids)}
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# Reinforce
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def reinforce(self, record_id: str) -> None:
|
|
149
|
+
"""Boost confidence toward 1.0 and reset last_retrieved."""
|
|
150
|
+
record = self._store.get_by_id(record_id)
|
|
151
|
+
if record is None or record.archived:
|
|
152
|
+
return
|
|
153
|
+
if _origin_value(record.origin) == Origin.HUMAN_APPROVED.value:
|
|
154
|
+
return # immune
|
|
155
|
+
|
|
156
|
+
new_conf = min(1.0, record.confidence + self.REINFORCE_RATE)
|
|
157
|
+
self._store.update_confidence(record_id, new_conf)
|
|
158
|
+
self._store.increment_retrieval_count(record_id)
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
# Record outcome (core logic, moved from AgentDB)
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def record_outcome(
|
|
165
|
+
self,
|
|
166
|
+
session_id: str,
|
|
167
|
+
agent_id: str,
|
|
168
|
+
outcome_type: str,
|
|
169
|
+
outcome_value: float,
|
|
170
|
+
) -> list[str]:
|
|
171
|
+
"""
|
|
172
|
+
Adjust confidence for records retrieved in this session.
|
|
173
|
+
|
|
174
|
+
Rules by origin:
|
|
175
|
+
- agent_inferred: positive (+0.05 * outcome_value, cap +0.1),
|
|
176
|
+
negative (-0.02 * (1 - outcome_value), cap -0.1)
|
|
177
|
+
- provider_ingested: positive (no change), negative (same as above)
|
|
178
|
+
- human_approved: never adjusted
|
|
179
|
+
|
|
180
|
+
Clears the session record IDs after adjusting.
|
|
181
|
+
Returns list of adjusted record IDs (for audit log use by the caller).
|
|
182
|
+
"""
|
|
183
|
+
seen_ids = list(dict.fromkeys(self._session_record_ids.get(session_id, [])))
|
|
184
|
+
is_positive = outcome_value >= 0.5
|
|
185
|
+
adjusted_ids: list[str] = []
|
|
186
|
+
|
|
187
|
+
for record_id in seen_ids:
|
|
188
|
+
record = self._store.get_by_id(record_id)
|
|
189
|
+
if record is None:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
origin = _origin_value(record.origin)
|
|
193
|
+
if origin == Origin.HUMAN_APPROVED.value:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if origin == Origin.AGENT_INFERRED.value:
|
|
197
|
+
if is_positive:
|
|
198
|
+
delta = min(0.05 * outcome_value, 0.1)
|
|
199
|
+
new_conf = min(1.0, record.confidence + delta)
|
|
200
|
+
else:
|
|
201
|
+
delta = min(0.02 * (1 - outcome_value), 0.1)
|
|
202
|
+
new_conf = max(0.0, record.confidence - delta)
|
|
203
|
+
self._store.update_confidence(record_id, new_conf)
|
|
204
|
+
adjusted_ids.append(record_id)
|
|
205
|
+
elif origin == Origin.PROVIDER_INGESTED.value:
|
|
206
|
+
if not is_positive:
|
|
207
|
+
delta = min(0.02 * (1 - outcome_value), 0.1)
|
|
208
|
+
new_conf = max(0.0, record.confidence - delta)
|
|
209
|
+
self._store.update_confidence(record_id, new_conf)
|
|
210
|
+
adjusted_ids.append(record_id)
|
|
211
|
+
|
|
212
|
+
# Clear session record IDs so the next retrieve starts fresh
|
|
213
|
+
if session_id in self._session_record_ids:
|
|
214
|
+
self._session_record_ids[session_id] = []
|
|
215
|
+
|
|
216
|
+
# Persist outcome row
|
|
217
|
+
now = _utcnow().isoformat()
|
|
218
|
+
with self._store._connect() as conn:
|
|
219
|
+
conn.execute(
|
|
220
|
+
"INSERT INTO outcomes "
|
|
221
|
+
"(id, session_id, agent_id, outcome_type, outcome_value, linked_retrieval_sequence, created_at) "
|
|
222
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
223
|
+
(str(uuid.uuid4()), session_id, agent_id, outcome_type, outcome_value, None, now),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return adjusted_ids
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PermissionDenied(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Prefixes owned by AgentDB's own framework machinery (not agent business
|
|
15
|
+
# data) — writable by default regardless of default_policy/can_write,
|
|
16
|
+
# because gating them requires every integrating deployment to remember a
|
|
17
|
+
# permissions.yaml edit before the feature works at all. An explicit
|
|
18
|
+
# cannot_write entry still overrides this (see check_write below) — this is
|
|
19
|
+
# a default, not an unconditional bypass.
|
|
20
|
+
FRAMEWORK_MANAGED_WRITE_PREFIXES: tuple[str, ...] = ("proposal/",)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PermissionRouter:
|
|
24
|
+
"""
|
|
25
|
+
Routes permission checks based on a YAML policy file.
|
|
26
|
+
|
|
27
|
+
YAML format:
|
|
28
|
+
default_policy: deny
|
|
29
|
+
human_zone_prefixes:
|
|
30
|
+
- "skills/"
|
|
31
|
+
agents:
|
|
32
|
+
agent_id:
|
|
33
|
+
can_write:
|
|
34
|
+
- "memory/working/"
|
|
35
|
+
cannot_write:
|
|
36
|
+
- "skills/"
|
|
37
|
+
can_read:
|
|
38
|
+
- "memory/working/"
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, permissions_path: Any) -> None:
|
|
42
|
+
path = Path(permissions_path)
|
|
43
|
+
with path.open("r", encoding="utf-8") as f:
|
|
44
|
+
self._config: dict = yaml.safe_load(f) or {}
|
|
45
|
+
|
|
46
|
+
self._default_policy: str = self._config.get("default_policy", "deny")
|
|
47
|
+
self._human_zone_prefixes: list[str] = self._config.get("human_zone_prefixes", [])
|
|
48
|
+
self._agents: dict = self._config.get("agents", {})
|
|
49
|
+
|
|
50
|
+
def _matches_any(self, key: str, patterns: list[str]) -> bool:
|
|
51
|
+
for pattern in patterns:
|
|
52
|
+
# Treat patterns ending with '/' as prefix matches,
|
|
53
|
+
# also support glob patterns
|
|
54
|
+
if pattern.endswith("/"):
|
|
55
|
+
if key.startswith(pattern):
|
|
56
|
+
return True
|
|
57
|
+
elif fnmatch.fnmatch(key, pattern + "*") or fnmatch.fnmatch(key, pattern):
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def is_human_zone(self, key: str) -> bool:
|
|
62
|
+
for prefix in self._human_zone_prefixes:
|
|
63
|
+
if key.startswith(prefix):
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def check_write(self, key: str, agent_id: str, trust_zone: str) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Raises PermissionDenied if the agent is not allowed to write the key.
|
|
70
|
+
|
|
71
|
+
Rules:
|
|
72
|
+
- If trust_zone == "agent_inferred" and key is in a human_zone_prefix: deny
|
|
73
|
+
- Check cannot_write before can_write
|
|
74
|
+
- default_policy: deny means deny unless explicitly allowed
|
|
75
|
+
"""
|
|
76
|
+
# Agent-inferred writes to human zones are always denied
|
|
77
|
+
if trust_zone == "agent_inferred" and self.is_human_zone(key):
|
|
78
|
+
raise PermissionDenied(
|
|
79
|
+
f"Agent '{agent_id}' cannot write to human zone key '{key}' "
|
|
80
|
+
f"with trust_zone '{trust_zone}'"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
agent_config = self._agents.get(agent_id, {})
|
|
84
|
+
|
|
85
|
+
# Check cannot_write first
|
|
86
|
+
cannot_write = agent_config.get("cannot_write", [])
|
|
87
|
+
if self._matches_any(key, cannot_write):
|
|
88
|
+
raise PermissionDenied(
|
|
89
|
+
f"Agent '{agent_id}' is explicitly denied write access to '{key}'"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Check can_write
|
|
93
|
+
can_write = agent_config.get("can_write", [])
|
|
94
|
+
if self._matches_any(key, can_write):
|
|
95
|
+
return # allowed
|
|
96
|
+
|
|
97
|
+
# Framework-managed prefixes are writable by default (cannot_write,
|
|
98
|
+
# checked above, still overrides this).
|
|
99
|
+
if self._matches_any(key, FRAMEWORK_MANAGED_WRITE_PREFIXES):
|
|
100
|
+
return # allowed
|
|
101
|
+
|
|
102
|
+
# Fall through to default policy
|
|
103
|
+
if self._default_policy == "deny":
|
|
104
|
+
raise PermissionDenied(
|
|
105
|
+
f"Agent '{agent_id}' has no explicit write permission for '{key}' "
|
|
106
|
+
f"and default_policy is 'deny'"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def unconfigured_agents(self, agent_ids: list[str]) -> list[str]:
|
|
110
|
+
"""Return the subset of agent_ids that have no entry in this config.
|
|
111
|
+
|
|
112
|
+
Useful for pre-flight checks at startup: call with all known bot
|
|
113
|
+
identities and warn/fail if any are returned.
|
|
114
|
+
"""
|
|
115
|
+
return [a for a in agent_ids if a not in self._agents]
|
|
116
|
+
|
|
117
|
+
def check_read(self, key: str, agent_id: str) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Raises PermissionDenied if the agent is not allowed to read the key.
|
|
120
|
+
"""
|
|
121
|
+
agent_config = self._agents.get(agent_id, {})
|
|
122
|
+
|
|
123
|
+
can_read = agent_config.get("can_read", [])
|
|
124
|
+
if self._matches_any(key, can_read):
|
|
125
|
+
return # allowed
|
|
126
|
+
|
|
127
|
+
if self._default_policy == "deny":
|
|
128
|
+
raise PermissionDenied(
|
|
129
|
+
f"Agent '{agent_id}' has no explicit read permission for '{key}' "
|
|
130
|
+
f"and default_policy is 'deny'"
|
|
131
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
AgentDB,
|
|
3
|
+
AuditLog,
|
|
4
|
+
AuditEntry,
|
|
5
|
+
RetrievalResult,
|
|
6
|
+
SystemStats,
|
|
7
|
+
SkillsAPI,
|
|
8
|
+
EVENT_MEMORY_WRITE,
|
|
9
|
+
EVENT_MEMORY_RETRIEVE,
|
|
10
|
+
EVENT_MEMORY_OUTCOME,
|
|
11
|
+
EVENT_EXTERNAL_CONFLICT_DETECTED,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentDB",
|
|
16
|
+
"AuditLog",
|
|
17
|
+
"AuditEntry",
|
|
18
|
+
"RetrievalResult",
|
|
19
|
+
"SystemStats",
|
|
20
|
+
"SkillsAPI",
|
|
21
|
+
"EVENT_MEMORY_WRITE",
|
|
22
|
+
"EVENT_MEMORY_RETRIEVE",
|
|
23
|
+
"EVENT_MEMORY_OUTCOME",
|
|
24
|
+
"EVENT_EXTERNAL_CONFLICT_DETECTED",
|
|
25
|
+
]
|