superlocalmemory 3.4.18 → 3.4.21

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 (172) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +42 -34
  3. package/bin/slm +11 -0
  4. package/bin/slm.bat +12 -0
  5. package/package.json +4 -3
  6. package/pyproject.toml +3 -2
  7. package/scripts/build-slm-hook.ps1 +40 -0
  8. package/scripts/build-slm-hook.sh +45 -0
  9. package/scripts/build_entry.py +452 -0
  10. package/scripts/ci/stage5b_gate.sh +50 -0
  11. package/scripts/postinstall/validation.js +187 -0
  12. package/scripts/postinstall-interactive.js +756 -0
  13. package/scripts/postinstall_binary.js +287 -0
  14. package/scripts/release_manifest.py +273 -0
  15. package/scripts/slm-hook.spec +56 -0
  16. package/skills/slm-build-graph/SKILL.md +423 -0
  17. package/skills/slm-list-recent/SKILL.md +348 -0
  18. package/skills/slm-recall/SKILL.md +343 -0
  19. package/skills/slm-remember/SKILL.md +194 -0
  20. package/skills/slm-show-patterns/SKILL.md +224 -0
  21. package/skills/slm-status/SKILL.md +363 -0
  22. package/skills/slm-switch-profile/SKILL.md +442 -0
  23. package/src/superlocalmemory/cli/commands.py +219 -79
  24. package/src/superlocalmemory/cli/context_commands.py +192 -0
  25. package/src/superlocalmemory/cli/daemon.py +15 -1
  26. package/src/superlocalmemory/cli/db_migrate.py +80 -0
  27. package/src/superlocalmemory/cli/escape_hatch.py +220 -0
  28. package/src/superlocalmemory/cli/main.py +72 -1
  29. package/src/superlocalmemory/core/context_cache.py +397 -0
  30. package/src/superlocalmemory/core/embeddings.py +8 -2
  31. package/src/superlocalmemory/core/engine.py +38 -2
  32. package/src/superlocalmemory/core/engine_wiring.py +1 -1
  33. package/src/superlocalmemory/core/ram_lock.py +111 -0
  34. package/src/superlocalmemory/core/recall_pipeline.py +433 -3
  35. package/src/superlocalmemory/core/recall_worker.py +8 -3
  36. package/src/superlocalmemory/core/security_primitives.py +635 -0
  37. package/src/superlocalmemory/core/shadow_router.py +319 -0
  38. package/src/superlocalmemory/core/slm_disabled.py +87 -0
  39. package/src/superlocalmemory/core/slmignore.py +125 -0
  40. package/src/superlocalmemory/core/topic_signature.py +143 -0
  41. package/src/superlocalmemory/core/worker_pool.py +14 -3
  42. package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
  43. package/src/superlocalmemory/evolution/budget.py +321 -0
  44. package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
  45. package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
  46. package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
  47. package/src/superlocalmemory/hooks/adapter_base.py +317 -0
  48. package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
  49. package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
  50. package/src/superlocalmemory/hooks/context_payload.py +312 -0
  51. package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
  52. package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
  53. package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
  54. package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
  55. package/src/superlocalmemory/hooks/ide_connector.py +25 -2
  56. package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
  57. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
  58. package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
  59. package/src/superlocalmemory/hooks/session_registry.py +186 -0
  60. package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
  61. package/src/superlocalmemory/hooks/sync_loop.py +114 -0
  62. package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
  63. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
  64. package/src/superlocalmemory/infra/backup.py +3 -3
  65. package/src/superlocalmemory/infra/cloud_backup.py +2 -2
  66. package/src/superlocalmemory/infra/event_bus.py +2 -2
  67. package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
  68. package/src/superlocalmemory/learning/arm_catalog.py +99 -0
  69. package/src/superlocalmemory/learning/bandit.py +526 -0
  70. package/src/superlocalmemory/learning/bandit_cache.py +133 -0
  71. package/src/superlocalmemory/learning/behavioral.py +53 -1
  72. package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
  73. package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
  74. package/src/superlocalmemory/learning/database.py +256 -0
  75. package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
  76. package/src/superlocalmemory/learning/ensemble.py +300 -0
  77. package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
  78. package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
  79. package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
  80. package/src/superlocalmemory/learning/labeler.py +87 -0
  81. package/src/superlocalmemory/learning/legacy_migration.py +277 -0
  82. package/src/superlocalmemory/learning/memory_merge.py +160 -0
  83. package/src/superlocalmemory/learning/model_cache.py +269 -0
  84. package/src/superlocalmemory/learning/model_rollback.py +278 -0
  85. package/src/superlocalmemory/learning/outcome_queue.py +284 -0
  86. package/src/superlocalmemory/learning/pattern_miner.py +415 -0
  87. package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
  88. package/src/superlocalmemory/learning/ranker.py +225 -81
  89. package/src/superlocalmemory/learning/ranker_common.py +163 -0
  90. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
  91. package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
  92. package/src/superlocalmemory/learning/reward.py +777 -0
  93. package/src/superlocalmemory/learning/reward_archive.py +210 -0
  94. package/src/superlocalmemory/learning/reward_boost.py +201 -0
  95. package/src/superlocalmemory/learning/reward_proxy.py +326 -0
  96. package/src/superlocalmemory/learning/shadow_test.py +524 -0
  97. package/src/superlocalmemory/learning/signal_worker.py +270 -0
  98. package/src/superlocalmemory/learning/signals.py +314 -0
  99. package/src/superlocalmemory/learning/trigram_index.py +547 -0
  100. package/src/superlocalmemory/mcp/server.py +5 -5
  101. package/src/superlocalmemory/mcp/tools_context.py +183 -0
  102. package/src/superlocalmemory/mcp/tools_core.py +92 -27
  103. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
  104. package/src/superlocalmemory/retrieval/engine.py +52 -0
  105. package/src/superlocalmemory/retrieval/reranker.py +4 -2
  106. package/src/superlocalmemory/server/api.py +2 -2
  107. package/src/superlocalmemory/server/bandit_loops.py +140 -0
  108. package/src/superlocalmemory/server/middleware/__init__.py +11 -0
  109. package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
  110. package/src/superlocalmemory/server/routes/backup.py +36 -13
  111. package/src/superlocalmemory/server/routes/behavioral.py +50 -19
  112. package/src/superlocalmemory/server/routes/brain.py +1234 -0
  113. package/src/superlocalmemory/server/routes/data_io.py +4 -4
  114. package/src/superlocalmemory/server/routes/events.py +2 -2
  115. package/src/superlocalmemory/server/routes/helpers.py +1 -1
  116. package/src/superlocalmemory/server/routes/learning.py +192 -7
  117. package/src/superlocalmemory/server/routes/memories.py +189 -1
  118. package/src/superlocalmemory/server/routes/prewarm.py +171 -0
  119. package/src/superlocalmemory/server/routes/profiles.py +3 -3
  120. package/src/superlocalmemory/server/routes/token.py +88 -0
  121. package/src/superlocalmemory/server/routes/ws.py +5 -5
  122. package/src/superlocalmemory/server/security_middleware.py +13 -7
  123. package/src/superlocalmemory/server/ui.py +2 -2
  124. package/src/superlocalmemory/server/unified_daemon.py +335 -3
  125. package/src/superlocalmemory/storage/migration_runner.py +545 -0
  126. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
  127. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
  128. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
  129. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
  130. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
  131. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
  132. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
  133. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
  134. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
  135. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
  136. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
  137. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
  138. package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
  139. package/src/superlocalmemory/storage/models.py +4 -0
  140. package/src/superlocalmemory/ui/css/brain.css +409 -0
  141. package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
  142. package/src/superlocalmemory/ui/index.html +459 -1345
  143. package/src/superlocalmemory/ui/js/brain.js +1321 -0
  144. package/src/superlocalmemory/ui/js/clusters.js +123 -4
  145. package/src/superlocalmemory/ui/js/init.js +48 -39
  146. package/src/superlocalmemory/ui/js/memories.js +88 -2
  147. package/src/superlocalmemory/ui/js/modal.js +71 -1
  148. package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
  149. package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
  150. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
  151. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  152. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  153. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
  154. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
  155. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
  156. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
  157. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
  158. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
  159. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  160. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  161. package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
  162. package/src/superlocalmemory/ui/js/behavioral.js +0 -447
  163. package/src/superlocalmemory/ui/js/graph-core.js +0 -447
  164. package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
  165. package/src/superlocalmemory/ui/js/learning.js +0 -435
  166. package/src/superlocalmemory/ui/js/patterns.js +0 -93
  167. package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
  168. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
  169. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  170. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  171. package/src/superlocalmemory.egg-info/requires.txt +0 -58
  172. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -40,11 +40,33 @@ from superlocalmemory.evolution.triggers import (
40
40
  )
