akernel-runtime 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.
- akernel_runtime-0.1.0.dist-info/METADATA +270 -0
- akernel_runtime-0.1.0.dist-info/RECORD +40 -0
- akernel_runtime-0.1.0.dist-info/WHEEL +5 -0
- akernel_runtime-0.1.0.dist-info/entry_points.txt +2 -0
- akernel_runtime-0.1.0.dist-info/licenses/LICENSE +201 -0
- akernel_runtime-0.1.0.dist-info/licenses/NOTICE +4 -0
- akernel_runtime-0.1.0.dist-info/top_level.txt +1 -0
- context_kernel/__init__.py +4 -0
- context_kernel/__main__.py +5 -0
- context_kernel/agent_reports.py +188 -0
- context_kernel/benchmarks.py +493 -0
- context_kernel/budget.py +72 -0
- context_kernel/cli.py +2953 -0
- context_kernel/context.py +161 -0
- context_kernel/evals.py +347 -0
- context_kernel/global_memory.py +126 -0
- context_kernel/loop.py +1617 -0
- context_kernel/marketplace.py +194 -0
- context_kernel/marketplace_data/skills/context_budget.json +27 -0
- context_kernel/marketplace_data/skills/context_compaction.json +27 -0
- context_kernel/marketplace_data/skills/edit_file.json +27 -0
- context_kernel/marketplace_data/skills/index.json +66 -0
- context_kernel/marketplace_data/skills/long_task_planning.json +27 -0
- context_kernel/marketplace_data/skills/multi_file_bugfix.json +28 -0
- context_kernel/memory.py +515 -0
- context_kernel/models.py +144 -0
- context_kernel/planner.py +155 -0
- context_kernel/policy.py +271 -0
- context_kernel/project.py +317 -0
- context_kernel/providers.py +1264 -0
- context_kernel/report_costs.py +375 -0
- context_kernel/runner.py +78 -0
- context_kernel/skills.py +318 -0
- context_kernel/state_writer.py +108 -0
- context_kernel/storage.py +171 -0
- context_kernel/tasks.py +549 -0
- context_kernel/text.py +42 -0
- context_kernel/tokenizer.py +22 -0
- context_kernel/tools.py +544 -0
- context_kernel/verifier.py +77 -0
context_kernel/memory.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import sqlite3
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from .models import MemoryRecord, SelectedMemory, utc_now
|
|
11
|
+
from .storage import Workspace
|
|
12
|
+
from .text import matched_terms
|
|
13
|
+
from .tokenizer import estimate_tokens
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ALLOWED_KINDS = {"fact", "preference", "project_state", "task_state", "decision"}
|
|
17
|
+
PINNED_TAGS = {"keep", "pinned", "global"}
|
|
18
|
+
RECOVERABLE_KINDS = {"task_state", "project_state"}
|
|
19
|
+
STRONG_MEMORY_TERMS = {
|
|
20
|
+
"agent",
|
|
21
|
+
"architecture",
|
|
22
|
+
"budget",
|
|
23
|
+
"cli",
|
|
24
|
+
"context",
|
|
25
|
+
"eval",
|
|
26
|
+
"kernel",
|
|
27
|
+
"memory",
|
|
28
|
+
"mvp",
|
|
29
|
+
"phase2",
|
|
30
|
+
"prototype",
|
|
31
|
+
"routing",
|
|
32
|
+
"runtime",
|
|
33
|
+
"skill",
|
|
34
|
+
"token",
|
|
35
|
+
"tokens",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MemoryStore:
|
|
40
|
+
def __init__(self, workspace: Workspace):
|
|
41
|
+
self.workspace = workspace
|
|
42
|
+
self._ensure_schema()
|
|
43
|
+
self._migrate_jsonl_if_needed()
|
|
44
|
+
|
|
45
|
+
def add(self, kind: str, text: str, tags: list[str] | None = None) -> MemoryRecord:
|
|
46
|
+
validate_kind(kind)
|
|
47
|
+
clean_text = normalize_text(text)
|
|
48
|
+
clean_tags = normalize_tags(tags or [])
|
|
49
|
+
text_hash = memory_hash(kind, clean_text)
|
|
50
|
+
existing = self._active_by_hash(kind, text_hash)
|
|
51
|
+
if existing:
|
|
52
|
+
merged_tags = sorted(set(existing.tags).union(clean_tags))
|
|
53
|
+
if merged_tags != existing.tags:
|
|
54
|
+
return self.update(existing.id, tags=merged_tags)
|
|
55
|
+
return existing
|
|
56
|
+
|
|
57
|
+
now = utc_now()
|
|
58
|
+
record = MemoryRecord(
|
|
59
|
+
id=uuid4().hex[:12],
|
|
60
|
+
kind=kind,
|
|
61
|
+
text=clean_text,
|
|
62
|
+
tags=clean_tags,
|
|
63
|
+
created_at=now,
|
|
64
|
+
updated_at=now,
|
|
65
|
+
)
|
|
66
|
+
with self._connect() as db:
|
|
67
|
+
db.execute(
|
|
68
|
+
"""
|
|
69
|
+
INSERT INTO memories(id, kind, text, tags, text_hash, created_at, updated_at, archived_at)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL)
|
|
71
|
+
""",
|
|
72
|
+
(
|
|
73
|
+
record.id,
|
|
74
|
+
record.kind,
|
|
75
|
+
record.text,
|
|
76
|
+
json.dumps(record.tags, ensure_ascii=False),
|
|
77
|
+
text_hash,
|
|
78
|
+
record.created_at,
|
|
79
|
+
record.updated_at,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
return record
|
|
83
|
+
|
|
84
|
+
def get(self, record_id: str, *, include_archived: bool = False) -> MemoryRecord:
|
|
85
|
+
with self._connect() as db:
|
|
86
|
+
if include_archived:
|
|
87
|
+
row = db.execute("SELECT * FROM memories WHERE id = ?", (record_id,)).fetchone()
|
|
88
|
+
else:
|
|
89
|
+
row = db.execute(
|
|
90
|
+
"SELECT * FROM memories WHERE id = ? AND archived_at IS NULL",
|
|
91
|
+
(record_id,),
|
|
92
|
+
).fetchone()
|
|
93
|
+
if row is None:
|
|
94
|
+
raise KeyError(f"Memory record not found: {record_id}")
|
|
95
|
+
return record_from_row(row)
|
|
96
|
+
|
|
97
|
+
def update(
|
|
98
|
+
self,
|
|
99
|
+
record_id: str,
|
|
100
|
+
*,
|
|
101
|
+
kind: str | None = None,
|
|
102
|
+
text: str | None = None,
|
|
103
|
+
tags: list[str] | None = None,
|
|
104
|
+
) -> MemoryRecord:
|
|
105
|
+
current = self.get(record_id)
|
|
106
|
+
next_kind = kind or current.kind
|
|
107
|
+
validate_kind(next_kind)
|
|
108
|
+
next_text = normalize_text(text) if text is not None else current.text
|
|
109
|
+
next_tags = normalize_tags(tags) if tags is not None else current.tags
|
|
110
|
+
next_hash = memory_hash(next_kind, next_text)
|
|
111
|
+
duplicate = self._active_by_hash(next_kind, next_hash)
|
|
112
|
+
if duplicate and duplicate.id != record_id:
|
|
113
|
+
raise ValueError(f"Update would duplicate existing memory: {duplicate.id}")
|
|
114
|
+
|
|
115
|
+
now = utc_now()
|
|
116
|
+
with self._connect() as db:
|
|
117
|
+
db.execute(
|
|
118
|
+
"""
|
|
119
|
+
UPDATE memories
|
|
120
|
+
SET kind = ?, text = ?, tags = ?, text_hash = ?, updated_at = ?
|
|
121
|
+
WHERE id = ? AND archived_at IS NULL
|
|
122
|
+
""",
|
|
123
|
+
(
|
|
124
|
+
next_kind,
|
|
125
|
+
next_text,
|
|
126
|
+
json.dumps(next_tags, ensure_ascii=False),
|
|
127
|
+
next_hash,
|
|
128
|
+
now,
|
|
129
|
+
record_id,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
return self.get(record_id)
|
|
133
|
+
|
|
134
|
+
def forget(self, record_id: str) -> bool:
|
|
135
|
+
now = utc_now()
|
|
136
|
+
with self._connect() as db:
|
|
137
|
+
cursor = db.execute(
|
|
138
|
+
"UPDATE memories SET archived_at = ?, updated_at = ? WHERE id = ? AND archived_at IS NULL",
|
|
139
|
+
(now, now, record_id),
|
|
140
|
+
)
|
|
141
|
+
return cursor.rowcount > 0
|
|
142
|
+
|
|
143
|
+
def prune(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
max_records: int | None = None,
|
|
147
|
+
max_tokens: int | None = None,
|
|
148
|
+
dry_run: bool = False,
|
|
149
|
+
) -> dict[str, object]:
|
|
150
|
+
records = self.all()
|
|
151
|
+
if max_records is None and max_tokens is None:
|
|
152
|
+
raise ValueError("memory prune requires --max-records, --max-tokens, or both.")
|
|
153
|
+
if max_records is not None and max_records < 0:
|
|
154
|
+
raise ValueError("--max-records must be >= 0")
|
|
155
|
+
if max_tokens is not None and max_tokens < 0:
|
|
156
|
+
raise ValueError("--max-tokens must be >= 0")
|
|
157
|
+
|
|
158
|
+
keep_limit = max_records if max_records is not None else len(records)
|
|
159
|
+
decisions = self.retention_analysis(records)
|
|
160
|
+
ranked = sorted(decisions, key=lambda item: item["retention_key"], reverse=True)
|
|
161
|
+
kept: list[dict[str, object]] = []
|
|
162
|
+
used_tokens = 0
|
|
163
|
+
for decision in ranked:
|
|
164
|
+
cost = int(decision["token_cost"])
|
|
165
|
+
if len(kept) >= keep_limit:
|
|
166
|
+
continue
|
|
167
|
+
if max_tokens is not None and used_tokens + cost > max_tokens:
|
|
168
|
+
continue
|
|
169
|
+
kept.append(decision)
|
|
170
|
+
used_tokens += cost
|
|
171
|
+
|
|
172
|
+
kept_ids = {str(decision["record"]["id"]) for decision in kept}
|
|
173
|
+
candidates = [decision for decision in decisions if str(decision["record"]["id"]) not in kept_ids]
|
|
174
|
+
if not dry_run:
|
|
175
|
+
for decision in candidates:
|
|
176
|
+
self.forget(str(decision["record"]["id"]))
|
|
177
|
+
return {
|
|
178
|
+
"dry_run": dry_run,
|
|
179
|
+
"active_before": len(records),
|
|
180
|
+
"kept": len(kept),
|
|
181
|
+
"archived": 0 if dry_run else len(candidates),
|
|
182
|
+
"candidate_count": len(candidates),
|
|
183
|
+
"kept_tokens": used_tokens,
|
|
184
|
+
"active_tokens": sum(int(decision["token_cost"]) for decision in decisions),
|
|
185
|
+
"recoverable_candidates": sum(1 for decision in candidates if decision["recoverability"]["level"] != "none"),
|
|
186
|
+
"candidates": [decision["record"] for decision in candidates],
|
|
187
|
+
"candidate_decisions": [strip_retention_key(decision) for decision in candidates],
|
|
188
|
+
"kept_decisions": [strip_retention_key(decision) for decision in kept],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def retention_analysis(self, records: list[MemoryRecord] | None = None) -> list[dict[str, object]]:
|
|
192
|
+
active_records = records if records is not None else self.all()
|
|
193
|
+
recovery_index = memory_recovery_index(self.workspace)
|
|
194
|
+
total = max(1, len(active_records))
|
|
195
|
+
decisions: list[dict[str, object]] = []
|
|
196
|
+
for index, record in enumerate(active_records):
|
|
197
|
+
token_cost = estimate_tokens(record.to_dict())
|
|
198
|
+
recoverability = recoverability_summary(record, recovery_index)
|
|
199
|
+
score, reasons = retention_score(record, token_cost, recoverability, index=index, total=total)
|
|
200
|
+
decisions.append(
|
|
201
|
+
{
|
|
202
|
+
"record": record.to_dict(),
|
|
203
|
+
"score": score,
|
|
204
|
+
"token_cost": token_cost,
|
|
205
|
+
"recoverability": recoverability,
|
|
206
|
+
"reasons": reasons,
|
|
207
|
+
"retention_key": (score, -token_cost, record.updated_at, record.id),
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
return decisions
|
|
211
|
+
|
|
212
|
+
def all(self, kind: str | None = None, *, include_archived: bool = False) -> list[MemoryRecord]:
|
|
213
|
+
if kind:
|
|
214
|
+
validate_kind(kind)
|
|
215
|
+
where: list[str] = []
|
|
216
|
+
params: list[str] = []
|
|
217
|
+
if kind:
|
|
218
|
+
where.append("kind = ?")
|
|
219
|
+
params.append(kind)
|
|
220
|
+
if not include_archived:
|
|
221
|
+
where.append("archived_at IS NULL")
|
|
222
|
+
clause = f"WHERE {' AND '.join(where)}" if where else ""
|
|
223
|
+
with self._connect() as db:
|
|
224
|
+
rows = db.execute(
|
|
225
|
+
f"SELECT * FROM memories {clause} ORDER BY created_at ASC, id ASC",
|
|
226
|
+
params,
|
|
227
|
+
).fetchall()
|
|
228
|
+
return [record_from_row(row) for row in rows]
|
|
229
|
+
|
|
230
|
+
def search(self, query: str, kind: str | None = None, limit: int = 5, budget_tokens: int | None = None) -> list[SelectedMemory]:
|
|
231
|
+
selected: list[SelectedMemory] = []
|
|
232
|
+
records = self.all(kind)
|
|
233
|
+
total = len(records)
|
|
234
|
+
for index, record in enumerate(records):
|
|
235
|
+
haystack = " ".join([record.kind, record.text, " ".join(record.tags)])
|
|
236
|
+
matches = matched_terms(query, haystack)
|
|
237
|
+
strong_matches = sorted(set(matches).intersection(STRONG_MEMORY_TERMS))
|
|
238
|
+
if is_relevant_memory_match(matches, strong_matches):
|
|
239
|
+
recency_bonus = max(0, min(3, total - index - 1))
|
|
240
|
+
kind_bonus = 2 if kind and record.kind == kind else 0
|
|
241
|
+
score = len(matches) * 10 + len(strong_matches) * 8 + recency_bonus + kind_bonus
|
|
242
|
+
reason_parts = [f"matched terms: {', '.join(matches)}"]
|
|
243
|
+
if strong_matches:
|
|
244
|
+
reason_parts.append(f"strong terms: {', '.join(strong_matches)}")
|
|
245
|
+
if recency_bonus:
|
|
246
|
+
reason_parts.append(f"recency bonus: {recency_bonus}")
|
|
247
|
+
if kind_bonus:
|
|
248
|
+
reason_parts.append(f"kind filter bonus: {kind_bonus}")
|
|
249
|
+
selected.append(
|
|
250
|
+
SelectedMemory(
|
|
251
|
+
record=record,
|
|
252
|
+
score=score,
|
|
253
|
+
reason="; ".join(reason_parts),
|
|
254
|
+
matched_terms=matches,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
selected = sorted(selected, key=lambda item: item.score, reverse=True)[:limit]
|
|
259
|
+
if budget_tokens is None:
|
|
260
|
+
return selected
|
|
261
|
+
|
|
262
|
+
packed: list[SelectedMemory] = []
|
|
263
|
+
remaining = budget_tokens
|
|
264
|
+
for item in selected:
|
|
265
|
+
cost = estimate_tokens(item.record.to_dict())
|
|
266
|
+
if cost <= remaining:
|
|
267
|
+
packed.append(item)
|
|
268
|
+
remaining -= cost
|
|
269
|
+
return packed
|
|
270
|
+
|
|
271
|
+
@contextmanager
|
|
272
|
+
def _connect(self) -> Iterator[sqlite3.Connection]:
|
|
273
|
+
self.workspace.state.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
db = sqlite3.connect(self.workspace.memory_db)
|
|
275
|
+
db.row_factory = sqlite3.Row
|
|
276
|
+
try:
|
|
277
|
+
yield db
|
|
278
|
+
db.commit()
|
|
279
|
+
finally:
|
|
280
|
+
db.close()
|
|
281
|
+
|
|
282
|
+
def _ensure_schema(self) -> None:
|
|
283
|
+
with self._connect() as db:
|
|
284
|
+
db.execute(
|
|
285
|
+
"""
|
|
286
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
287
|
+
id TEXT PRIMARY KEY,
|
|
288
|
+
kind TEXT NOT NULL,
|
|
289
|
+
text TEXT NOT NULL,
|
|
290
|
+
tags TEXT NOT NULL,
|
|
291
|
+
text_hash TEXT NOT NULL,
|
|
292
|
+
created_at TEXT NOT NULL,
|
|
293
|
+
updated_at TEXT NOT NULL,
|
|
294
|
+
archived_at TEXT
|
|
295
|
+
)
|
|
296
|
+
"""
|
|
297
|
+
)
|
|
298
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_memories_kind ON memories(kind)")
|
|
299
|
+
db.execute("CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(kind, text_hash)")
|
|
300
|
+
|
|
301
|
+
def _migrate_jsonl_if_needed(self) -> None:
|
|
302
|
+
if not self.workspace.memory_file.exists():
|
|
303
|
+
return
|
|
304
|
+
rows = Workspace.read_jsonl(self.workspace.memory_file)
|
|
305
|
+
if not rows:
|
|
306
|
+
return
|
|
307
|
+
with self._connect() as db:
|
|
308
|
+
count = db.execute("SELECT COUNT(*) AS count FROM memories").fetchone()["count"]
|
|
309
|
+
if count:
|
|
310
|
+
return
|
|
311
|
+
for row in rows:
|
|
312
|
+
record = MemoryRecord.from_dict(row)
|
|
313
|
+
validate_kind(record.kind)
|
|
314
|
+
self._insert_migrated(record)
|
|
315
|
+
|
|
316
|
+
def _insert_migrated(self, record: MemoryRecord) -> None:
|
|
317
|
+
clean_text = normalize_text(record.text)
|
|
318
|
+
clean_tags = normalize_tags(record.tags)
|
|
319
|
+
text_hash = memory_hash(record.kind, clean_text)
|
|
320
|
+
if self._active_by_hash(record.kind, text_hash):
|
|
321
|
+
return
|
|
322
|
+
with self._connect() as db:
|
|
323
|
+
db.execute(
|
|
324
|
+
"""
|
|
325
|
+
INSERT OR IGNORE INTO memories(id, kind, text, tags, text_hash, created_at, updated_at, archived_at)
|
|
326
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
|
+
""",
|
|
328
|
+
(
|
|
329
|
+
record.id,
|
|
330
|
+
record.kind,
|
|
331
|
+
clean_text,
|
|
332
|
+
json.dumps(clean_tags, ensure_ascii=False),
|
|
333
|
+
text_hash,
|
|
334
|
+
record.created_at,
|
|
335
|
+
record.updated_at,
|
|
336
|
+
record.archived_at,
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def _active_by_hash(self, kind: str, text_hash: str) -> MemoryRecord | None:
|
|
341
|
+
with self._connect() as db:
|
|
342
|
+
row = db.execute(
|
|
343
|
+
"""
|
|
344
|
+
SELECT * FROM memories
|
|
345
|
+
WHERE kind = ? AND text_hash = ? AND archived_at IS NULL
|
|
346
|
+
ORDER BY created_at ASC
|
|
347
|
+
LIMIT 1
|
|
348
|
+
""",
|
|
349
|
+
(kind, text_hash),
|
|
350
|
+
).fetchone()
|
|
351
|
+
return record_from_row(row) if row else None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def validate_kind(kind: str) -> None:
|
|
355
|
+
if kind not in ALLOWED_KINDS:
|
|
356
|
+
raise ValueError(f"Unsupported memory kind: {kind}. Expected one of: {', '.join(sorted(ALLOWED_KINDS))}")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def normalize_text(text: str) -> str:
|
|
360
|
+
normalized = " ".join(text.split())
|
|
361
|
+
if not normalized:
|
|
362
|
+
raise ValueError("Memory text cannot be empty.")
|
|
363
|
+
return normalized
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def normalize_tags(tags: list[str] | None) -> list[str]:
|
|
367
|
+
return sorted({tag.strip() for tag in tags or [] if tag.strip()})
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def memory_hash(kind: str, text: str) -> str:
|
|
371
|
+
canonical = f"{kind}\n{' '.join(text.split()).casefold()}"
|
|
372
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:24]
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def memory_retention_key(record: MemoryRecord) -> tuple[int, int, str, str]:
|
|
376
|
+
kind_priority = {
|
|
377
|
+
"preference": 50,
|
|
378
|
+
"decision": 45,
|
|
379
|
+
"fact": 40,
|
|
380
|
+
"project_state": 30,
|
|
381
|
+
"task_state": 20,
|
|
382
|
+
}.get(record.kind, 10)
|
|
383
|
+
tag_priority = 100 if any(tag.casefold() in {"keep", "pinned", "global"} for tag in record.tags) else 0
|
|
384
|
+
text_cost = estimate_tokens(record.to_dict())
|
|
385
|
+
return (tag_priority + kind_priority, -text_cost, record.updated_at, record.id)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def retention_score(
|
|
389
|
+
record: MemoryRecord,
|
|
390
|
+
token_cost: int,
|
|
391
|
+
recoverability: dict[str, object],
|
|
392
|
+
*,
|
|
393
|
+
index: int,
|
|
394
|
+
total: int,
|
|
395
|
+
) -> tuple[int, list[str]]:
|
|
396
|
+
kind_score = {
|
|
397
|
+
"preference": 60,
|
|
398
|
+
"decision": 55,
|
|
399
|
+
"fact": 42,
|
|
400
|
+
"project_state": 32,
|
|
401
|
+
"task_state": 18,
|
|
402
|
+
}.get(record.kind, 10)
|
|
403
|
+
reasons = [f"kind:{record.kind}+{kind_score}"]
|
|
404
|
+
score = kind_score
|
|
405
|
+
|
|
406
|
+
pinned_tags = sorted(set(tag.casefold() for tag in record.tags).intersection(PINNED_TAGS))
|
|
407
|
+
if pinned_tags:
|
|
408
|
+
score += 100
|
|
409
|
+
reasons.append(f"pinned:{','.join(pinned_tags)}+100")
|
|
410
|
+
|
|
411
|
+
recency_score = min(10, max(0, int(((index + 1) / max(1, total)) * 10)))
|
|
412
|
+
if recency_score:
|
|
413
|
+
score += recency_score
|
|
414
|
+
reasons.append(f"recency+{recency_score}")
|
|
415
|
+
|
|
416
|
+
if recoverability["level"] != "none":
|
|
417
|
+
if record.kind in RECOVERABLE_KINDS:
|
|
418
|
+
score -= 18
|
|
419
|
+
reasons.append("trace_recoverable-18")
|
|
420
|
+
else:
|
|
421
|
+
score += 4
|
|
422
|
+
reasons.append("trace_linked+4")
|
|
423
|
+
elif record.kind in RECOVERABLE_KINDS:
|
|
424
|
+
score += 8
|
|
425
|
+
reasons.append("not_recoverable+8")
|
|
426
|
+
|
|
427
|
+
size_penalty = min(25, max(0, token_cost // 40))
|
|
428
|
+
if size_penalty:
|
|
429
|
+
score -= size_penalty
|
|
430
|
+
reasons.append(f"token_cost-{size_penalty}")
|
|
431
|
+
return score, reasons
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def recoverability_summary(record: MemoryRecord, recovery_index: dict[str, list[str]]) -> dict[str, object]:
|
|
435
|
+
sources = recovery_index.get(record.id, [])
|
|
436
|
+
if not sources:
|
|
437
|
+
return {"level": "none", "sources": [], "reason": "No linked trace or task ref was found."}
|
|
438
|
+
level = "high" if record.kind in RECOVERABLE_KINDS else "linked"
|
|
439
|
+
return {
|
|
440
|
+
"level": level,
|
|
441
|
+
"sources": sources[:5],
|
|
442
|
+
"reason": "Record id appears in trace, agent-run, or task references.",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def memory_recovery_index(workspace: Workspace) -> dict[str, list[str]]:
|
|
447
|
+
index: dict[str, list[str]] = {}
|
|
448
|
+
for folder, label in [
|
|
449
|
+
(workspace.traces_dir, "trace"),
|
|
450
|
+
(workspace.agent_runs_dir, "agent_run"),
|
|
451
|
+
(workspace.tasks_dir, "task"),
|
|
452
|
+
]:
|
|
453
|
+
if not folder.exists():
|
|
454
|
+
continue
|
|
455
|
+
for path in sorted(folder.glob("*.json")):
|
|
456
|
+
data = safe_read_json(path)
|
|
457
|
+
if not data:
|
|
458
|
+
continue
|
|
459
|
+
source = f"{label}:{data.get('id') or path.stem}"
|
|
460
|
+
for record_id in memory_ids_from_document(data):
|
|
461
|
+
add_recovery_source(index, record_id, source)
|
|
462
|
+
return index
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def memory_ids_from_document(data: dict[str, object]) -> list[str]:
|
|
466
|
+
ids: list[str] = []
|
|
467
|
+
state = data.get("state")
|
|
468
|
+
if isinstance(state, dict):
|
|
469
|
+
records = state.get("records", [])
|
|
470
|
+
if isinstance(records, list):
|
|
471
|
+
for record in records:
|
|
472
|
+
if isinstance(record, dict) and record.get("id"):
|
|
473
|
+
ids.append(str(record["id"]))
|
|
474
|
+
refs = data.get("refs")
|
|
475
|
+
if isinstance(refs, dict):
|
|
476
|
+
memories = refs.get("memories", [])
|
|
477
|
+
if isinstance(memories, list):
|
|
478
|
+
ids.extend(str(item) for item in memories if item)
|
|
479
|
+
return ids
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def add_recovery_source(index: dict[str, list[str]], record_id: str, source: str) -> None:
|
|
483
|
+
sources = index.setdefault(record_id, [])
|
|
484
|
+
if source not in sources:
|
|
485
|
+
sources.append(source)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def safe_read_json(path) -> dict[str, object] | None:
|
|
489
|
+
try:
|
|
490
|
+
data = Workspace.read_json(path)
|
|
491
|
+
except (OSError, ValueError):
|
|
492
|
+
return None
|
|
493
|
+
return data if isinstance(data, dict) else None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def strip_retention_key(decision: dict[str, object]) -> dict[str, object]:
|
|
497
|
+
return {key: value for key, value in decision.items() if key != "retention_key"}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def record_from_row(row: sqlite3.Row) -> MemoryRecord:
|
|
501
|
+
return MemoryRecord(
|
|
502
|
+
id=str(row["id"]),
|
|
503
|
+
kind=str(row["kind"]),
|
|
504
|
+
text=str(row["text"]),
|
|
505
|
+
tags=list(json.loads(row["tags"])),
|
|
506
|
+
created_at=str(row["created_at"]),
|
|
507
|
+
updated_at=str(row["updated_at"]),
|
|
508
|
+
archived_at=row["archived_at"],
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def is_relevant_memory_match(matches: list[str], strong_matches: list[str]) -> bool:
|
|
513
|
+
if len(matches) >= 2:
|
|
514
|
+
return True
|
|
515
|
+
return bool(strong_matches)
|
context_kernel/models.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def utc_now() -> str:
|
|
9
|
+
return datetime.now(timezone.utc).isoformat()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Skill:
|
|
14
|
+
id: str
|
|
15
|
+
name: str
|
|
16
|
+
summary: str
|
|
17
|
+
intent: str
|
|
18
|
+
inputs: list[str] = field(default_factory=list)
|
|
19
|
+
outputs: list[str] = field(default_factory=list)
|
|
20
|
+
constraints: list[str] = field(default_factory=list)
|
|
21
|
+
failure_modes: list[str] = field(default_factory=list)
|
|
22
|
+
procedure: list[str] = field(default_factory=list)
|
|
23
|
+
examples: list[str] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def from_dict(data: dict[str, Any]) -> "Skill":
|
|
27
|
+
required = ["id", "name", "summary", "intent"]
|
|
28
|
+
missing = [key for key in required if not data.get(key)]
|
|
29
|
+
if missing:
|
|
30
|
+
raise ValueError(f"Skill is missing required fields: {', '.join(missing)}")
|
|
31
|
+
return Skill(
|
|
32
|
+
id=str(data["id"]),
|
|
33
|
+
name=str(data["name"]),
|
|
34
|
+
summary=str(data["summary"]),
|
|
35
|
+
intent=str(data["intent"]),
|
|
36
|
+
inputs=list(data.get("inputs", [])),
|
|
37
|
+
outputs=list(data.get("outputs", [])),
|
|
38
|
+
constraints=list(data.get("constraints", [])),
|
|
39
|
+
failure_modes=list(data.get("failure_modes", [])),
|
|
40
|
+
procedure=list(data.get("procedure", [])),
|
|
41
|
+
examples=list(data.get("examples", [])),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, Any]:
|
|
45
|
+
return {
|
|
46
|
+
"id": self.id,
|
|
47
|
+
"name": self.name,
|
|
48
|
+
"summary": self.summary,
|
|
49
|
+
"intent": self.intent,
|
|
50
|
+
"inputs": self.inputs,
|
|
51
|
+
"outputs": self.outputs,
|
|
52
|
+
"constraints": self.constraints,
|
|
53
|
+
"failure_modes": self.failure_modes,
|
|
54
|
+
"procedure": self.procedure,
|
|
55
|
+
"examples": self.examples,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def render_level(self, level: str) -> dict[str, Any]:
|
|
59
|
+
if level == "l0":
|
|
60
|
+
return {"id": self.id, "name": self.name, "summary": self.summary}
|
|
61
|
+
if level == "l1":
|
|
62
|
+
return {
|
|
63
|
+
"id": self.id,
|
|
64
|
+
"name": self.name,
|
|
65
|
+
"summary": self.summary,
|
|
66
|
+
"intent": self.intent,
|
|
67
|
+
"inputs": self.inputs,
|
|
68
|
+
"outputs": self.outputs,
|
|
69
|
+
}
|
|
70
|
+
if level == "l2":
|
|
71
|
+
data = self.render_level("l1")
|
|
72
|
+
data.update(
|
|
73
|
+
{
|
|
74
|
+
"constraints": self.constraints,
|
|
75
|
+
"failure_modes": self.failure_modes,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
return data
|
|
79
|
+
if level == "l3":
|
|
80
|
+
return self.to_dict()
|
|
81
|
+
raise ValueError(f"Unsupported skill level: {level}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class MemoryRecord:
|
|
86
|
+
id: str
|
|
87
|
+
kind: str
|
|
88
|
+
text: str
|
|
89
|
+
tags: list[str] = field(default_factory=list)
|
|
90
|
+
created_at: str = field(default_factory=utc_now)
|
|
91
|
+
updated_at: str = field(default_factory=utc_now)
|
|
92
|
+
archived_at: str | None = None
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict[str, Any]:
|
|
95
|
+
return {
|
|
96
|
+
"id": self.id,
|
|
97
|
+
"kind": self.kind,
|
|
98
|
+
"text": self.text,
|
|
99
|
+
"tags": self.tags,
|
|
100
|
+
"created_at": self.created_at,
|
|
101
|
+
"updated_at": self.updated_at,
|
|
102
|
+
"archived_at": self.archived_at,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def from_dict(data: dict[str, Any]) -> "MemoryRecord":
|
|
107
|
+
created_at = str(data.get("created_at", utc_now()))
|
|
108
|
+
return MemoryRecord(
|
|
109
|
+
id=str(data["id"]),
|
|
110
|
+
kind=str(data["kind"]),
|
|
111
|
+
text=str(data["text"]),
|
|
112
|
+
tags=list(data.get("tags", [])),
|
|
113
|
+
created_at=created_at,
|
|
114
|
+
updated_at=str(data.get("updated_at", created_at)),
|
|
115
|
+
archived_at=data.get("archived_at"),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class Budget:
|
|
121
|
+
profile: str
|
|
122
|
+
total: int
|
|
123
|
+
request: int
|
|
124
|
+
runtime: int
|
|
125
|
+
memory: int
|
|
126
|
+
skills: int
|
|
127
|
+
reserve: int
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class SelectedSkill:
|
|
132
|
+
skill: Skill
|
|
133
|
+
level: str
|
|
134
|
+
score: int
|
|
135
|
+
reason: str
|
|
136
|
+
matched_terms: list[str] = field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class SelectedMemory:
|
|
141
|
+
record: MemoryRecord
|
|
142
|
+
score: int
|
|
143
|
+
reason: str
|
|
144
|
+
matched_terms: list[str] = field(default_factory=list)
|