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,389 @@
1
+ """Performance event buffering and JSONL persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import threading
8
+ import time
9
+ from typing import Any
10
+
11
+ from rich.text import Text
12
+
13
+ from .chat_protocol import get_attr
14
+ from .config import Config, PERF_HISTORY_FILE
15
+ from .display import console, show_info
16
+ from .private_event_store import PrivateEventStore, RetentionPolicy
17
+
18
+ PERF_BUFFER: list[dict[str, Any]] = []
19
+ _PERF_BUFFER_LOCK = threading.Lock()
20
+ _PERF_FLUSH_LOCK = threading.Lock()
21
+ _PERF_TAIL_BLOCK_BYTES = 64 * 1024
22
+ _PERF_TAIL_MAX_BYTES = 2 * 1024 * 1024
23
+ _PERF_BUFFER_MAX_RECORDS = 256
24
+ _PRIVATE_PERF_MAX_RECORDS = 1_000
25
+ _PRIVATE_PERF_MAX_BYTES = 4 * 1024 * 1024
26
+ _PRIVATE_PERF_MAX_AGE_SECONDS = 30 * 24 * 60 * 60
27
+
28
+
29
+ def _private_perf_store() -> PrivateEventStore:
30
+ return PrivateEventStore(
31
+ PERF_HISTORY_FILE.parent / "private" / "runtime_events.jsonl",
32
+ policy=RetentionPolicy(
33
+ max_records=_PRIVATE_PERF_MAX_RECORDS,
34
+ max_bytes=_PRIVATE_PERF_MAX_BYTES,
35
+ max_age_seconds=_PRIVATE_PERF_MAX_AGE_SECONDS,
36
+ ),
37
+ )
38
+
39
+
40
+ def private_perf_store_readiness() -> dict[str, Any]:
41
+ """Return content-free permission and retention state for diagnostics."""
42
+
43
+ return _private_perf_store().readiness()
44
+
45
+
46
+ def runtime_performance_snapshot(history: list[dict[str, Any]] | None = None) -> dict[str, Any]:
47
+ """Score comparable latency series and return the worst L3 CUSUM result."""
48
+ from .evals.performance_regression import detect_cusum
49
+
50
+ if history is not None:
51
+ rows = list(history)
52
+ else:
53
+ with _PERF_BUFFER_LOCK:
54
+ buffered = list(PERF_BUFFER)
55
+ rows = [*load_perf_history(limit=120), *buffered]
56
+
57
+ values_by_series: dict[str, list[float]] = {}
58
+ latest_by_series: dict[str, int] = {}
59
+ for position, item in enumerate(rows):
60
+ event = str(item.get("event") or "")
61
+ if event == "chat":
62
+ try:
63
+ duration_ms = float(item.get("total_duration") or 0) / 1_000_000
64
+ except (TypeError, ValueError):
65
+ continue
66
+ if duration_ms <= 0:
67
+ continue
68
+ series_key = f"chat:{item.get('model') or '?'}"
69
+ elif event == "tool" and str(item.get("status") or "") == "worked":
70
+ try:
71
+ duration_ms = float(item.get("duration_ms") or 0)
72
+ except (TypeError, ValueError):
73
+ continue
74
+ if duration_ms <= 0:
75
+ continue
76
+ series_key = f"tool:{item.get('tool') or '?'}"
77
+ else:
78
+ continue
79
+ values_by_series.setdefault(series_key, []).append(duration_ms)
80
+ latest_by_series[series_key] = position
81
+
82
+ if not values_by_series:
83
+ return {"series": "none", **detect_cusum([]).to_dict()}
84
+
85
+ scored = [(series, detect_cusum(values)) for series, values in values_by_series.items()]
86
+ eligible = [item for item in scored if item[1].state.value != "insufficient_data"]
87
+ candidates = eligible or scored
88
+
89
+ state_risk = {
90
+ "insufficient_data": 0,
91
+ "improving": 1,
92
+ "stable": 2,
93
+ "regressing": 3,
94
+ }
95
+
96
+ def risk(item: tuple[str, Any]) -> tuple[float, float, int, int]:
97
+ series, result = item
98
+ scale = float(result.scale or 0.0)
99
+ upward_pressure = float(result.positive_score) / scale if scale > 0 else 0.0
100
+ return (
101
+ float(state_risk[result.state.value]),
102
+ upward_pressure,
103
+ int(result.sample_count),
104
+ latest_by_series[series],
105
+ )
106
+
107
+ series_key, result = max(candidates, key=risk)
108
+ return {"series": series_key, **result.to_dict()}
109
+
110
+
111
+ def runtime_quality_snapshot(
112
+ cfg: Config,
113
+ *,
114
+ tool_limit: int = 12,
115
+ performance_history: list[dict[str, Any]] | None = None,
116
+ ) -> dict[str, Any]:
117
+ """Return safe, observable runtime quality signals for ``/selfcheck``.
118
+
119
+ Tool cadence comes from the bounded attempt ledger. Private model reasoning
120
+ is deliberately neither persisted nor inspected, so its availability is
121
+ reported instead of manufacturing a score from hidden state.
122
+ """
123
+ from .evals.cot_quality import score_tool_sequence
124
+
125
+ recent = cfg.attempt_ledger[-max(1, tool_limit) :]
126
+ tool_names = [
127
+ str(item.get("tool") or "")
128
+ for item in recent
129
+ if str(item.get("tool") or "").strip()
130
+ ]
131
+ return {
132
+ "tool_sequence": score_tool_sequence(tool_names).to_dict(),
133
+ "performance": runtime_performance_snapshot(performance_history),
134
+ "reasoning_quality": {
135
+ "status": "not_collected",
136
+ "reason": "Private model reasoning is neither persisted nor inspected.",
137
+ },
138
+ }
139
+
140
+
141
+ def render_runtime_quality_snapshot(cfg: Config) -> str:
142
+ """Render the safe quality snapshot used by ``/selfcheck``."""
143
+ snapshot = runtime_quality_snapshot(cfg)
144
+ sequence = snapshot["tool_sequence"]
145
+ performance = snapshot["performance"]
146
+ reasoning = snapshot["reasoning_quality"]
147
+ baseline = performance.get("baseline")
148
+ baseline_text = f", baseline {float(baseline):.1f} ms" if baseline is not None else ""
149
+ return "\n".join(
150
+ (
151
+ "[bold primary]Runtime quality diagnostics[/]",
152
+ (
153
+ f" tool cadence: {sequence['pattern']} "
154
+ f"(score {sequence['sequence_score']:.2f}, "
155
+ f"verification {'yes' if sequence['verification_present'] else 'no'})"
156
+ ),
157
+ (
158
+ f" latency trend: {performance['state']} "
159
+ f"({performance['series']}, n={performance['sample_count']}{baseline_text})"
160
+ ),
161
+ f" reasoning quality: {reasoning['status']} — {reasoning['reason']}",
162
+ )
163
+ )
164
+
165
+
166
+ def _runtime_status() -> dict[str, Any]:
167
+ from . import main as _main
168
+
169
+ return _main.RUNTIME_STATUS
170
+
171
+
172
+ def append_perf_record(record: dict[str, Any]) -> None:
173
+ with _PERF_BUFFER_LOCK:
174
+ PERF_BUFFER.append(record)
175
+ should_flush = len(PERF_BUFFER) >= 12 or record.get("event") != "tool"
176
+ if should_flush:
177
+ flush_perf_records()
178
+
179
+
180
+ def flush_perf_records() -> bool:
181
+ global PERF_BUFFER
182
+
183
+ # Serialize flushers, but release the producer lock before disk I/O. The
184
+ # object swap gives this flush an immutable batch while new events continue
185
+ # accumulating in a fresh buffer.
186
+ with _PERF_FLUSH_LOCK:
187
+ with _PERF_BUFFER_LOCK:
188
+ if not PERF_BUFFER:
189
+ return True
190
+ pending = PERF_BUFFER
191
+ PERF_BUFFER = []
192
+ try:
193
+ _private_perf_store().append({"kind": "perf_batch", "records": pending})
194
+ except Exception as exc:
195
+ # Telemetry must never break the runtime. Preserve a bounded newest
196
+ # suffix so a later writable flush can recover without a memory leak.
197
+ with _PERF_BUFFER_LOCK:
198
+ PERF_BUFFER = [*pending, *PERF_BUFFER][-_PERF_BUFFER_MAX_RECORDS:]
199
+ try:
200
+ _runtime_status()["perf_store"] = {
201
+ "status": "degraded",
202
+ "error_type": type(exc).__name__,
203
+ "buffered": len(PERF_BUFFER),
204
+ }
205
+ except Exception:
206
+ pass
207
+ return False
208
+ try:
209
+ _runtime_status()["perf_store"] = {
210
+ "status": "ready",
211
+ "buffered": len(PERF_BUFFER),
212
+ }
213
+ except Exception:
214
+ pass
215
+ return True
216
+
217
+
218
+ def record_perf_event(event: str, **fields: Any) -> None:
219
+ record = {"event": event, "timestamp": time.time(), **fields}
220
+ _runtime_status()[f"last_{event}_metrics"] = record
221
+ append_perf_record(record)
222
+
223
+
224
+ def record_chat_metrics(cfg: Config, chunk: Any) -> None:
225
+ metric_names = (
226
+ "total_duration",
227
+ "load_duration",
228
+ "prompt_eval_count",
229
+ "prompt_eval_duration",
230
+ "eval_count",
231
+ "eval_duration",
232
+ )
233
+ metrics = {name: get_attr(chunk, name, None) for name in metric_names}
234
+ if not any(value is not None for value in metrics.values()):
235
+ return
236
+ record = {
237
+ "event": "chat",
238
+ "timestamp": time.time(),
239
+ "model": cfg.model,
240
+ "cloud": cfg.cloud,
241
+ "keep_alive": cfg.keep_alive,
242
+ **metrics,
243
+ }
244
+ _runtime_status()["last_metrics"] = record
245
+ append_perf_record(record)
246
+
247
+
248
+ def log_embed_perf(record: dict[str, Any], *, source: str, backend: str | None = None) -> None:
249
+ payload: dict[str, Any] = {"timestamp": time.time(), "source": source}
250
+ if backend is not None:
251
+ payload["backend"] = backend
252
+ payload.update(record)
253
+ try:
254
+ _private_perf_store().append({"kind": "embed", "record": payload})
255
+ except (OSError, TypeError, ValueError):
256
+ pass
257
+
258
+
259
+ def _load_legacy_perf_history(limit: int) -> list[dict[str, Any]]:
260
+ if not PERF_HISTORY_FILE.exists():
261
+ return []
262
+ line_limit = max(1, int(limit))
263
+ if os.name == "posix":
264
+ try:
265
+ os.chmod(PERF_HISTORY_FILE, 0o600)
266
+ except OSError:
267
+ pass
268
+ try:
269
+ with PERF_HISTORY_FILE.open("rb") as handle:
270
+ handle.seek(0, 2)
271
+ start = handle.tell()
272
+ chunks: list[bytes] = []
273
+ newline_count = 0
274
+ bytes_read = 0
275
+ while start > 0 and newline_count < line_limit + 1 and bytes_read < _PERF_TAIL_MAX_BYTES:
276
+ chunk_size = min(_PERF_TAIL_BLOCK_BYTES, start, _PERF_TAIL_MAX_BYTES - bytes_read)
277
+ start -= chunk_size
278
+ handle.seek(start)
279
+ chunk = handle.read(chunk_size)
280
+ chunks.append(chunk)
281
+ newline_count += chunk.count(b"\n")
282
+ bytes_read += len(chunk)
283
+ starts_on_boundary = start == 0
284
+ if start > 0:
285
+ handle.seek(start - 1)
286
+ starts_on_boundary = handle.read(1) == b"\n"
287
+ except OSError:
288
+ return []
289
+ payload = b"".join(reversed(chunks))
290
+ if not starts_on_boundary:
291
+ _partial, separator, payload = payload.partition(b"\n")
292
+ if not separator:
293
+ return []
294
+ lines = payload.decode("utf-8", errors="replace").splitlines()[-line_limit:]
295
+ rows: list[dict[str, Any]] = []
296
+ for raw in lines:
297
+ try:
298
+ item = json.loads(raw)
299
+ except json.JSONDecodeError:
300
+ continue
301
+ if isinstance(item, dict):
302
+ rows.append(item)
303
+ return rows
304
+
305
+
306
+ def load_perf_history(limit: int = 8) -> list[dict[str, Any]]:
307
+ line_limit = max(1, int(limit))
308
+ private_rows: list[dict[str, Any]] = []
309
+ try:
310
+ events = _private_perf_store().read_events(limit=max(32, line_limit))
311
+ except OSError:
312
+ events = []
313
+ for event in events:
314
+ if event.get("kind") != "perf_batch":
315
+ continue
316
+ records = event.get("records")
317
+ if isinstance(records, list):
318
+ private_rows.extend(item for item in records if isinstance(item, dict))
319
+ private_rows = private_rows[-line_limit:]
320
+ if len(private_rows) >= line_limit:
321
+ return private_rows
322
+ legacy = _load_legacy_perf_history(line_limit - len(private_rows))
323
+ return [*legacy, *private_rows][-line_limit:]
324
+
325
+
326
+ def format_duration_ns(value: Any) -> str:
327
+ try:
328
+ amount = float(value or 0)
329
+ except (TypeError, ValueError):
330
+ return "?"
331
+ if amount <= 0:
332
+ return "0 ms"
333
+ return f"{amount / 1_000_000:.1f} ms"
334
+
335
+
336
+ def show_perf_summary() -> None:
337
+ recent = load_perf_history(limit=24)
338
+ chats = [item for item in recent if item.get("event", "chat") == "chat"]
339
+ tool_events = [item for item in recent if item.get("event") in {"tool", "compaction"}]
340
+ rs = _runtime_status()
341
+ latest = rs.get("last_metrics") or (chats[-1] if chats else None)
342
+ if not latest:
343
+ if not tool_events:
344
+ show_info("No performance metrics captured yet. Run a chat turn first.")
345
+ return
346
+ show_info("No chat timing metrics captured yet. Showing runtime events only.")
347
+ else:
348
+ console.print("[bold primary]Latest latency[/]")
349
+ console.print(
350
+ f" total {format_duration_ns(latest.get('total_duration'))}"
351
+ f" | load {format_duration_ns(latest.get('load_duration'))}"
352
+ f" | prompt {format_duration_ns(latest.get('prompt_eval_duration'))}"
353
+ f" | eval {format_duration_ns(latest.get('eval_duration'))}"
354
+ )
355
+ console.print(
356
+ f" tokens in {latest.get('prompt_eval_count', '?')}"
357
+ f" | out {latest.get('eval_count', '?')}"
358
+ f" | keep_alive {latest.get('keep_alive', '?')}"
359
+ )
360
+ if chats:
361
+ console.print("[bold primary]Recent[/]")
362
+ for item in chats[-5:]:
363
+ model_label = str(item.get("model", "?")).encode("ascii", "replace").decode("ascii")
364
+ console.print(
365
+ Text(
366
+ f" {model_label}: "
367
+ f"total {format_duration_ns(item.get('total_duration'))}, "
368
+ f"load {format_duration_ns(item.get('load_duration'))}, "
369
+ f"prompt {format_duration_ns(item.get('prompt_eval_duration'))}, "
370
+ f"eval {format_duration_ns(item.get('eval_duration'))}"
371
+ )
372
+ )
373
+ if tool_events:
374
+ console.print("[bold primary]Runtime events[/]")
375
+ for item in tool_events[-5:]:
376
+ if item.get("event") == "tool":
377
+ console.print(
378
+ Text(
379
+ f" tool {item.get('tool', '?')}: "
380
+ f"{item.get('status', '?')} in {item.get('duration_ms', '?')} ms"
381
+ )
382
+ )
383
+ else:
384
+ console.print(
385
+ Text(
386
+ f" compaction: {item.get('duration_ms', '?')} ms, "
387
+ f"{item.get('messages_compacted', '?')} messages"
388
+ )
389
+ )
algo_cli/plugins.py ADDED
@@ -0,0 +1,245 @@
1
+ """Plugin system for Algo CLI.
2
+
3
+ Discovers and loads plugins from ~/.algo_cli/plugins/. Each plugin is a
4
+ directory containing:
5
+ - plugin.json (required manifest with metadata)
6
+ - __init__.py (Python module with optional entry points)
7
+
8
+ Entry points a plugin may export:
9
+ - register_actions() -> tuple[ActionSpec, ...]
10
+ - register_slash_commands() -> list[tuple[str, str]]
11
+ - register_tools() -> dict[str, Callable]
12
+ - on_load(config) -> None
13
+
14
+ The plugin manager is defensive: a broken plugin never crashes the CLI.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import importlib.util
19
+ import json
20
+ import logging
21
+ from dataclasses import asdict, dataclass, field
22
+ from pathlib import Path
23
+ from typing import Any, Callable
24
+
25
+ from .config import CONFIG_DIR
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ PLUGINS_DIR = CONFIG_DIR / "plugins"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class PluginManifest:
34
+ """Metadata for a discovered plugin."""
35
+ name: str
36
+ version: str
37
+ description: str
38
+ author: str = ""
39
+ entry_points: tuple[str, ...] = ()
40
+ enabled: bool = True
41
+
42
+ def as_dict(self) -> dict[str, Any]:
43
+ return asdict(self)
44
+
45
+
46
+ @dataclass
47
+ class LoadedPlugin:
48
+ """A plugin that has been discovered and optionally loaded."""
49
+ manifest: PluginManifest
50
+ module: Any = None
51
+ path: Path = field(default_factory=Path)
52
+ load_error: str = ""
53
+ loaded: bool = False
54
+
55
+ @property
56
+ def name(self) -> str:
57
+ return self.manifest.name
58
+
59
+ def as_dict(self) -> dict[str, Any]:
60
+ logical_path = f"plugins/{self.path.name}" if self.path.name else "plugins"
61
+ return {
62
+ "name": self.manifest.name,
63
+ "version": self.manifest.version,
64
+ "description": self.manifest.description,
65
+ "author": self.manifest.author,
66
+ "enabled": self.manifest.enabled,
67
+ "loaded": self.loaded,
68
+ "load_error": self.load_error,
69
+ "path": logical_path,
70
+ "entry_points": list(self.manifest.entry_points),
71
+ }
72
+
73
+
74
+ def _parse_manifest(manifest_path: Path) -> PluginManifest | None:
75
+ """Parse a plugin.json manifest file. Returns None on failure."""
76
+ try:
77
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
78
+ except (json.JSONDecodeError, OSError) as exc:
79
+ logger.warning("Failed to parse plugin manifest %s: %s", manifest_path, exc)
80
+ return None
81
+
82
+ name = data.get("name", "")
83
+ version = data.get("version", "0.0.0")
84
+ description = data.get("description", "")
85
+ if not name:
86
+ logger.warning("Plugin manifest at %s has no 'name' field", manifest_path)
87
+ return None
88
+
89
+ entry_points = tuple(data.get("entry_points", []))
90
+ enabled = data.get("enabled", True)
91
+ author = data.get("author", "")
92
+
93
+ return PluginManifest(
94
+ name=name,
95
+ version=version,
96
+ description=description,
97
+ author=author,
98
+ entry_points=entry_points,
99
+ enabled=enabled,
100
+ )
101
+
102
+
103
+ def discover_plugins(plugins_dir: Path | None = None) -> list[PluginManifest]:
104
+ """Discover all plugin manifests in the plugins directory.
105
+
106
+ Returns a list of parsed manifests. Does not load plugin code.
107
+ """
108
+ root = plugins_dir or PLUGINS_DIR
109
+ if not root.is_dir():
110
+ return []
111
+
112
+ manifests: list[PluginManifest] = []
113
+ for entry in sorted(root.iterdir()):
114
+ if not entry.is_dir():
115
+ continue
116
+ manifest_path = entry / "plugin.json"
117
+ if not manifest_path.exists():
118
+ continue
119
+ parsed = _parse_manifest(manifest_path)
120
+ if parsed is not None:
121
+ manifests.append(parsed)
122
+ return manifests
123
+
124
+
125
+ def load_plugin(manifest: PluginManifest, plugins_dir: Path | None = None) -> LoadedPlugin:
126
+ """Load a single plugin module by its manifest.
127
+
128
+ Returns a LoadedPlugin. If loading fails, loaded=False and load_error is set.
129
+ """
130
+ root = plugins_dir or PLUGINS_DIR
131
+ plugin_path = root / manifest.name
132
+ init_file = plugin_path / "__init__.py"
133
+
134
+ loaded = LoadedPlugin(manifest=manifest, path=plugin_path)
135
+
136
+ if not manifest.enabled:
137
+ loaded.load_error = "Plugin is disabled in manifest"
138
+ return loaded
139
+
140
+ if not init_file.exists():
141
+ loaded.load_error = f"No __init__.py found at {init_file}"
142
+ return loaded
143
+
144
+ try:
145
+ spec = importlib.util.spec_from_file_location(
146
+ f"algo_cli_plugin_{manifest.name}",
147
+ init_file,
148
+ )
149
+ if spec is None or spec.loader is None:
150
+ loaded.load_error = "Could not create import spec"
151
+ return loaded
152
+
153
+ module = importlib.util.module_from_spec(spec)
154
+ spec.loader.exec_module(module)
155
+ loaded.module = module
156
+ loaded.loaded = True
157
+ except Exception as exc:
158
+ loaded.load_error = str(exc)
159
+ logger.warning("Failed to load plugin '%s': %s", manifest.name, exc)
160
+
161
+ return loaded
162
+
163
+
164
+ def load_all_plugins(plugins_dir: Path | None = None) -> list[LoadedPlugin]:
165
+ """Discover and load all enabled plugins.
166
+
167
+ Returns a list of LoadedPlugin objects (both successful and failed).
168
+ """
169
+ manifests = discover_plugins(plugins_dir)
170
+ return [load_plugin(m, plugins_dir) for m in manifests]
171
+
172
+
173
+ def collect_plugin_actions(plugins: list[LoadedPlugin]) -> list[Any]:
174
+ """Call register_actions() on each loaded plugin and collect results."""
175
+ actions: list[Any] = []
176
+ for plugin in plugins:
177
+ if not plugin.loaded or plugin.module is None:
178
+ continue
179
+ register_fn = getattr(plugin.module, "register_actions", None)
180
+ if register_fn is None or not callable(register_fn):
181
+ continue
182
+ try:
183
+ result = register_fn()
184
+ if isinstance(result, (list, tuple)):
185
+ actions.extend(result)
186
+ except Exception as exc:
187
+ logger.warning("Plugin '%s' register_actions() failed: %s", plugin.name, exc)
188
+ return actions
189
+
190
+
191
+ def collect_plugin_slash_commands(plugins: list[LoadedPlugin]) -> list[tuple[str, str]]:
192
+ """Call register_slash_commands() on each loaded plugin and collect results."""
193
+ commands: list[tuple[str, str]] = []
194
+ for plugin in plugins:
195
+ if not plugin.loaded or plugin.module is None:
196
+ continue
197
+ register_fn = getattr(plugin.module, "register_slash_commands", None)
198
+ if register_fn is None or not callable(register_fn):
199
+ continue
200
+ try:
201
+ result = register_fn()
202
+ if isinstance(result, (list, tuple)):
203
+ commands.extend(result)
204
+ except Exception as exc:
205
+ logger.warning("Plugin '%s' register_slash_commands() failed: %s", plugin.name, exc)
206
+ return commands
207
+
208
+
209
+ def collect_plugin_tools(plugins: list[LoadedPlugin]) -> dict[str, Callable]:
210
+ """Call register_tools() on each loaded plugin and collect results into a dict."""
211
+ tools: dict[str, Callable] = {}
212
+ for plugin in plugins:
213
+ if not plugin.loaded or plugin.module is None:
214
+ continue
215
+ register_fn = getattr(plugin.module, "register_tools", None)
216
+ if register_fn is None or not callable(register_fn):
217
+ continue
218
+ try:
219
+ result = register_fn()
220
+ if isinstance(result, dict):
221
+ tools.update(result)
222
+ except Exception as exc:
223
+ logger.warning("Plugin '%s' register_tools() failed: %s", plugin.name, exc)
224
+ return tools
225
+
226
+
227
+ def plugin_status(plugins_dir: Path | None = None) -> list[dict[str, Any]]:
228
+ """Return manifest status without importing or executing plugin code."""
229
+ root = plugins_dir or PLUGINS_DIR
230
+ return [
231
+ {
232
+ **manifest.as_dict(),
233
+ "loaded": False,
234
+ "load_error": "",
235
+ "path": f"plugins/{manifest.name}",
236
+ "state": "discovered" if manifest.enabled else "disabled",
237
+ }
238
+ for manifest in discover_plugins(root)
239
+ ]
240
+
241
+
242
+ def ensure_plugins_dir() -> Path:
243
+ """Create the plugins directory if it doesn't exist."""
244
+ PLUGINS_DIR.mkdir(parents=True, exist_ok=True)
245
+ return PLUGINS_DIR