41
41
  from superlocalmemory.evolution import mutation_generator as mutgen
42
42
  from superlocalmemory.evolution import blind_verifier as verifier
43
+ from superlocalmemory.evolution.budget import (
44
+ BudgetExhausted,
45
+ EvolutionBudget,
46
+ )
47
+ from superlocalmemory.evolution.llm_dispatch import _dispatch_llm
43
48
 
44
49
  logger = logging.getLogger(__name__)
45
50
 
46
51
  EVOLVED_SKILLS_DIR = Path.home() / ".claude" / "skills" / "evolved"
47
52
 
53
+ # Short-name → allow-listed model id. Kept narrow so an unknown alias
54
+ # falls through to haiku rather than silently dispatching sonnet.
55
+ _MODEL_ALIASES: dict[str, str] = {
56
+ "haiku": "claude-haiku-4-5",
57
+ "sonnet": "claude-sonnet-4-6",
58
+ "ollama": "ollama:llama3",
59
+ "ollama:llama3": "ollama:llama3",
60
+ "ollama:qwen2.5": "ollama:qwen2.5",
61
+ "claude-haiku-4-5": "claude-haiku-4-5",
62
+ "claude-sonnet-4-6": "claude-sonnet-4-6",
63
+ }
64
+
65
+
66
+ def _resolve_model_alias(alias: str) -> str:
67
+ """Translate a short alias (``haiku``/``sonnet``) to an allow-listed id."""
68
+ return _MODEL_ALIASES.get(alias, "claude-haiku-4-5")
69
+
48
70
 
