superlocalmemory 3.4.19 → 3.4.22
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.
- package/CHANGELOG.md +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- 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__(
|
|
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 —
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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."""
|