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.
Files changed (40) hide show
  1. akernel_runtime-0.1.0.dist-info/METADATA +270 -0
  2. akernel_runtime-0.1.0.dist-info/RECORD +40 -0
  3. akernel_runtime-0.1.0.dist-info/WHEEL +5 -0
  4. akernel_runtime-0.1.0.dist-info/entry_points.txt +2 -0
  5. akernel_runtime-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. akernel_runtime-0.1.0.dist-info/licenses/NOTICE +4 -0
  7. akernel_runtime-0.1.0.dist-info/top_level.txt +1 -0
  8. context_kernel/__init__.py +4 -0
  9. context_kernel/__main__.py +5 -0
  10. context_kernel/agent_reports.py +188 -0
  11. context_kernel/benchmarks.py +493 -0
  12. context_kernel/budget.py +72 -0
  13. context_kernel/cli.py +2953 -0
  14. context_kernel/context.py +161 -0
  15. context_kernel/evals.py +347 -0
  16. context_kernel/global_memory.py +126 -0
  17. context_kernel/loop.py +1617 -0
  18. context_kernel/marketplace.py +194 -0
  19. context_kernel/marketplace_data/skills/context_budget.json +27 -0
  20. context_kernel/marketplace_data/skills/context_compaction.json +27 -0
  21. context_kernel/marketplace_data/skills/edit_file.json +27 -0
  22. context_kernel/marketplace_data/skills/index.json +66 -0
  23. context_kernel/marketplace_data/skills/long_task_planning.json +27 -0
  24. context_kernel/marketplace_data/skills/multi_file_bugfix.json +28 -0
  25. context_kernel/memory.py +515 -0
  26. context_kernel/models.py +144 -0
  27. context_kernel/planner.py +155 -0
  28. context_kernel/policy.py +271 -0
  29. context_kernel/project.py +317 -0
  30. context_kernel/providers.py +1264 -0
  31. context_kernel/report_costs.py +375 -0
  32. context_kernel/runner.py +78 -0
  33. context_kernel/skills.py +318 -0
  34. context_kernel/state_writer.py +108 -0
  35. context_kernel/storage.py +171 -0
  36. context_kernel/tasks.py +549 -0
  37. context_kernel/text.py +42 -0
  38. context_kernel/tokenizer.py +22 -0
  39. context_kernel/tools.py +544 -0
  40. context_kernel/verifier.py +77 -0
@@ -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)
@@ -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)