49
71
  def detect_backend() -> str:
50
72
  """Auto-detect best available LLM backend.
@@ -85,13 +107,34 @@ class SkillEvolver:
85
107
  Auto-detects LLM backend: claude CLI → Ollama → API → none.
86
108
  """
87
109
 
88
- def __init__(self, db_path: str | Path, config: object | None = None):
110
+ def __init__(
111
+ self,
112
+ db_path: str | Path,
113
+ config: object | None = None,
114
+ *,
115
+ profile_id: str = "default",
116
+ budget: EvolutionBudget | None = None,
117
+ ):
89
118
  self._db_path = str(db_path)
90
119
  self._store = EvolutionStore(db_path)
91
120
  self._degradation = DegradationTrigger(db_path)
92
121
  self._health = HealthCheckTrigger(db_path)
93
122
  self._config = config
94
123
  self._backend: str | None = None
124
+ self._profile_id = profile_id
125
+ self._current_cycle_id: str | None = None
126
+
127
+ # SB-3: SkillEvolver always holds a budget. Default one is rooted
128
+ # at ~/.superlocalmemory so production callers pick it up
129
+ # automatically. Tests inject their own.
130
+ if budget is None:
131
+ slm_home = Path.home() / ".superlocalmemory"
132
+ budget = EvolutionBudget(
133
+ profile_id=profile_id,
134
+ learning_db=Path(self._db_path),
135
+ lock_dir=slm_home,
136
+ )
137
+ self._budget = budget
95
138
 
96
139
  def _is_enabled(self) -> bool:
97
140
  """Check if evolution is enabled in config."""
@@ -117,7 +160,13 @@ class SkillEvolver:
117
160
  return self._backend
118
161
 
119
162
  def run_consolidation_cycle(self, profile_id: str = "default") -> dict:
120
- """Run during consolidation. Checks triggers 2 and 3."""
163
+ """Run during consolidation. Checks triggers 2 and 3.
164
+
165
+ Wrapped in ``self._budget.cycle()`` — honours the
166
+ 30min/10-LLM-calls/3-cycles-per-day caps (SB-3). If the budget
167
+ is exhausted, returns ``{"aborted": True}`` rather than raising
168
+ so the consolidation worker can continue cleanly.
169
+ """
121
170
  if not self._is_enabled():
122
171
  return {"enabled": False, "message": "Evolution disabled. Enable via: slm config set evolution.enabled true"}
123
172
 
@@ -126,6 +175,30 @@ class SkillEvolver:
126
175
  return {"enabled": True, "backend": "none",
127
176
  "message": "No LLM backend available. Install Claude Code, Ollama, or set an API key."}
128
177
 
178
+ try:
179
+ with self._budget.cycle() as _b:
180
+ self._current_cycle_id = None # budget already recorded one
181
+ return self._run_consolidation_body(profile_id, backend)
182
+ except BudgetExhausted as exc:
183
+ logger.warning(
184
+ "evolution cycle aborted: budget exhausted [%s]",
185
+ getattr(exc, "dimension", "?"),
186
+ )
187
+ return {
188
+ "enabled": True,
189
+ "backend": backend,
190
+ "aborted": True,
191
+ "budget_exhausted": True,
192
+ "dimension": getattr(exc, "dimension", None),
193
+ "candidates": 0, "evolved": 0, "rejected": 0, "skipped": 0,
194
+ }
195
+ finally:
196
+ self._current_cycle_id = None
197
+
198
+ def _run_consolidation_body(
199
+ self, profile_id: str, backend: str,
200
+ ) -> dict:
201
+ """Inner consolidation loop — runs under an open budget cycle."""
129
202
  self._store.reset_cycle()
