agent-memory-os 0.2.3__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,231 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ import re
5
+
6
+ from .schema import SearchResult
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class ContextDecision:
11
+ memory_id: str
12
+ selected: bool
13
+ effective_score: float
14
+ token_count: int
15
+ reason: list[str] = field(default_factory=list)
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class ContextPackReport:
20
+ text: str
21
+ decisions: list[ContextDecision]
22
+ used_tokens: int
23
+ max_tokens: int
24
+
25
+
26
+ def approx_tokens(text: str) -> int:
27
+ # Conservative, dependency-free approximation for MVP.
28
+ return max(1, (len(text) + 3) // 4)
29
+
30
+
31
+ def build_context_pack(results: list[SearchResult], *, max_tokens: int = 1200) -> str:
32
+ return build_context_pack_report(results, max_tokens=max_tokens).text
33
+
34
+
35
+ def build_context_pack_report(results: list[SearchResult], *, max_tokens: int = 1200) -> ContextPackReport:
36
+ if max_tokens < 32:
37
+ raise ValueError("max_tokens must be >= 32")
38
+
39
+ ranked = sorted(results, key=_arbitration_score, reverse=True)
40
+ conflict_keys = _conflict_keys(results)
41
+
42
+ header = "MEMORY CONTEXT PACK:"
43
+ used = approx_tokens(header + "\n")
44
+ lines: list[str] = [header]
45
+ decisions: list[ContextDecision] = []
46
+ seen_claims: set[tuple[str, str]] = set()
47
+ selected_count = 0
48
+ core_selected_under_pressure = False
49
+
50
+ # Reserved budget for core memories: ensure at least 25% of total budget
51
+ # or 300 tokens (whichever is larger) is reserved for core items.
52
+ core_reserve_limit = max(300, int(max_tokens * 0.25))
53
+
54
+ for result in ranked:
55
+ record = result.record
56
+ score = _arbitration_score(result)
57
+ reason = _base_reasons(result, conflict_keys)
58
+ claim_key = _claim_key(result)
59
+ claim_value = _claim_value(result)
60
+ duplicate_key = (claim_key, claim_value)
61
+
62
+ if duplicate_key in seen_claims:
63
+ reason.append("duplicate_cluster_suppressed")
64
+ decisions.append(
65
+ ContextDecision(
66
+ memory_id=record.id,
67
+ selected=False,
68
+ effective_score=score,
69
+ token_count=_line_tokens(result, selected_count + 1, conflict_keys),
70
+ reason=reason,
71
+ )
72
+ )
73
+ continue
74
+
75
+ line = _format_line(result, selected_count + 1, conflict_keys)
76
+ cost = approx_tokens(line) + 1
77
+
78
+ # CORE RESERVE LOGIC:
79
+ # If it's a core memory and we haven't exhausted the reserve yet,
80
+ # let it in even if it exceeds the overall max_tokens (up to a reasonable limit).
81
+ is_core = "core_reserved_budget" in reason
82
+
83
+ if core_selected_under_pressure and not is_core:
84
+ reason.append("core_reserve_protected")
85
+ reason.append("budget_exceeded")
86
+ decisions.append(
87
+ ContextDecision(
88
+ memory_id=record.id,
89
+ selected=False,
90
+ effective_score=score,
91
+ token_count=cost,
92
+ reason=reason,
93
+ )
94
+ )
95
+ continue
96
+
97
+ if used + cost > max_tokens:
98
+ if is_core and used < core_reserve_limit:
99
+ # Allow core memory to 'burst' slightly over total budget if within reserve limit
100
+ reason.append("core_reserve_burst")
101
+ else:
102
+ reason.append("budget_exceeded")
103
+ decisions.append(
104
+ ContextDecision(
105
+ memory_id=record.id,
106
+ selected=False,
107
+ effective_score=score,
108
+ token_count=cost,
109
+ reason=reason,
110
+ )
111
+ )
112
+ continue
113
+
114
+ reason.append("fits_budget")
115
+ selected_count += 1
116
+ if is_core and max_tokens <= core_reserve_limit:
117
+ core_selected_under_pressure = True
118
+ lines.append(line)
119
+ used += cost
120
+ seen_claims.add(duplicate_key)
121
+ decisions.append(
122
+ ContextDecision(
123
+ memory_id=record.id,
124
+ selected=True,
125
+ effective_score=score,
126
+ token_count=cost,
127
+ reason=reason,
128
+ )
129
+ )
130
+
131
+ return ContextPackReport(
132
+ text="\n".join(lines) + "\n",
133
+ decisions=decisions,
134
+ used_tokens=used,
135
+ max_tokens=max_tokens,
136
+ )
137
+
138
+
139
+ def _format_line(result: SearchResult, idx: int, conflict_keys: set[str]) -> str:
140
+ r = result.record
141
+ tags = ",".join(r.tags[:5])
142
+ conflict = " [CONFLICT]" if _claim_key(result) in conflict_keys else ""
143
+ return (
144
+ f"- [{idx}]{conflict} ({r.scope}/{r.type}; importance={r.importance:.2f}; "
145
+ f"confidence={r.confidence:.2f}; score={_arbitration_score(result):.3f}; tags={tags}) {r.content}"
146
+ )
147
+
148
+
149
+ def _line_tokens(result: SearchResult, idx: int, conflict_keys: set[str]) -> int:
150
+ return approx_tokens(_format_line(result, idx, conflict_keys)) + 1
151
+
152
+
153
+ def _base_reasons(result: SearchResult, conflict_keys: set[str]) -> list[str]:
154
+ r = result.record
155
+ reasons = ["acl_allowed", "not_expired"]
156
+ source = r.source or {}
157
+ tags = set(r.tags)
158
+ if r.confidence >= 0.8:
159
+ reasons.append("high_confidence")
160
+ elif r.confidence < 0.5:
161
+ reasons.append("low_confidence")
162
+ if r.importance >= 0.8:
163
+ reasons.append("high_importance")
164
+ if source.get("authoritative") is True or "authoritative" in tags or "core" in tags:
165
+ reasons.append("authoritative")
166
+ if source.get("permanence") is True or r.pinned:
167
+ reasons.append("permanent")
168
+ if _weight(source) > 8:
169
+ reasons.append("weight_gt_8")
170
+ if {"authoritative", "permanent", "weight_gt_8"}.intersection(reasons):
171
+ reasons.append("core_reserved_budget")
172
+ if _claim_key(result) in conflict_keys:
173
+ reasons.append("conflict_detected")
174
+ if result.reason:
175
+ reasons.append(f"provider:{result.reason}")
176
+ return reasons
177
+
178
+
179
+ def _arbitration_score(result: SearchResult) -> float:
180
+ r = result.record
181
+ source = r.source or {}
182
+ tags = set(r.tags)
183
+ score = float(result.score)
184
+ score += r.confidence * 1.2
185
+ score += r.importance * 1.2
186
+ if source.get("authoritative") is True or "authoritative" in tags or "core" in tags:
187
+ score += 1.0
188
+ if source.get("permanence") is True or r.pinned:
189
+ score += 0.8
190
+ if _weight(source) > 8:
191
+ score += 0.8
192
+ if r.confidence < 0.5:
193
+ score -= 1.0
194
+ return score
195
+
196
+
197
+ def _conflict_keys(results: list[SearchResult]) -> set[str]:
198
+ claims_by_key: dict[str, set[str]] = {}
199
+ for result in results:
200
+ source = result.record.source or {}
201
+ if "claim" not in source:
202
+ continue
203
+ claims_by_key.setdefault(_claim_key(result), set()).add(str(source["claim"]))
204
+ return {key for key, claims in claims_by_key.items() if len(claims) > 1}
205
+
206
+
207
+ def _claim_key(result: SearchResult) -> str:
208
+ source = result.record.source or {}
209
+ key = source.get("claim_key")
210
+ if key:
211
+ return str(key)
212
+ return _fingerprint(result.record.content)
213
+
214
+
215
+ def _claim_value(result: SearchResult) -> str:
216
+ source = result.record.source or {}
217
+ if "claim" in source:
218
+ return str(source["claim"])
219
+ return _fingerprint(result.record.content)
220
+
221
+
222
+ def _fingerprint(text: str) -> str:
223
+ normalized = re.sub(r"\W+", " ", text.casefold()).strip()
224
+ return " ".join(normalized.split())[:160]
225
+
226
+
227
+ def _weight(source: dict) -> float:
228
+ try:
229
+ return float(source.get("weight", 0))
230
+ except (TypeError, ValueError):
231
+ return 0.0