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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|