algo-cli-runtime 0.14.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 (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,394 @@
1
+ """Echo Veil integration for algo-cli tiered memory.
2
+
3
+ Wraps Echo Veil's Oracle as a decay-driven memory layer that sits between the
4
+ harness index and retrieval. Memories are managed through active/compressed/
5
+ archived tiers with proximity-based decay, preserving recent context while
6
+ compressing older data.
7
+
8
+ Usage:
9
+ from algo_cli.memory_echo_veil import EchoVeilMemoryLayer
10
+
11
+ # Initialize with embedding function and capacity
12
+ layer = EchoVeilMemoryLayer(
13
+ embed_fn=embed_fn,
14
+ capacity=400,
15
+ crypto_key=None # or AesGcmCryptoShield key for production
16
+ )
17
+
18
+ # Before retrieval: enrich context with active memories
19
+ enriched_context = layer.observe(query_embedding)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import os
27
+ import stat
28
+ import sys
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+ from typing import Any, Callable, Optional
32
+
33
+ try:
34
+ import echo_veil as _echo_veil_package
35
+ from echo_veil import Oracle, WorkspaceConfig, AesGcmCryptoShield
36
+ ECHO_VEIL_AVAILABLE = True
37
+ ECHO_VEIL_IMPORT_ERROR = ""
38
+ ECHO_VEIL_MODULE_ORIGIN = str(getattr(_echo_veil_package, "__file__", "") or "")
39
+ except ImportError as exc:
40
+ ECHO_VEIL_AVAILABLE = False
41
+ ECHO_VEIL_IMPORT_ERROR = type(exc).__name__
42
+ ECHO_VEIL_MODULE_ORIGIN = ""
43
+ Oracle = None # type: ignore
44
+ WorkspaceConfig = None # type: ignore
45
+ AesGcmCryptoShield = None # type: ignore
46
+
47
+ from .config import CONFIG_DIR, _atomic_write_text
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+
52
+ def get_echo_veil_readiness(config: dict[str, Any] | None = None) -> dict[str, Any]:
53
+ """Report the optional integration's current, deliberately narrow wiring.
54
+
55
+ Persisted state currently restores text metadata for diagnostics, not the
56
+ Oracle's memory tiers. Likewise, the runtime does not yet feed memories to
57
+ ``sprout`` or inject ``observe`` results into retrieval. Keep those stages
58
+ false until the corresponding end-to-end paths exist.
59
+ """
60
+ if config is None:
61
+ config_path = CONFIG_DIR / "config.json"
62
+ try:
63
+ loaded = json.loads(config_path.read_text(encoding="utf-8"))
64
+ except (OSError, json.JSONDecodeError):
65
+ loaded = {}
66
+ config = loaded if isinstance(loaded, dict) else {}
67
+
68
+ return {
69
+ "installed": ECHO_VEIL_AVAILABLE,
70
+ "enabled": bool(config.get("echo_veil_enabled", False)),
71
+ "write_wired": False,
72
+ "retrieval_wired": False,
73
+ "persistence_wired": False,
74
+ "readiness_source": "algo_cli.memory_echo_veil.get_echo_veil_readiness",
75
+ "runtime": f"{sys.implementation.name}-{sys.version_info.major}.{sys.version_info.minor}",
76
+ "module_origin": ECHO_VEIL_MODULE_ORIGIN or None,
77
+ "import_error": ECHO_VEIL_IMPORT_ERROR or None,
78
+ }
79
+
80
+
81
+ def _memory_records(raw: Any) -> list[dict[str, str]]:
82
+ if raw is None or isinstance(raw, (str, bytes, int, float, bool)):
83
+ return []
84
+ try:
85
+ iterator = iter(raw)
86
+ except TypeError:
87
+ return []
88
+
89
+ records: list[dict[str, str]] = []
90
+ for item in iterator:
91
+ topic = item.get("topic") if isinstance(item, dict) else getattr(item, "topic", None)
92
+ if topic is None:
93
+ topic = item.get("title") if isinstance(item, dict) else getattr(item, "title", None)
94
+ if topic is not None:
95
+ records.append({"topic": str(topic)})
96
+ return records
97
+
98
+
99
+ def _tier_count(raw: Any, records: list[dict[str, str]]) -> int:
100
+ if isinstance(raw, int) and not isinstance(raw, bool):
101
+ return raw
102
+ if raw is None or isinstance(raw, (str, bytes, float, bool)):
103
+ return len(records)
104
+ try:
105
+ return len(raw)
106
+ except TypeError:
107
+ return len(records)
108
+
109
+
110
+ class EchoVeilMemoryLayer:
111
+ """Tiered memory layer using Echo Veil's decay-driven architecture.
112
+
113
+ Manages memories through active/compressed/archived tiers with proximity-
114
+ based decay. Recent context stays active; older data is compressed and
115
+ eventually archived while preserving structured tension for contradictions.
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ embed_fn: Callable[[list[str]], list[list[float]]],
121
+ capacity: int = 400,
122
+ crypto_key: Optional[bytes] = None,
123
+ environment: str = "development",
124
+ persist_path: Optional[Path] = None,
125
+ crypto_key_path: Optional[Path] = None
126
+ ):
127
+ """Initialize Echo Veil memory layer.
128
+
129
+ Args:
130
+ embed_fn: Function to embed text into vectors (e.g., Ollama embed())
131
+ capacity: Maximum active memories before decay kicks in
132
+ crypto_key: AES-256-GCM key for production encryption, or None for dev
133
+ environment: "development" or "production" - production requires valid crypto
134
+ persist_path: Optional path to persist memory state; defaults to ~/.algo_cli/echo_veil_state.json
135
+ crypto_key_path: Path to JSON file containing 'key_hex' field (loads if crypto_key is None)
136
+ """
137
+ if not ECHO_VEIL_AVAILABLE:
138
+ raise ImportError(
139
+ "Echo Veil not installed. Run: pip install echo-veil"
140
+ )
141
+
142
+ self.embed_fn = embed_fn
143
+ self.capacity = capacity
144
+
145
+ # Load crypto key from file if path provided and no key given
146
+ if crypto_key is None and crypto_key_path is not None:
147
+ import json as _json
148
+ with open(crypto_key_path, 'r') as _f:
149
+ _key_data = _json.load(_f)
150
+ crypto_key = bytes.fromhex(_key_data['key_hex'])
151
+ self.environment = environment
152
+ self._loaded_state: dict[str, Any] | None = None
153
+ self._memory_text_by_title: dict[str, str] = {}
154
+
155
+ # Set up crypto shield for production
156
+ if environment == "production":
157
+ if crypto_key is None:
158
+ raise ValueError(
159
+ "Production mode requires a crypto key. "
160
+ "Generate one with AesGcmCryptoShield.generate_key()"
161
+ )
162
+ self.shield = AesGcmCryptoShield(crypto_key)
163
+ else:
164
+ # Development uses null shield (no encryption)
165
+ from echo_veil.crypto_shield import NullCryptoShield
166
+ self.shield = NullCryptoShield()
167
+
168
+ # Initialize Oracle with workspace config
169
+ self.oracle = Oracle(WorkspaceConfig(capacity=capacity), shield=self.shield)
170
+
171
+ # Persistence path
172
+ if persist_path is None:
173
+ self.persist_path = CONFIG_DIR / "echo_veil_state.json"
174
+ else:
175
+ self.persist_path = persist_path
176
+
177
+ # Load state if exists
178
+ self._load_state()
179
+
180
+ def sprout(self, title: str, text: str, embedding: list[float]) -> None:
181
+ """Add a new memory to the active tier.
182
+
183
+ Args:
184
+ title: Short descriptive title for the memory
185
+ text: Full text content of the memory (stored in metadata)
186
+ embedding: Vector representation from embed_fn
187
+ """
188
+ # Echo Veil uses topic as the vine identifier. The upstream API only
189
+ # accepts topic + embedding, so retain text locally for diagnostics and
190
+ # persistence metadata rather than silently discarding the argument.
191
+ self._memory_text_by_title[str(title)] = str(text)
192
+ self.oracle.sprout(title, embedding)
193
+
194
+ def observe(self, query_embedding: list[float]) -> dict[str, Any]:
195
+ """Process current intent through Echo Veil's decay model.
196
+
197
+ Returns context about active/compressed/archived memories relevant to query.
198
+
199
+ Args:
200
+ query_embedding: Vector representation of the current query/intent
201
+
202
+ Returns:
203
+ Dict with keys:
204
+ - active: List of currently active memory titles and proximity scores
205
+ - compressed: Summary of recently compressed memories (if any)
206
+ - drift_detected: Boolean indicating if intent has drifted significantly
207
+ """
208
+ # Run observe cycle which triggers decay
209
+ self.oracle.observe(query_embedding)
210
+
211
+ # Get current state from GardenersReport (counts only)
212
+ gardeners_report = self.oracle.report()
213
+
214
+ # Extract active memories directly from workspace vines
215
+ workspace_vines = getattr(self.oracle.workspace, 'vines', []) or []
216
+ active_memories = []
217
+ for vine in workspace_vines:
218
+ if hasattr(vine, 'topic'):
219
+ title = str(vine.topic)
220
+ active_memories.append({
221
+ "title": title,
222
+ "text": self._memory_text_by_title.get(title, ""),
223
+ "proximity": None,
224
+ "proximity_available": False,
225
+ })
226
+
227
+ # Extract compressed memories (twilight_grove may be a count or records)
228
+ twilight_raw = getattr(gardeners_report, 'twilight_grove', 0)
229
+ twilight_count = _tier_count(twilight_raw, _memory_records(twilight_raw))
230
+
231
+ return {
232
+ "active": active_memories,
233
+ "compressed": f"{twilight_count} memories in twilight grove",
234
+ "drift_detected": None,
235
+ "drift_available": False,
236
+ }
237
+
238
+ def get_report(self) -> dict[str, Any]:
239
+ """Get comprehensive memory state report.
240
+
241
+ Returns:
242
+ Dict with memory statistics and tier breakdown
243
+ """
244
+ gardeners_report = self.oracle.report()
245
+ active_raw = getattr(gardeners_report, 'thriving_vines', [])
246
+ compressed_raw = getattr(gardeners_report, 'twilight_grove', [])
247
+ active_memories = _memory_records(active_raw)
248
+ compressed_memories = _memory_records(compressed_raw)
249
+
250
+ return {
251
+ "active_memories": active_memories,
252
+ "compressed_memories": compressed_memories,
253
+ "archived_memories": [], # Would need archive access
254
+ "active_count": _tier_count(active_raw, active_memories),
255
+ "compressed_count": _tier_count(compressed_raw, compressed_memories),
256
+ "memory_pressure": getattr(gardeners_report, 'memory_pressure', None),
257
+ }
258
+
259
+ def capability_report(self) -> dict[str, Any]:
260
+ """Get defensive readiness and capability report.
261
+
262
+ Returns:
263
+ Dict covering crypto, storage, vector index, persistence status
264
+ """
265
+ cap_report = self.oracle.capability_report()
266
+
267
+ return {
268
+ "crypto_status": getattr(cap_report, 'crypto_status', {}),
269
+ "storage_status": getattr(cap_report, 'storage_status', {}),
270
+ "vector_index_status": getattr(cap_report, 'vector_index_status', {})
271
+ }
272
+
273
+ def _save_state(self) -> None:
274
+ """Persist memory state to disk."""
275
+ try:
276
+ # Extract serializable state from Oracle
277
+ report = self.get_report()
278
+ state = {
279
+ "capacity": self.capacity,
280
+ "environment": self.environment,
281
+ "active_count": report.get("active_count", len(report.get("active_memories", []))),
282
+ "compressed_count": report.get("compressed_count", len(report.get("compressed_memories", []))),
283
+ "archived_count": len(report.get("archived_memories", [])),
284
+ "memory_text_by_title": self._memory_text_by_title,
285
+ "last_updated": datetime.now(timezone.utc).isoformat()
286
+ }
287
+ self.persist_path.parent.mkdir(parents=True, exist_ok=True)
288
+ if os.name == "posix":
289
+ os.chmod(self.persist_path.parent, 0o700)
290
+ _atomic_write_text(self.persist_path, json.dumps(state, indent=2))
291
+ if os.name == "posix":
292
+ os.chmod(self.persist_path, 0o600)
293
+ except Exception as exc:
294
+ # Don't fail on persistence errors - memory layer should be resilient.
295
+ logger.debug("Echo Veil state save failed: %s", exc)
296
+
297
+ def _load_state(self) -> None:
298
+ """Load persisted state from disk if available."""
299
+ try:
300
+ if self.persist_path.exists():
301
+ with open(self.persist_path, 'r') as f:
302
+ state = json.load(f)
303
+ if isinstance(state, dict):
304
+ self._loaded_state = state
305
+ texts = state.get("memory_text_by_title")
306
+ if isinstance(texts, dict):
307
+ self._memory_text_by_title = {str(k): str(v) for k, v in texts.items()}
308
+ # State is informational - Oracle manages actual memory tiers.
309
+ return None
310
+ except Exception as exc:
311
+ logger.debug("Echo Veil state load failed: %s", exc)
312
+
313
+ def __del__(self):
314
+ """Cleanup - save state on deletion."""
315
+ self._save_state()
316
+
317
+
318
+ def create_echo_veil_layer(
319
+ embed_fn: Callable[[list[str]], list[list[float]]],
320
+ config: dict[str, Any] | None = None,
321
+ crypto_key_path: Optional[str] = None,
322
+ ) -> Optional[EchoVeilMemoryLayer]:
323
+ """Factory function to create Echo Veil layer from algo-cli config.
324
+
325
+ Args:
326
+ embed_fn: Embedding function (e.g., from harness.py)
327
+ config: algo-cli config dict; if None, reads from ~/.algo_cli/config.json
328
+ crypto_key_path: explicit path to key file (overrides config); the file
329
+ may be either a JSON envelope containing a ``key_hex`` field or raw
330
+ 32-byte key material.
331
+
332
+ Returns:
333
+ EchoVeilMemoryLayer instance or None if not enabled/available
334
+ """
335
+ if not ECHO_VEIL_AVAILABLE:
336
+ return None
337
+
338
+ if config is None:
339
+ config_path = CONFIG_DIR / "config.json"
340
+ if config_path.exists():
341
+ with open(config_path, 'r') as f:
342
+ config = json.load(f)
343
+ else:
344
+ return None
345
+
346
+ # Check if Echo Veil is enabled in config
347
+ if not config.get("echo_veil_enabled", False):
348
+ return None
349
+
350
+ capacity = config.get("echo_veil_capacity", 400)
351
+ environment = "production" if config.get("echo_veil_production", False) else "development"
352
+
353
+ # Resolve key path: explicit param wins, else fall back to config
354
+ resolved_key_path = crypto_key_path or config.get("echo_veil_crypto_key_path")
355
+
356
+ # Get crypto key (production mode only)
357
+ crypto_key = None
358
+ if environment == "production" and resolved_key_path:
359
+ key_file = Path(resolved_key_path)
360
+ if not key_file.exists():
361
+ raise FileNotFoundError(f"Echo Veil crypto key not found: {resolved_key_path}")
362
+ if key_file.is_symlink() or not key_file.is_file():
363
+ raise PermissionError("Echo Veil crypto key must be a regular non-symlink file")
364
+ if os.name == "posix" and stat.S_IMODE(key_file.stat().st_mode) & 0o077:
365
+ raise PermissionError(
366
+ "Echo Veil production key permissions are too broad; require mode 0600 or stricter"
367
+ )
368
+
369
+ # Try to parse as JSON envelope first, fall back to raw bytes
370
+ try:
371
+ with open(key_file, 'r') as f:
372
+ key_data = json.load(f)
373
+ key_hex = key_data.get("key_hex")
374
+ if not key_hex:
375
+ raise ValueError(f"Key file missing 'key_hex': {resolved_key_path}")
376
+ crypto_key = bytes.fromhex(key_hex)
377
+ except (json.JSONDecodeError, ValueError) as exc:
378
+ # Not JSON or missing field; try raw bytes
379
+ with open(key_file, 'rb') as f:
380
+ raw = f.read()
381
+ if len(raw) == 32:
382
+ crypto_key = raw
383
+ else:
384
+ raise ValueError(
385
+ f"Key file at {resolved_key_path} is not a valid JSON envelope "
386
+ f"with 'key_hex' and is not exactly 32 raw bytes (got {len(raw)} bytes)"
387
+ ) from exc
388
+
389
+ return EchoVeilMemoryLayer(
390
+ embed_fn=embed_fn,
391
+ capacity=capacity,
392
+ crypto_key=crypto_key,
393
+ environment=environment,
394
+ )
@@ -0,0 +1,112 @@
1
+ """Runtime seam for bounded automatic durable-memory capture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Mapping, Sequence
7
+ from typing import Any
8
+
9
+ from . import config as config_module
10
+ from . import memory_candidates
11
+ from .config import Config
12
+ from .perf_telemetry import record_perf_event
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _EXPLICIT_MEMORY_TOOLS = frozenset({"remember", "append_lesson"})
17
+ _SUCCESSFUL_TOOL_STATUSES = frozenset({"worked"})
18
+
19
+
20
+ def _successful_explicit_memory_write(tool_calls: Sequence[Mapping[str, Any]]) -> bool:
21
+ return any(
22
+ str(call.get("name") or "") in _EXPLICIT_MEMORY_TOOLS
23
+ and str(call.get("status") or "") in _SUCCESSFUL_TOOL_STATUSES
24
+ for call in tool_calls
25
+ )
26
+
27
+
28
+ def _record_result(result: Mapping[str, Any], *, source: str) -> None:
29
+ counts_value = result.get("counts")
30
+ state_value = result.get("state")
31
+ reasons_value = result.get("reason_counts")
32
+ counts = counts_value if isinstance(counts_value, Mapping) else {}
33
+ state = state_value if isinstance(state_value, Mapping) else {}
34
+ reasons = reasons_value if isinstance(reasons_value, Mapping) else {}
35
+ record_perf_event(
36
+ "memory_candidate",
37
+ source="agent" if source == "agent" else "chat",
38
+ status=str(result.get("status") or "unknown"),
39
+ reason=str(result.get("reason") or ""),
40
+ extracted=int(counts.get("extracted") or 0),
41
+ evaluated=int(counts.get("evaluated") or 0),
42
+ eligible=int(counts.get("eligible") or 0),
43
+ stored=int(counts.get("stored") or 0),
44
+ rejected=int(counts.get("rejected") or 0),
45
+ daily_writes=int(state.get("daily_writes") or 0),
46
+ auto_fingerprints=int(state.get("auto_fingerprints") or 0),
47
+ reason_counts={str(key): int(value) for key, value in reasons.items()},
48
+ )
49
+
50
+
51
+ def capture_completed_user_turn(
52
+ cfg: Config,
53
+ original_user_text: str,
54
+ *,
55
+ completed: bool,
56
+ tool_calls: Sequence[Mapping[str, Any]] = (),
57
+ source: str = "chat",
58
+ ) -> dict[str, Any]:
59
+ """Capture at most one memory after a verified completion boundary.
60
+
61
+ Only ``original_user_text`` reaches the deterministic candidate processor.
62
+ Assistant, tool, retrieval, and specialist output are never inspected.
63
+ """
64
+
65
+ safe_source = "agent" if source == "agent" else "chat"
66
+ if not completed:
67
+ result = {
68
+ "status": "skipped",
69
+ "reason": "incomplete_turn",
70
+ "counts": {},
71
+ "reason_counts": {},
72
+ "state": {},
73
+ }
74
+ _record_result(result, source=safe_source)
75
+ return result
76
+ if _successful_explicit_memory_write(tool_calls):
77
+ result = {
78
+ "status": "skipped",
79
+ "reason": "explicit_memory_write",
80
+ "counts": {},
81
+ "reason_counts": {},
82
+ "state": {},
83
+ }
84
+ _record_result(result, source=safe_source)
85
+ return result
86
+
87
+ try:
88
+ return memory_candidates.process_memory_candidates(
89
+ original_user_text,
90
+ tuple(str(item) for item in cfg.memories),
91
+ config_module.MEMORY_CANDIDATE_STATE_FILE,
92
+ bool(cfg.memory_auto_capture_enabled),
93
+ cfg.remember_fact,
94
+ telemetry=lambda result: _record_result(result, source=safe_source),
95
+ daily_limit=int(cfg.memory_auto_daily_limit),
96
+ entry_limit=int(cfg.memory_auto_entry_limit),
97
+ char_limit=int(cfg.memory_auto_char_limit),
98
+ )
99
+ except Exception as exc: # Completion must never fail because memory capture did.
100
+ logger.debug("Automatic memory capture failed: %s", exc)
101
+ result = {
102
+ "status": "error",
103
+ "reason": type(exc).__name__,
104
+ "counts": {},
105
+ "reason_counts": {"runtime_error": 1},
106
+ "state": {},
107
+ }
108
+ _record_result(result, source=safe_source)
109
+ return result
110
+
111
+
112
+ __all__ = ["capture_completed_user_turn"]