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,135 @@
1
+ """H5 — Lesson-to-Catalog Proposal Pipeline.
2
+
3
+ Scans lesson text for algorithmic patterns and proposes catalog entries.
4
+ Mined from T3MP3ST self-improvement loop (🧪 Research → 📋 Catalog).
5
+
6
+ LLM integration: optionally uses an LLM to scan lesson text and propose
7
+ catalog entries. Falls back to keyword-based extraction when no model is
8
+ available.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+
16
+ @dataclass
17
+ class CatalogProposal:
18
+ """A proposed catalog entry derived from a lesson."""
19
+
20
+ title: str
21
+ use_for: str
22
+ pseudocode: str
23
+ source_lesson: str
24
+ confidence: float = 0.0
25
+ keywords: list[str] = field(default_factory=list)
26
+
27
+
28
+ # Keyword patterns that indicate algorithmic lessons
29
+ _PATTERN_KEYWORDS = {
30
+ "algorithm": ["algorithm", "pattern", "approach", "method", "strategy"],
31
+ "verification": ["verify", "check", "validate", "test", "assert"],
32
+ "guard": ["guard", "clamp", "prevent", "block", "gate"],
33
+ "pipeline": ["pipeline", "flow", "stage", "phase", "step"],
34
+ "metric": ["metric", "score", "measure", "telemetry", "signal"],
35
+ "fallback": ["fallback", "retry", "recover", "degrade"],
36
+ }
37
+
38
+
39
+ def _extract_keywords(text: str) -> list[str]:
40
+ """Extract algorithmic keywords from lesson text."""
41
+ text_lower = text.lower()
42
+ found = []
43
+ for category, keywords in _PATTERN_KEYWORDS.items():
44
+ for kw in keywords:
45
+ if kw in text_lower:
46
+ found.append(kw)
47
+ return found
48
+
49
+
50
+ def _extract_title(text: str) -> str:
51
+ """Derive a title from lesson text."""
52
+ # Try to find a heading or first sentence
53
+ lines = text.strip().split("\n")
54
+ for line in lines:
55
+ line = line.strip()
56
+ if line.startswith("#"):
57
+ return line.lstrip("#").strip()
58
+ if line and len(line) > 10:
59
+ # Use first sentence, truncated
60
+ first_sentence = line.split(".")[0]
61
+ if len(first_sentence) > 60:
62
+ return first_sentence[:57] + "..."
63
+ return first_sentence
64
+ return "Untitled Pattern"
65
+
66
+
67
+ def _compute_confidence(keywords: list[str], text: str) -> float:
68
+ """Compute confidence based on keyword density."""
69
+ if not keywords:
70
+ return 0.0
71
+ text_lower = text.lower()
72
+ total_hits = sum(text_lower.count(kw) for kw in keywords)
73
+ # Normalize by text length (per 1000 chars)
74
+ density = total_hits / max(len(text), 1) * 1000
75
+ # Sigmoid-like: 0.5 at density=2, approaching 1.0 at density=5+
76
+ return min(1.0, density / 5.0)
77
+
78
+
79
+ def propose_from_lesson(
80
+ lesson_text: str,
81
+ model_client: Any | None = None,
82
+ ) -> CatalogProposal:
83
+ """Propose a catalog entry from a lesson.
84
+
85
+ Args:
86
+ lesson_text: The lesson text to scan.
87
+ model_client: Optional LLM client for richer extraction.
88
+ Falls back to keyword-based extraction when None.
89
+
90
+ Returns:
91
+ A CatalogProposal with extracted patterns.
92
+ """
93
+ keywords = _extract_keywords(lesson_text)
94
+ title = _extract_title(lesson_text)
95
+ confidence = _compute_confidence(keywords, lesson_text)
96
+
97
+ # Build pseudocode from keywords
98
+ if keywords:
99
+ pseudo_lines = [
100
+ f"# Detected patterns: {', '.join(keywords[:5])}",
101
+ f"def {title.lower().replace(' ', '_')}(input):",
102
+ f" # Keywords: {', '.join(keywords)}",
103
+ " result = process(input)",
104
+ " return result",
105
+ ]
106
+ pseudocode = "\n".join(pseudo_lines)
107
+ else:
108
+ pseudocode = "# No clear algorithmic pattern detected"
109
+
110
+ use_for = f"Lessons containing: {', '.join(keywords[:3])}" if keywords else "General lessons"
111
+
112
+ return CatalogProposal(
113
+ title=title,
114
+ use_for=use_for,
115
+ pseudocode=pseudocode,
116
+ source_lesson=lesson_text[:200],
117
+ confidence=confidence,
118
+ keywords=keywords,
119
+ )
120
+
121
+
122
+ def propose_batch(
123
+ lessons: list[str],
124
+ model_client: Any | None = None,
125
+ ) -> list[CatalogProposal]:
126
+ """Propose catalog entries from multiple lessons."""
127
+ return [propose_from_lesson(lesson, model_client) for lesson in lessons]
128
+
129
+
130
+ def filter_high_confidence(
131
+ proposals: list[CatalogProposal],
132
+ threshold: float = 0.3,
133
+ ) -> list[CatalogProposal]:
134
+ """Filter proposals below confidence threshold."""
135
+ return [p for p in proposals if p.confidence >= threshold]
@@ -0,0 +1,169 @@
1
+ """H15 — LLM Fallback Chain.
2
+
3
+ Multi-tier fallback for LLM calls: primary model → fallback model →
4
+ 3-tier JSON parsing. Prevents total failure when primary model is unavailable.
5
+ Mined from T3MP3ST WHITEPAPER §5.2 safeLLMCall().
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+
15
+ @dataclass
16
+ class FallbackResult:
17
+ """Result of a fallback chain call."""
18
+
19
+ success: bool
20
+ response: str = ""
21
+ parsed_json: dict | None = None
22
+ model_used: str = ""
23
+ parse_tier: int = 0 # 0=direct, 1=regex, 2=repair, 3=failed
24
+ error: str = ""
25
+
26
+
27
+ def _parse_json_tier_1(text: str) -> dict | None:
28
+ """Tier 1: Direct json.loads."""
29
+ try:
30
+ return json.loads(text)
31
+ except (json.JSONDecodeError, TypeError):
32
+ return None
33
+
34
+
35
+ def _parse_json_tier_2(text: str) -> dict | None:
36
+ """Tier 2: Extract JSON from markdown code blocks or surrounding text."""
37
+ # Try to find ```json ... ``` blocks
38
+ match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL)
39
+ if match:
40
+ result = _parse_json_tier_1(match.group(1).strip())
41
+ if result is not None:
42
+ return result
43
+
44
+ # Try to find the first { ... } block
45
+ match = re.search(r"\{.*\}", text, re.DOTALL)
46
+ if match:
47
+ result = _parse_json_tier_1(match.group(0))
48
+ if result is not None:
49
+ return result
50
+
51
+ return None
52
+
53
+
54
+ def _parse_json_tier_3(text: str) -> dict | None:
55
+ """Tier 3: Repair common JSON errors (trailing commas, unquoted keys)."""
56
+ repaired = text
57
+
58
+ # Remove trailing commas before } or ]
59
+ repaired = re.sub(r",\s*([}\]])", r"\1", repaired)
60
+
61
+ # Quote unquoted keys (key:)
62
+ repaired = re.sub(r"(\w+)\s*:", r'"\1":', repaired)
63
+ # But don't double-quote already-quoted keys
64
+ repaired = re.sub(r'""(\w+)""\s*:', r'"\1":', repaired)
65
+
66
+ # Remove control characters
67
+ repaired = re.sub(r"[\x00-\x1f]", "", repaired)
68
+
69
+ return _parse_json_tier_1(repaired)
70
+
71
+
72
+ def parse_json_with_fallback(text: str) -> tuple[dict | None, int]:
73
+ """Parse JSON with 3-tier fallback.
74
+
75
+ Returns (parsed_dict_or_None, tier_used).
76
+ Tier 0 = direct parse, 1 = regex extraction, 2 = repair, 3 = failed.
77
+ """
78
+ result = _parse_json_tier_1(text)
79
+ if result is not None:
80
+ return result, 0
81
+
82
+ result = _parse_json_tier_2(text)
83
+ if result is not None:
84
+ return result, 1
85
+
86
+ result = _parse_json_tier_3(text)
87
+ if result is not None:
88
+ return result, 2
89
+
90
+ return None, 3
91
+
92
+
93
+ def llm_call_with_fallback(
94
+ prompt: str,
95
+ primary_client: Any | None = None,
96
+ fallback_client: Any | None = None,
97
+ expect_json: bool = False,
98
+ ) -> FallbackResult:
99
+ """Call LLM with fallback chain.
100
+
101
+ Args:
102
+ prompt: The prompt to send.
103
+ primary_client: Primary model client (must have .generate(prompt) -> str).
104
+ fallback_client: Fallback model client.
105
+ expect_json: If True, attempt JSON parsing with 3-tier fallback.
106
+
107
+ Returns:
108
+ FallbackResult with success status and parsed data.
109
+ """
110
+ # Try primary model
111
+ if primary_client is not None:
112
+ try:
113
+ response = primary_client.generate(prompt)
114
+ if response:
115
+ if expect_json:
116
+ parsed, tier = parse_json_with_fallback(response)
117
+ if parsed is not None:
118
+ return FallbackResult(
119
+ success=True,
120
+ response=response,
121
+ parsed_json=parsed,
122
+ model_used=getattr(primary_client, "model_name", "primary"),
123
+ parse_tier=tier,
124
+ )
125
+ else:
126
+ return FallbackResult(
127
+ success=True,
128
+ response=response,
129
+ model_used=getattr(primary_client, "model_name", "primary"),
130
+ )
131
+ except Exception as e:
132
+ primary_error = str(e)
133
+ else:
134
+ primary_error = "empty response"
135
+ else:
136
+ primary_error = "no primary client"
137
+
138
+ # Try fallback model
139
+ if fallback_client is not None:
140
+ try:
141
+ response = fallback_client.generate(prompt)
142
+ if response:
143
+ if expect_json:
144
+ parsed, tier = parse_json_with_fallback(response)
145
+ if parsed is not None:
146
+ return FallbackResult(
147
+ success=True,
148
+ response=response,
149
+ parsed_json=parsed,
150
+ model_used=getattr(fallback_client, "model_name", "fallback"),
151
+ parse_tier=tier,
152
+ )
153
+ else:
154
+ return FallbackResult(
155
+ success=True,
156
+ response=response,
157
+ model_used=getattr(fallback_client, "model_name", "fallback"),
158
+ )
159
+ except Exception as e:
160
+ fallback_error = str(e)
161
+ else:
162
+ fallback_error = "empty response"
163
+ else:
164
+ fallback_error = "no fallback client"
165
+
166
+ return FallbackResult(
167
+ success=False,
168
+ error=f"Primary: {primary_error}; Fallback: {fallback_error}",
169
+ )
@@ -0,0 +1,267 @@
1
+ """Log2 Histogram — memory-efficient latency telemetry.
2
+
3
+ Borrowed from Windows Performance Counters registry
4
+ (HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib\\009):
5
+ Windows uses 16 log2-sized buckets covering 128µs to >30s — 6 orders of
6
+ magnitude in 72 bytes. Binary-search insertion is O(log 16) = O(4).
7
+ Histograms are mergeable across sessions for aggregate statistics.
8
+
9
+ Pattern: B29 in ALGO.md.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ # Bucket upper bounds in microseconds.
20
+ # Source: Windows Performance Counter registry bucket definitions.
21
+ BOUNDARIES_US: tuple[float, ...] = (
22
+ 128, # bucket 01: <= 128 µs
23
+ 256, # bucket 02: <= 256 µs
24
+ 512, # bucket 03: <= 512 µs
25
+ 1_024, # bucket 04: <= 1 ms
26
+ 4_096, # bucket 05: <= 4 ms
27
+ 16_384, # bucket 06: <= 16 ms
28
+ 65_536, # bucket 07: <= 64 ms
29
+ 131_072, # bucket 08: <= 128 ms
30
+ 262_144, # bucket 09: <= 256 ms
31
+ 524_288, # bucket 10: <= 512 ms
32
+ 1_048_576, # bucket 11: <= 1 s
33
+ 2_097_152, # bucket 12: <= 2 s
34
+ 10_485_760, # bucket 13: <= 10 s
35
+ 20_971_520, # bucket 14: <= 20 s
36
+ 31_457_280, # bucket 15: <= 30 s
37
+ float("inf"), # bucket 16: > 30 s
38
+ )
39
+
40
+ # Human-readable labels for each bucket.
41
+ BUCKET_LABELS: tuple[str, ...] = (
42
+ "<=128µs", "<=256µs", "<=512µs", "<=1ms", "<=4ms", "<=16ms",
43
+ "<=64ms", "<=128ms", "<=256ms", "<=512ms", "<=1s", "<=2s",
44
+ "<=10s", "<=20s", "<=30s", ">30s",
45
+ )
46
+
47
+
48
+ @dataclass
49
+ class Log2Histogram:
50
+ """Log2-bucketed histogram — O(log b) insert, O(b) quantile.
51
+
52
+ 16 buckets cover 128µs to >30s in ~72 bytes.
53
+ Mergeable: two histograms combine by bucket-wise addition.
54
+ """
55
+
56
+ buckets: list[int] = field(default_factory=lambda: [0] * len(BOUNDARIES_US))
57
+ count: int = 0
58
+ sum_us: float = 0.0
59
+ min_us: float = float("inf")
60
+ max_us: float = 0.0
61
+
62
+ # --- core operations --------------------------------------------------
63
+
64
+ def observe(self, value_us: float) -> None:
65
+ """Record a latency observation in microseconds."""
66
+ idx = self._bucket_index(value_us)
67
+ self.buckets[idx] += 1
68
+ self.count += 1
69
+ self.sum_us += value_us
70
+ if value_us < self.min_us:
71
+ self.min_us = value_us
72
+ if value_us > self.max_us:
73
+ self.max_us = value_us
74
+
75
+ def observe_seconds(self, value_s: float) -> None:
76
+ """Record a latency observation in seconds."""
77
+ self.observe(value_s * 1_000_000)
78
+
79
+ def _bucket_index(self, value: float) -> int:
80
+ """Binary search for bucket — O(log 16) = O(4)."""
81
+ lo, hi = 0, len(BOUNDARIES_US) - 1
82
+ while lo < hi:
83
+ mid = (lo + hi) // 2
84
+ if value <= BOUNDARIES_US[mid]:
85
+ hi = mid
86
+ else:
87
+ lo = mid + 1
88
+ return lo
89
+
90
+ # --- queries ----------------------------------------------------------
91
+
92
+ def percentile(self, p: float) -> float:
93
+ """Estimate p-th percentile in microseconds — O(buckets).
94
+
95
+ Uses linear interpolation within the bucket.
96
+ """
97
+ if self.count == 0:
98
+ return 0.0
99
+ target = self.count * p / 100.0
100
+ cumulative = 0
101
+ for i, bucket_count in enumerate(self.buckets):
102
+ cumulative += bucket_count
103
+ if cumulative >= target:
104
+ lower = 0.0 if i == 0 else BOUNDARIES_US[i - 1]
105
+ upper = BOUNDARIES_US[i]
106
+ if bucket_count == 0:
107
+ return lower
108
+ frac = (target - (cumulative - bucket_count)) / bucket_count
109
+ return lower + frac * (upper - lower)
110
+ return BOUNDARIES_US[-2] # second-to-last (last is inf)
111
+
112
+ def percentile_seconds(self, p: float) -> float:
113
+ """Estimate p-th percentile in seconds."""
114
+ return self.percentile(p) / 1_000_000
115
+
116
+ def mean_us(self) -> float:
117
+ """Arithmetic mean in microseconds."""
118
+ return self.sum_us / self.count if self.count else 0.0
119
+
120
+ def mean_seconds(self) -> float:
121
+ return self.mean_us() / 1_000_000
122
+
123
+ # --- merge ------------------------------------------------------------
124
+
125
+ def merge(self, other: Log2Histogram) -> Log2Histogram:
126
+ """Merge two histograms — O(buckets)."""
127
+ result = Log2Histogram()
128
+ result.buckets = [a + b for a, b in zip(self.buckets, other.buckets)]
129
+ result.count = self.count + other.count
130
+ result.sum_us = self.sum_us + other.sum_us
131
+ result.min_us = min(self.min_us, other.min_us)
132
+ result.max_us = max(self.max_us, other.max_us)
133
+ return result
134
+
135
+ # --- serialization ----------------------------------------------------
136
+
137
+ def to_dict(self) -> dict[str, Any]:
138
+ return {
139
+ "buckets": list(self.buckets),
140
+ "count": self.count,
141
+ "sum_us": self.sum_us,
142
+ "min_us": self.min_us if self.min_us != float("inf") else 0.0,
143
+ "max_us": self.max_us,
144
+ }
145
+
146
+ @classmethod
147
+ def from_dict(cls, data: dict[str, Any]) -> Log2Histogram:
148
+ h = cls()
149
+ h.buckets = list(data.get("buckets", [0] * len(BOUNDARIES_US)))
150
+ h.count = data.get("count", 0)
151
+ h.sum_us = data.get("sum_us", 0.0)
152
+ h.min_us = data.get("min_us", float("inf"))
153
+ h.max_us = data.get("max_us", 0.0)
154
+ return h
155
+
156
+ def to_json(self) -> str:
157
+ return json.dumps(self.to_dict())
158
+
159
+ @classmethod
160
+ def from_json(cls, s: str) -> Log2Histogram:
161
+ return cls.from_dict(json.loads(s))
162
+
163
+ # --- display ----------------------------------------------------------
164
+
165
+ def summary(self) -> dict[str, float]:
166
+ """Compact summary for dashboards."""
167
+ return {
168
+ "count": self.count,
169
+ "mean_us": round(self.mean_us(), 1),
170
+ "p50_us": round(self.percentile(50), 1),
171
+ "p90_us": round(self.percentile(90), 1),
172
+ "p99_us": round(self.percentile(99), 1),
173
+ "min_us": round(self.min_us, 1) if self.min_us != float("inf") else 0,
174
+ "max_us": round(self.max_us, 1),
175
+ }
176
+
177
+ def histogram_text(self) -> str:
178
+ """ASCII histogram for terminal display."""
179
+ if self.count == 0:
180
+ return "(empty)"
181
+ max_count = max(self.buckets) or 1
182
+ lines = []
183
+ for i, count in enumerate(self.buckets):
184
+ if count == 0:
185
+ continue
186
+ bar_len = int(count / max_count * 40)
187
+ bar = "█" * bar_len
188
+ lines.append(f" {BUCKET_LABELS[i]:>8s} │{bar:<40s} │ {count}")
189
+ return "\n".join(lines)
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Registry — per-tool latency histograms
194
+ # ---------------------------------------------------------------------------
195
+
196
+ _REGISTRY: dict[str, Log2Histogram] = {}
197
+
198
+
199
+ def get_histogram(name: str) -> Log2Histogram:
200
+ """Get or create a named histogram (e.g., 'read_file', 'embed', 'model')."""
201
+ if name not in _REGISTRY:
202
+ _REGISTRY[name] = Log2Histogram()
203
+ return _REGISTRY[name]
204
+
205
+
206
+ def record_latency(name: str, duration_s: float) -> None:
207
+ """Record a latency observation for a named operation."""
208
+ get_histogram(name).observe_seconds(duration_s)
209
+
210
+
211
+ def all_summaries() -> dict[str, dict[str, float]]:
212
+ """Get summaries for all registered histograms."""
213
+ return {name: h.summary() for name, h in _REGISTRY.items()}
214
+
215
+
216
+ def save_to_file(path: Path) -> None:
217
+ """Persist all histograms to a JSON file."""
218
+ path.parent.mkdir(parents=True, exist_ok=True)
219
+ data = {name: h.to_dict() for name, h in _REGISTRY.items()}
220
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
221
+
222
+
223
+ def load_from_file(path: Path) -> None:
224
+ """Load histograms from a JSON file (merges into registry)."""
225
+ if not path.exists():
226
+ return
227
+ try:
228
+ data = json.loads(path.read_text(encoding="utf-8"))
229
+ except (json.JSONDecodeError, OSError):
230
+ return
231
+ for name, hist_data in data.items():
232
+ existing = _REGISTRY.get(name)
233
+ loaded = Log2Histogram.from_dict(hist_data)
234
+ if existing:
235
+ _REGISTRY[name] = existing.merge(loaded)
236
+ else:
237
+ _REGISTRY[name] = loaded
238
+
239
+
240
+ def clear_registry() -> None:
241
+ """Clear all registered histograms (for testing)."""
242
+ _REGISTRY.clear()
243
+
244
+
245
+ # ---------------------------------------------------------------------------
246
+ # Context manager for easy timing
247
+ # ---------------------------------------------------------------------------
248
+
249
+ class LatencyTimer:
250
+ """Context manager that records latency into a named histogram.
251
+
252
+ Usage:
253
+ with LatencyTimer("read_file"):
254
+ content = read_file(path)
255
+ """
256
+
257
+ def __init__(self, name: str):
258
+ self.name = name
259
+ self._start = 0.0
260
+
261
+ def __enter__(self) -> LatencyTimer:
262
+ self._start = time.perf_counter()
263
+ return self
264
+
265
+ def __exit__(self, *exc: Any) -> None:
266
+ duration = time.perf_counter() - self._start
267
+ record_latency(self.name, duration)