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.
@@ -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)]
@@ -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
@@ -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)}