130
203
  results = {"candidates": 0, "evolved": 0, "rejected": 0, "skipped": 0, "backend": backend}
131
204
 
@@ -163,10 +236,33 @@ class SkillEvolver:
163
236
  def run_post_session(
164
237
  self, session_id: str, profile_id: str = "default",
165
238
  ) -> dict:
166
- """Run after a session ends. Checks trigger 1."""
239
+ """Run after a session ends. Checks trigger 1.
240
+
241
+ Wrapped in a budget cycle (SB-3) so post-session evolution is
242
+ subject to the same 10-LLM-call / 30-min wall-time cap.
243
+ """
167
244
  if not self._is_enabled():
168
245
  return {"enabled": False, "candidates": 0, "evolved": 0, "rejected": 0}
169
246
 
247
+ try:
248
+ with self._budget.cycle() as _b:
249
+ return self._run_post_session_body(session_id, profile_id)
250
+ except BudgetExhausted as exc:
251
+ logger.warning(
252
+ "post-session evolution aborted: budget exhausted [%s]",
253
+ getattr(exc, "dimension", "?"),
254
+ )
255
+ return {
256
+ "enabled": True, "aborted": True, "budget_exhausted": True,
257
+ "dimension": getattr(exc, "dimension", None),
258
+ "candidates": 0, "evolved": 0, "rejected": 0,
259
+ }
260
+ finally:
261
+ self._current_cycle_id = None
262
+
263
+ def _run_post_session_body(
264
+ self, session_id: str, profile_id: str,
265
+ ) -> dict:
170
266
  results = {"candidates": 0, "evolved": 0, "rejected": 0}
171
267
 
172
268
  trigger = PostSessionTrigger(self._db_path)
@@ -304,108 +400,62 @@ class SkillEvolver:
304
400
  return "evolved"
305
401
 
306
402
  # ------------------------------------------------------------------
307
- # LLM calls — isolated, easy to mock in tests
403
+ # LLM calls — single-line funnel through evolution.llm_dispatch
308
404
  # ------------------------------------------------------------------
