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,236 @@
1
+ """Runtime QoS and named log helpers (J6 + J7).
2
+
3
+ Inspired by macOS launchd ``POSIXSpawnType`` and ``StandardErrorPath``:
4
+ classify tools by expected runtime posture and give each tool a stable log path.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ import threading
10
+ import time
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Any, Generic, TypeVar
15
+
16
+ from .config import CONFIG_DIR
17
+
18
+
19
+ class SpawnClass(str, Enum):
20
+ ADAPTIVE = "adaptive"
21
+ INTERACTIVE = "interactive"
22
+ BACKGROUND = "background"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class RuntimeHint:
27
+ tool_name: str
28
+ spawn_class: SpawnClass
29
+ log_path: str
30
+ log_suppression: bool
31
+ reason: str
32
+ estimated_cost: float
33
+
34
+ def to_dict(self) -> dict:
35
+ return {
36
+ "tool_name": self.tool_name,
37
+ "spawn_class": self.spawn_class.value,
38
+ "log_path": self.log_path,
39
+ "log_suppression": self.log_suppression,
40
+ "reason": self.reason,
41
+ "estimated_cost": self.estimated_cost,
42
+ }
43
+
44
+
45
+ T = TypeVar("T")
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class ScheduledJob(Generic[T]):
50
+ key: str
51
+ payload: T
52
+ spawn_class: SpawnClass
53
+ estimated_cost: float
54
+ enqueued_at: float
55
+ virtual_finish: float
56
+ sequence: int
57
+
58
+
59
+ class WeightedFairQueue(Generic[T]):
60
+ """Small deterministic weighted-fair queue with starvation-safe aging."""
61
+
62
+ DEFAULT_WEIGHTS = {
63
+ SpawnClass.INTERACTIVE: 4.0,
64
+ SpawnClass.ADAPTIVE: 2.0,
65
+ SpawnClass.BACKGROUND: 1.0,
66
+ }
67
+
68
+ def __init__(
69
+ self,
70
+ *,
71
+ weights: dict[SpawnClass, float] | None = None,
72
+ aging_rate: float = 0.05,
73
+ ) -> None:
74
+ configured = dict(self.DEFAULT_WEIGHTS)
75
+ configured.update(weights or {})
76
+ self.weights = {key: max(0.01, float(value)) for key, value in configured.items()}
77
+ self.aging_rate = max(0.0, float(aging_rate))
78
+ self._class_finish = {spawn: 0.0 for spawn in SpawnClass}
79
+ self._jobs: list[ScheduledJob[T]] = []
80
+ self._sequence = 0
81
+ self._lock = threading.RLock()
82
+
83
+ def enqueue(
84
+ self,
85
+ key: str,
86
+ payload: T,
87
+ spawn_class: SpawnClass,
88
+ *,
89
+ estimated_cost: float = 1.0,
90
+ now: float | None = None,
91
+ ) -> ScheduledJob[T]:
92
+ with self._lock:
93
+ if not self._jobs:
94
+ self._class_finish = {spawn: 0.0 for spawn in SpawnClass}
95
+ cost = max(0.01, float(estimated_cost))
96
+ finish = self._class_finish[spawn_class] + cost / self.weights[spawn_class]
97
+ self._class_finish[spawn_class] = finish
98
+ job = ScheduledJob(
99
+ key=str(key),
100
+ payload=payload,
101
+ spawn_class=spawn_class,
102
+ estimated_cost=cost,
103
+ enqueued_at=time.monotonic() if now is None else float(now),
104
+ virtual_finish=finish,
105
+ sequence=self._sequence,
106
+ )
107
+ self._sequence += 1
108
+ self._jobs.append(job)
109
+ return job
110
+
111
+ def pop(self, *, now: float | None = None) -> ScheduledJob[T] | None:
112
+ with self._lock:
113
+ if not self._jobs:
114
+ return None
115
+ current = time.monotonic() if now is None else float(now)
116
+
117
+ def priority(job: ScheduledJob[T]) -> tuple[float, int]:
118
+ waited = max(0.0, current - job.enqueued_at)
119
+ return job.virtual_finish - self.aging_rate * waited, job.sequence
120
+
121
+ selected = min(self._jobs, key=priority)
122
+ self._jobs.remove(selected)
123
+ return selected
124
+
125
+ def __len__(self) -> int:
126
+ with self._lock:
127
+ return len(self._jobs)
128
+
129
+
130
+ SENSITIVE_TOOL_NAMES = frozenset({
131
+ "credential_helpers_get",
132
+ "credential_helpers_store",
133
+ "x_account_post",
134
+ "x_account_reply",
135
+ "x_account_post_action",
136
+ })
137
+ BACKGROUND_TOOL_NAMES = frozenset({
138
+ "harness_refresh",
139
+ "reindex_knowledge_graph",
140
+ "model_pull",
141
+ })
142
+ INTERACTIVE_TOOL_NAMES = frozenset({
143
+ "session_command",
144
+ "session_slash",
145
+ "available_actions",
146
+ })
147
+
148
+ _LOW_COST_TOOL_NAMES = frozenset({
149
+ "read_file",
150
+ "list_directory",
151
+ "search_files",
152
+ "find_unique_anchor",
153
+ "git_status",
154
+ "available_actions",
155
+ })
156
+
157
+
158
+ def _safe_tool_name(tool_name: str) -> str:
159
+ return re.sub(r"[^A-Za-z0-9_.-]+", "_", tool_name or "tool").strip("_") or "tool"
160
+
161
+
162
+ def named_tool_log_path(tool_name: str, *, log_root: Path | str | None = None, suppress: bool = False) -> Path:
163
+ """Return a stable named log path for a tool, or /dev/null when explicitly suppressed."""
164
+ if suppress:
165
+ return Path("/dev/null")
166
+ root = Path(log_root).expanduser() if log_root is not None else CONFIG_DIR / "logs" / "tools"
167
+ safe = _safe_tool_name(tool_name)
168
+ stamp = time.strftime("%Y%m%d")
169
+ return root / safe / f"{stamp}.log"
170
+
171
+
172
+ def classify_tool_runtime(tool_name: str, args: dict[str, Any] | None = None) -> RuntimeHint:
173
+ """Classify a tool call into a launchd-style runtime posture."""
174
+ name = (tool_name or "").strip()
175
+ args = args or {}
176
+ suppress = name in SENSITIVE_TOOL_NAMES or bool(args.get("suppress_log"))
177
+ if name in BACKGROUND_TOOL_NAMES:
178
+ spawn = SpawnClass.BACKGROUND
179
+ reason = "known long-running/background maintenance tool"
180
+ estimated_cost = 8.0
181
+ elif name in INTERACTIVE_TOOL_NAMES:
182
+ spawn = SpawnClass.INTERACTIVE
183
+ reason = "session/user-facing command"
184
+ estimated_cost = 1.0
185
+ elif name == "run_shell":
186
+ command = str(args.get("command", "")).lower()
187
+ if any(token in command for token in ("pytest", "ruff", "mypy", "npm test", "cargo test")):
188
+ spawn = SpawnClass.ADAPTIVE
189
+ reason = "verification command; adaptive runtime"
190
+ estimated_cost = 4.0
191
+ else:
192
+ spawn = SpawnClass.INTERACTIVE
193
+ reason = "shell command may require user-facing output"
194
+ estimated_cost = 2.0
195
+ elif name in _LOW_COST_TOOL_NAMES:
196
+ spawn = SpawnClass.ADAPTIVE
197
+ reason = "bounded read-only operation"
198
+ estimated_cost = 1.0
199
+ else:
200
+ spawn = SpawnClass.BACKGROUND if suppress else SpawnClass.ADAPTIVE
201
+ reason = "default runtime classification"
202
+ estimated_cost = 2.0
203
+ log_path = named_tool_log_path(name, suppress=suppress)
204
+ return RuntimeHint(name, spawn, str(log_path), suppress, reason, estimated_cost)
205
+
206
+
207
+ def order_tool_batch_by_qos(calls: list[tuple[str, dict[str, Any]]]) -> list[int]:
208
+ """Return call indexes in weighted-fair dispatch order."""
209
+ scheduler: WeightedFairQueue[int] = WeightedFairQueue()
210
+ enqueued_at = time.monotonic()
211
+ for index, (name, args) in enumerate(calls):
212
+ hint = classify_tool_runtime(name, args)
213
+ scheduler.enqueue(
214
+ str(index),
215
+ index,
216
+ hint.spawn_class,
217
+ estimated_cost=hint.estimated_cost,
218
+ now=enqueued_at,
219
+ )
220
+ ordered: list[int] = []
221
+ while len(scheduler):
222
+ job = scheduler.pop(now=enqueued_at)
223
+ if job is not None:
224
+ ordered.append(job.payload)
225
+ return ordered
226
+
227
+
228
+ __all__ = [
229
+ "RuntimeHint",
230
+ "ScheduledJob",
231
+ "SpawnClass",
232
+ "WeightedFairQueue",
233
+ "classify_tool_runtime",
234
+ "named_tool_log_path",
235
+ "order_tool_batch_by_qos",
236
+ ]
@@ -0,0 +1,320 @@
1
+ """Ollama / xAI clients, local server startup, and harness-gateway lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ from contextlib import contextmanager
13
+ from pathlib import Path
14
+ from typing import Any
15
+ from urllib.error import URLError
16
+ from urllib.parse import urljoin
17
+ from urllib.request import Request, urlopen
18
+
19
+ from ollama import Client
20
+
21
+ from .config import Config, load_runtime_env
22
+ from . import harness
23
+ from . import model_info as _model_info_module
24
+ from . import xai_auth
25
+ from . import chatgpt_auth
26
+ from .display import show_error, show_info
27
+ from .model_routing import require_cloud_api_key, uses_ollama_cloud
28
+
29
+ LOCAL_STARTUP_TIMEOUT_SECONDS = 12
30
+ SERVER_READY_TTL_SECONDS = 5.0
31
+ GATEWAY_READY_TTL_SECONDS = 5.0
32
+
33
+ SERVER_READY_CACHE: dict[str, tuple[float, bool]] = {}
34
+ GATEWAY_PROCESS: subprocess.Popen[Any] | None = None
35
+ GATEWAY_READY_CACHE: dict[str, tuple[float, bool]] = {}
36
+ _TOOL_ENV_KEYS = ("OLLAMA_HOST", "ALGO_CLI_GATEWAY_URL", "OLLAMA_CLI_GATEWAY_URL")
37
+ _TOOL_ENV_CONDITION = threading.Condition(threading.RLock())
38
+ _TOOL_ENV_ACTIVE_VALUES: tuple[str, str, str] | None = None
39
+ _TOOL_ENV_ACTIVE_COUNT = 0
40
+ _TOOL_ENV_PREVIOUS: dict[str, str | None] = {}
41
+ _TOOL_ENV_THREAD_STATE = threading.local()
42
+
43
+
44
+ def create_client(cfg: Config) -> Any:
45
+ load_runtime_env(override=True)
46
+ if _model_info_module.is_xai_model(cfg.model):
47
+ from . import xai_client
48
+
49
+ return xai_client.active_xai_client()
50
+ if _model_info_module.is_chatgpt_model(cfg.model):
51
+ from . import chatgpt_client
52
+
53
+ return chatgpt_client.active_chatgpt_client()
54
+ timeout = max(1.0, float(cfg.chat_stream_timeout_seconds))
55
+ if uses_ollama_cloud(cfg):
56
+ require_cloud_api_key(cfg)
57
+ api_key = os.environ.get("OLLAMA_API_KEY", "").strip()
58
+ headers = {"Authorization": f"Bearer {api_key}"}
59
+ return Client(host="https://ollama.com", headers=headers, timeout=timeout)
60
+ return Client(host=cfg.host, timeout=timeout)
61
+
62
+
63
+ def client_for_model(model: str, cfg: Config, active_client: Any) -> Any:
64
+ if not model or model == cfg.model:
65
+ return active_client
66
+ load_runtime_env(override=True)
67
+ if _model_info_module.is_xai_model(model):
68
+ if not xai_auth.get_valid_token():
69
+ show_info(f"Agent block model {model} needs xAI OAuth; falling back to {cfg.model}.")
70
+ return active_client
71
+ from . import xai_client
72
+
73
+ return xai_client.active_xai_client()
74
+ if _model_info_module.is_chatgpt_model(model):
75
+ if not chatgpt_auth.get_valid_token():
76
+ show_info(f"Agent block model {model} needs ChatGPT OAuth; falling back to {cfg.model}.")
77
+ return active_client
78
+ from . import chatgpt_client
79
+
80
+ return chatgpt_client.active_chatgpt_client()
81
+ timeout = max(1.0, float(cfg.chat_stream_timeout_seconds))
82
+ if uses_ollama_cloud(cfg):
83
+ require_cloud_api_key(cfg)
84
+ api_key = os.environ.get("OLLAMA_API_KEY", "").strip()
85
+ headers = {"Authorization": f"Bearer {api_key}"}
86
+ return Client(host="https://ollama.com", headers=headers, timeout=timeout)
87
+ return Client(host=cfg.host, timeout=timeout)
88
+
89
+
90
+ def apply_tool_runtime_env(cfg: Config) -> None:
91
+ os.environ["OLLAMA_HOST"] = cfg.host
92
+ url = gateway_url()
93
+ os.environ["ALGO_CLI_GATEWAY_URL"] = url
94
+ os.environ["OLLAMA_CLI_GATEWAY_URL"] = url
95
+ if cfg.cloud:
96
+ load_runtime_env(override=True)
97
+
98
+
99
+ @contextmanager
100
+ def scoped_tool_runtime_env(cfg: Config):
101
+ """Lease process-global tool environment safely across agent threads.
102
+
103
+ Equal configurations share a reference-counted lease and remain concurrent.
104
+ Different configurations serialize because ``os.environ`` cannot represent
105
+ both values at once.
106
+ """
107
+ global _TOOL_ENV_ACTIVE_VALUES, _TOOL_ENV_ACTIVE_COUNT, _TOOL_ENV_PREVIOUS
108
+
109
+ url = gateway_url()
110
+ desired = (str(cfg.host), url, url)
111
+ thread_depth = int(getattr(_TOOL_ENV_THREAD_STATE, "depth", 0))
112
+ with _TOOL_ENV_CONDITION:
113
+ if thread_depth and _TOOL_ENV_ACTIVE_VALUES != desired:
114
+ raise RuntimeError("nested tool runtime environments must use the same configuration")
115
+ while _TOOL_ENV_ACTIVE_COUNT and _TOOL_ENV_ACTIVE_VALUES != desired:
116
+ _TOOL_ENV_CONDITION.wait()
117
+ if _TOOL_ENV_ACTIVE_COUNT == 0:
118
+ _TOOL_ENV_PREVIOUS = {key: os.environ.get(key) for key in _TOOL_ENV_KEYS}
119
+ try:
120
+ apply_tool_runtime_env(cfg)
121
+ except BaseException:
122
+ for key, value in _TOOL_ENV_PREVIOUS.items():
123
+ if value is None:
124
+ os.environ.pop(key, None)
125
+ else:
126
+ os.environ[key] = value
127
+ _TOOL_ENV_PREVIOUS = {}
128
+ _TOOL_ENV_CONDITION.notify_all()
129
+ raise
130
+ _TOOL_ENV_ACTIVE_VALUES = desired
131
+ _TOOL_ENV_ACTIVE_COUNT += 1
132
+ _TOOL_ENV_THREAD_STATE.depth = thread_depth + 1
133
+ try:
134
+ yield
135
+ finally:
136
+ with _TOOL_ENV_CONDITION:
137
+ _TOOL_ENV_THREAD_STATE.depth = max(0, int(getattr(_TOOL_ENV_THREAD_STATE, "depth", 1)) - 1)
138
+ _TOOL_ENV_ACTIVE_COUNT -= 1
139
+ if _TOOL_ENV_ACTIVE_COUNT == 0:
140
+ for key, value in _TOOL_ENV_PREVIOUS.items():
141
+ if value is None:
142
+ os.environ.pop(key, None)
143
+ else:
144
+ os.environ[key] = value
145
+ _TOOL_ENV_PREVIOUS = {}
146
+ _TOOL_ENV_ACTIVE_VALUES = None
147
+ _TOOL_ENV_CONDITION.notify_all()
148
+
149
+
150
+ def host_is_local(host: str) -> bool:
151
+ normalized = host.lower()
152
+ return "localhost" in normalized or "127.0.0.1" in normalized or "[::1]" in normalized
153
+
154
+
155
+ def ollama_server_ready(host: str) -> bool:
156
+ cached = SERVER_READY_CACHE.get(host)
157
+ now = time.time()
158
+ if cached and now - cached[0] <= SERVER_READY_TTL_SECONDS:
159
+ return cached[1]
160
+ try:
161
+ request = Request(urljoin(host.rstrip("/") + "/", "api/version"), method="GET")
162
+ with urlopen(request, timeout=1.5) as response:
163
+ ready = 200 <= response.status < 500
164
+ except (OSError, URLError, ValueError):
165
+ ready = False
166
+ SERVER_READY_CACHE[host] = (now, ready)
167
+ return ready
168
+
169
+
170
+ def start_ollama_server(cfg: Config) -> bool:
171
+ if (
172
+ uses_ollama_cloud(cfg)
173
+ or _model_info_module.is_xai_model(cfg.model)
174
+ or _model_info_module.is_chatgpt_model(cfg.model)
175
+ or not host_is_local(cfg.host)
176
+ ):
177
+ return True
178
+ return start_local_ollama_host(cfg.host)
179
+
180
+
181
+ def start_local_ollama_host(host: str) -> bool:
182
+ if not host_is_local(host):
183
+ return True
184
+ if ollama_server_ready(host):
185
+ return True
186
+
187
+ show_info("Local Ollama server is not responding; starting `ollama serve`.")
188
+ try:
189
+ kwargs: dict[str, Any] = {
190
+ "stdout": subprocess.DEVNULL,
191
+ "stderr": subprocess.DEVNULL,
192
+ "stdin": subprocess.DEVNULL,
193
+ "start_new_session": True,
194
+ }
195
+ if sys.platform.startswith("win"):
196
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS
197
+ subprocess.Popen(["ollama", "serve"], **kwargs)
198
+ SERVER_READY_CACHE.pop(host, None)
199
+ except FileNotFoundError:
200
+ show_error("Could not find `ollama` on PATH. Install Ollama or add it to PATH.")
201
+ return False
202
+ except Exception as exc:
203
+ show_error(f"Could not start Ollama server: {exc}")
204
+ return False
205
+
206
+ deadline = time.time() + LOCAL_STARTUP_TIMEOUT_SECONDS
207
+ while time.time() < deadline:
208
+ if ollama_server_ready(host):
209
+ show_info("Local Ollama server is running.")
210
+ return True
211
+ time.sleep(0.5)
212
+
213
+ show_error(f"Ollama server did not become ready at {host}.")
214
+ return False
215
+
216
+
217
+ def gateway_url() -> str:
218
+ return (
219
+ os.environ.get("ALGO_CLI_GATEWAY_URL")
220
+ or os.environ.get("OLLAMA_CLI_GATEWAY_URL")
221
+ or "http://127.0.0.1:8765"
222
+ ).rstrip("/")
223
+
224
+
225
+ def gateway_ready(url: str | None = None) -> bool:
226
+ url = (url or gateway_url()).rstrip("/")
227
+ cached = GATEWAY_READY_CACHE.get(url)
228
+ now = time.time()
229
+ if cached and now - cached[0] <= GATEWAY_READY_TTL_SECONDS:
230
+ return cached[1]
231
+ try:
232
+ request = Request(url + "/healthz", method="GET")
233
+ with urlopen(request, timeout=1.0) as response:
234
+ ready = 200 <= response.status < 500
235
+ except (OSError, URLError, ValueError):
236
+ ready = False
237
+ GATEWAY_READY_CACHE[url] = (now, ready)
238
+ return ready
239
+
240
+
241
+ def gateway_source_dir() -> Path:
242
+ return Path(__file__).resolve().parents[1] / "harness-gateway"
243
+
244
+
245
+ def gateway_command() -> tuple[list[str], Path] | None:
246
+ configured = os.environ.get("ALGO_CLI_GATEWAY_BIN") or os.environ.get("OLLAMA_CLI_GATEWAY_BIN")
247
+ if configured:
248
+ path = Path(configured).expanduser()
249
+ if path.exists():
250
+ return [str(path)], path.parent
251
+ source_dir = gateway_source_dir()
252
+ exe_name = "harness-gateway.exe" if sys.platform.startswith("win") else "harness-gateway"
253
+ built = source_dir / exe_name
254
+ if built.exists():
255
+ return [str(built)], source_dir
256
+ go = shutil.which("go") or (r"C:\Program Files\Go\bin\go.exe" if Path(r"C:\Program Files\Go\bin\go.exe").exists() else "")
257
+ if go and source_dir.exists():
258
+ return [go, "run", "."], source_dir
259
+ return None
260
+
261
+
262
+ def start_supplemental_gateway(cfg: Config) -> bool:
263
+ global GATEWAY_PROCESS
264
+ if not uses_ollama_cloud(cfg):
265
+ return True
266
+ if not start_local_ollama_host(cfg.host):
267
+ return False
268
+ url = gateway_url()
269
+ if gateway_ready(url):
270
+ return True
271
+ command_info = gateway_command()
272
+ if command_info is None:
273
+ show_info(
274
+ "Supplemental gateway unavailable. Build `harness-gateway` or install Go "
275
+ "for local embedding/OCR helpers in cloud mode."
276
+ )
277
+ return False
278
+ command, cwd = command_info
279
+ startup_timeout = 45 if len(command) >= 2 and Path(command[0]).name.lower().startswith("go") and command[1] == "run" else 20
280
+ addr = url.removeprefix("http://").removeprefix("https://")
281
+ args = command + ["-addr", addr, "-index", str(harness.INDEX_PATH), "-ollama", cfg.host]
282
+ try:
283
+ kwargs: dict[str, Any] = {
284
+ "cwd": str(cwd),
285
+ "stdout": subprocess.DEVNULL,
286
+ "stderr": subprocess.DEVNULL,
287
+ "stdin": subprocess.DEVNULL,
288
+ }
289
+ if sys.platform.startswith("win"):
290
+ kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
291
+ GATEWAY_PROCESS = subprocess.Popen(args, **kwargs)
292
+ GATEWAY_READY_CACHE.pop(url, None)
293
+ except Exception as exc:
294
+ show_error(f"Could not start supplemental gateway: {exc}")
295
+ return False
296
+ deadline = time.time() + startup_timeout
297
+ while time.time() < deadline:
298
+ if gateway_ready(url):
299
+ show_info(f"Supplemental gateway is running at {url}.")
300
+ return True
301
+ time.sleep(0.5)
302
+ show_error(f"Supplemental gateway did not become ready at {url}.")
303
+ return False
304
+
305
+
306
+ def shutdown_supplemental_gateway() -> None:
307
+ global GATEWAY_PROCESS
308
+ if GATEWAY_PROCESS and GATEWAY_PROCESS.poll() is None:
309
+ try:
310
+ GATEWAY_PROCESS.terminate()
311
+ GATEWAY_PROCESS.wait(timeout=3)
312
+ except Exception:
313
+ try:
314
+ GATEWAY_PROCESS.kill()
315
+ except Exception:
316
+ pass
317
+ GATEWAY_PROCESS = None
318
+
319
+
320
+ atexit.register(shutdown_supplemental_gateway)
@@ -0,0 +1,95 @@
1
+ """Model-callable session slash commands (same behavior as TUI /read, /ls, /cd)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from . import tools as tools_module
9
+ from .workspace_resolver import parse_path_arg
10
+
11
+ MODEL_SLASH_COMMANDS: tuple[tuple[str, str], ...] = (
12
+ ("/read", "Read a file relative to session cwd (deterministic). Args: /read PATH"),
13
+ ("/ls", "List directory under cwd. Args: /ls or /ls SUBPATH"),
14
+ ("/cd", "Change session cwd (saved). Args: /cd PATH"),
15
+ ("/cwd", "Show current session cwd. No args."),
16
+ )
17
+
18
+ _ALLOWED = frozenset(cmd for cmd, _ in MODEL_SLASH_COMMANDS)
19
+
20
+
21
+ def catalog_for_prompt() -> str:
22
+ lines = [
23
+ "Slash commands are session controls, not normal prose. If you merely write '/command' in a reply, nothing runs.",
24
+ "Choose the right path:",
25
+ " 1. User typed a slash command: the CLI handles it before you see it; do not re-run it unless asked.",
26
+ " 2. Need deterministic cwd file navigation/read: call session_slash with the full line:",
27
+ *[f" - {cmd} — {desc}" for cmd, desc in MODEL_SLASH_COMMANDS],
28
+ " 3. Need to change/check Algo CLI session state (/status, /mode, /context, /harness refresh, /agent, etc.): call session_command with the full line. Read-only/status commands run without approval; state-changing commands may require approval.",
29
+ " 4. Need actual work: prefer model-callable tools (read_file/write_file/search_files/run_shell/web_search/etc.) over slash commands.",
30
+ "When to use common slash commands:",
31
+ " - /status or /info: check active model, cwd, context, and toggles before giving configuration advice.",
32
+ " - /mode execute|explore|publish: switch operating posture only when the user's task clearly needs it or they ask.",
33
+ " - /context status|rebuild|clear: inspect or repair compressed context when context quality/length is the issue.",
34
+ " - /reason status|guide: inspect the active reasoning posture or show mode guidance before changing it.",
35
+ " - /reason react|reflexion|tot|got|mcts|qcr|neuro_symbolic: change reasoning posture for complex work only; do not switch modes for routine reads/edits.",
36
+ " Use react for normal tool loops, reflexion after failed/partial attempts, tot/got/mcts for ambiguous multi-path planning, qcr for comparing candidate solutions, and neuro_symbolic for verification-heavy logic/code claims.",
37
+ " - /harness refresh: refresh indexed skills/wiki/memory after local harness files changed.",
38
+ " - /intel query TERM: inspect the local repository intelligence graph before code/navigation decisions.",
39
+ " Use /intel status to check availability; /intel reindex requires approval because it writes the graph index.",
40
+ " - Google Workspace commands are read-only and auth-gated; call available_actions(topic='google') before using /google-login, /google-callback, /google-status, or /google subcommands.",
41
+ " - /route TASK: preview the recommended Agent Blocks route before a complex task.",
42
+ " - /agent [--pipeline NAME] TASK: run a traceable multi-block pipeline for larger implementation/research/review work.",
43
+ " - /agent team [--roles ROLE,ROLE[,ROLE,ROLE]] TASK: delegate independent analysis to 2-4 read-only child threads, then integrate once with normal write and verification gates.",
44
+ " - /agent threads or /agent show THREAD: inspect run history; /agent resume THREAD [TASK] continues it and /agent fork THREAD TASK branches it.",
45
+ " Delegate only when work has genuinely independent angles. Keep mutations in the integration pipeline; never ask child threads to edit the same workspace.",
46
+ " - /kernel check [NAME]: verify kernel imports, slash routes, and active action contracts without executing workloads.",
47
+ "Do not use slash commands for file edits; use write_file. Do not use slash commands for build/test; use run_shell.",
48
+ "For the full command/tool catalog and focused examples, call available_actions(topic='slash').",
49
+ ]
50
+ return "\n".join(lines)
51
+
52
+
53
+ def execute(command_line: str, cfg: Any, *, max_read_chars: int | None = None) -> str:
54
+ """Run one whitelisted slash command against cfg.cwd."""
55
+ stripped = (command_line or "").strip()
56
+ if not stripped.startswith("/"):
57
+ stripped = f"/{stripped}"
58
+ if not stripped:
59
+ return "Error: empty command. Allowed: /read, /ls, /cd, /cwd"
60
+
61
+ parts = stripped.split(None, 1)
62
+ command = parts[0].lower()
63
+ remainder = stripped[len(parts[0]):].strip() if len(parts) > 1 else ""
64
+
65
+ if command not in _ALLOWED:
66
+ allowed = ", ".join(sorted(_ALLOWED))
67
+ return f"Error: {command} is not model-invokable. Allowed: {allowed}"
68
+
69
+ if command == "/cwd" or (command == "/cd" and not remainder):
70
+ return f"cwd: {getattr(cfg, 'cwd', '.')}"
71
+
72
+ if command == "/cd":
73
+ target = parse_path_arg(remainder)
74
+ path = Path(target).expanduser()
75
+ if not path.is_absolute():
76
+ path = Path(getattr(cfg, "cwd", ".")) / path
77
+ if path.exists() and path.is_dir():
78
+ cfg.cwd = str(path.resolve())
79
+ if hasattr(cfg, "save"):
80
+ cfg.save()
81
+ return f"cwd: {cfg.cwd}"
82
+ return f"Error: not a directory: {path}"
83
+
84
+ if command == "/ls":
85
+ rel = parse_path_arg(remainder) or "."
86
+ return tools_module.list_directory(rel, cwd=getattr(cfg, "cwd", None), limit=40)
87
+
88
+ if command == "/read":
89
+ rel = parse_path_arg(remainder)
90
+ if not rel:
91
+ return "Error: usage: /read PATH"
92
+ limit = max_read_chars if max_read_chars is not None else tools_module.MAX_READ_CHARS
93
+ return tools_module.read_file(rel, cwd=getattr(cfg, "cwd", None), max_chars=limit)
94
+
95
+ return f"Error: unhandled command {command}"