405
+ #
406
+ # SB-2: every LLM call goes through ``_dispatch_llm``. That function
407
+ # owns model validation, redact_secrets, backend registry, and the
408
+ # cost-log write. No other code in this module touches a backend.
409
+ # SB-4: ``_dispatch_llm`` routes the claude-CLI path through
410
+ # ``run_subprocess_safe`` — no bare ``subprocess.run`` here.
411
+
412
+ def _llm_call(
413
+ self, prompt: str, max_tokens: int = 500, model: str = "haiku",
414
+ ) -> str:
415
+ """Funnel a prompt through :func:`_dispatch_llm`.
309
416
 
310
- def _llm_call(self, prompt: str, max_tokens: int = 500, model: str = "haiku") -> str:
311
- """Make an LLM call using the detected backend.
312
-
313
- Priority: claude CLI Ollama API empty string
314
- The `model` parameter differentiates generator ("sonnet") from
315
- verifier ("haiku") calls so mutations use a stronger model.
417
+ The ``model`` arg accepts short aliases (``haiku``/``sonnet``)
418
+ for backward compatibility; they are resolved to the allow-listed
419
+ model id before dispatch. Budget charge happens BEFORE dispatch;
420
+ on ``BudgetExhausted`` we return an empty string so the caller
421
+ treats this as "no evolution" (the usual fail-closed semantics).
316
422
  """
317
423
  backend = self._get_backend()
424
+ if backend == "none":
425
+ return ""
318
426
 
319
- if backend == "claude":
320
- return self._call_claude_cli(prompt, max_tokens, model=model)
321
- elif backend == "ollama":
322
- return self._call_ollama(prompt, max_tokens)
323
- elif backend in ("anthropic", "openai"):
324
- return self._call_api(prompt, max_tokens, backend, model=model)
325
- return ""
326
-
327
- def _call_claude_cli(self, prompt: str, max_tokens: int, model: str = "haiku") -> str:
328
- """Spawn `claude --model <model>` for a single completion (ECC pattern)."""
329
- import subprocess
330
- import tempfile
331
-
332
- # Write prompt to temp file (avoids shell escaping issues)
333
- with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
334
- f.write(prompt)
335
- prompt_file = f.name
336
-
427
+ # Charge the budget BEFORE dispatching so the cap is protective.
428
+ # If budget is exhausted we degrade gracefully (empty string).
337
429
  try:
338
- result = subprocess.run(
339
- ["claude", "--model", model, "--print", "--no-input",
340
- "--max-tokens", str(max_tokens),
341
- "--prompt-file", prompt_file],
342
- capture_output=True, text=True, timeout=120,
343
- env={**os.environ, "CLAUDE_CODE_ENTRYPOINT": "cli",
344
- "ECC_SKIP_OBSERVE": "1"}, # Don't observe our own evolution calls
345
- )
346
- return result.stdout.strip() if result.returncode == 0 else ""
347
- except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as exc:
348
- logger.debug("Claude CLI call failed: %s", exc)
430
+ self._budget.charge_llm_call()
431
+ self._budget.check_time()
432
+ except BudgetExhausted as exc:
433
+ logger.info("evolution _llm_call skipped: %s", exc)
349
434
  return ""
350
- finally:
351
- try:
352
- os.unlink(prompt_file)
353
- except OSError:
354
- pass
355
-
356
- def _call_ollama(self, prompt: str, max_tokens: int) -> str:
357
- """Call Ollama API for local LLM completion."""
358
- import urllib.request
359
- import json as _json
360
-
361
- payload = _json.dumps({
362
- "model": "llama3",
363
- "prompt": prompt,
364
- "stream": False,
365
- "options": {"num_predict": max_tokens},
366
- }).encode()
367
-
435
+ except RuntimeError:
436
+ # charge_llm_call/check_time outside cycle() — shouldn't
437
+ # happen in production wiring, but if a caller invokes
438
+ # _llm_call without opening a cycle (e.g. tests), let it
439
+ # pass so legacy behaviour is preserved.
440
+ pass
441
+
442
+ resolved_model = _resolve_model_alias(model)
368
443
  try:
369
- req = urllib.request.Request(
370
- "http://127.0.0.1:11434/api/generate",
371
- data=payload,
372
- headers={"Content-Type": "application/json"},
373
- method="POST",
444
+ return _dispatch_llm(
445
+ prompt,
446
+ model=resolved_model,
447
+ learning_db=Path(self._db_path),
448
+ profile_id=self._profile_id,
449
+ max_tokens=max_tokens,
450
+ cycle_id=self._current_cycle_id,
374
451
  )
375
- with urllib.request.urlopen(req, timeout=120) as resp:
376
- data = _json.loads(resp.read())
377
- return data.get("response", "")
378
- except Exception as exc:
379
- logger.debug("Ollama call failed: %s", exc)
452
+ except ValueError as exc:
453
+ # Gate rejected — treat like "no LLM available".
454
+ logger.warning("evolution dispatch rejected: %s", exc)
380
455
  return ""
381
-
382
- def _call_api(self, prompt: str, max_tokens: int, provider: str, model: str = "haiku") -> str:
383
- """Call Anthropic or OpenAI API directly."""
384
- try:
385
- if provider == "anthropic":
386
- import anthropic
387
- client = anthropic.Anthropic()
388
- api_model = "claude-sonnet-4-6-20250514" if model == "sonnet" else "claude-haiku-4-5-20251001"
389
- msg = client.messages.create(
390
- model=api_model,
391
- max_tokens=max_tokens,
392
- messages=[{"role": "user", "content": prompt}],
393
- )
394
- return msg.content[0].text if msg.content else ""
395
- elif provider == "openai":
396
- import openai
397
- client = openai.OpenAI()
398
- api_model = "gpt-4o" if model == "sonnet" else "gpt-4o-mini"
399
- resp = client.chat.completions.create(
400
- model=api_model,
401
- max_tokens=max_tokens,
402
- messages=[{"role": "user", "content": prompt}],
403
- )
404
- return resp.choices[0].message.content or ""
405
- except Exception as exc:
406
- logger.debug("API call failed (%s): %s", provider, exc)
456
+ except Exception as exc: # noqa: BLE001 — fail-closed
457
+ logger.debug("evolution dispatch failed: %s", exc)
407
458
  return ""
408
- return "" # Safety net: unmatched provider returns empty string
409
459
 
410
460
  def _llm_confirm(self, candidate: EvolutionCandidate, original: str) -> bool:
411
461
  """LLM confirmation gate."""