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,160 @@
1
+ """Model-aware runtime profiles.
2
+
3
+ The harness knows each model's parameter size and provider but historically
4
+ applied one static set of knobs (num_ctx, temperature, reflection cadence) to
5
+ every model. A 4B local model and a 671B cloud model have very different needs:
6
+ small models want a tighter window, cooler sampling, and more frequent
7
+ reflection; large/cloud models can take a wider window and lighter supervision.
8
+
9
+ This module derives a :class:`ModelProfile` from model metadata. It only fills
10
+ in values the user has NOT explicitly changed from the Config defaults, so an
11
+ explicit ``/ctx``, ``/temp``, or ``/thinkevery`` always wins.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import Any
18
+
19
+ from . import model_info as _model_info_module
20
+
21
+ # Config-default sentinels. A field still equal to its default is treated as
22
+ # "untouched" and therefore eligible for adaptation. Kept in sync with
23
+ # config.Config; a mismatch only means adaptation is slightly more conservative.
24
+ DEFAULT_NUM_CTX = 8192
25
+ DEFAULT_TEMPERATURE = 0.4
26
+ DEFAULT_TOOL_THINK_EVERY = 10
27
+
28
+ # Size bands in billions of parameters.
29
+ SMALL_MAX_B = 9.0 # <=9B -> small
30
+ MEDIUM_MAX_B = 32.0 # <=32B -> medium; above -> large
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ModelProfile:
35
+ size_class: str # "small" | "medium" | "large" | "unknown"
36
+ provider: str # "local" | "cloud" | "xai" | "chatgpt"
37
+ num_ctx: int
38
+ temperature: float
39
+ tool_think_every: int
40
+ note: str # short human-readable rationale
41
+
42
+
43
+ def _size_class(size_b: float | None) -> str:
44
+ if size_b is None:
45
+ return "unknown"
46
+ if size_b <= SMALL_MAX_B:
47
+ return "small"
48
+ if size_b <= MEDIUM_MAX_B:
49
+ return "medium"
50
+ return "large"
51
+
52
+
53
+ def _provider(cfg: Any) -> str:
54
+ model = getattr(cfg, "model", "")
55
+ if _model_info_module.is_xai_model(model):
56
+ return "xai"
57
+ if _model_info_module.is_chatgpt_model(model):
58
+ return "chatgpt"
59
+ return "cloud" if getattr(cfg, "cloud", False) else "local"
60
+
61
+
62
+ def recommend_profile(cfg: Any, model_info: dict[str, Any] | None) -> ModelProfile:
63
+ """Compute a recommended profile for the active model.
64
+
65
+ The recommendation is bounded by the model's real native context window
66
+ when known, so we never recommend a window the model cannot serve.
67
+ """
68
+ info = model_info or {}
69
+ size_b = _model_info_module.parameter_size_billions(info)
70
+ size_class = _size_class(size_b)
71
+ provider = _provider(cfg)
72
+ native_ctx = _model_info_module.get_context_length(info)
73
+
74
+ # Baseline recommendations by size class. Native context is a ceiling, not
75
+ # a default allocation: requesting a 128K-1M window for a short task wastes
76
+ # KV-cache memory and can sharply increase local prefill latency. Explicit
77
+ # /ctx user overrides still opt into wider windows in effective_params().
78
+ if size_class == "small":
79
+ num_ctx, temperature, think_every = 8192, 0.3, 6
80
+ note = "small model: tight fallback window, cooler sampling, frequent reflection"
81
+ elif size_class == "medium":
82
+ num_ctx, temperature, think_every = 16384, 0.4, 10
83
+ note = "medium model: standard fallback window and supervision"
84
+ elif size_class == "large":
85
+ num_ctx, temperature, think_every = 32768, 0.5, 14
86
+ note = "large model: wider fallback window, lighter supervision"
87
+ else: # unknown
88
+ # Remote models often report no size; assume capable but be moderate.
89
+ if provider in {"cloud", "xai", "chatgpt"}:
90
+ num_ctx, temperature, think_every = 32768, 0.4, 12
91
+ note = "unknown-size remote model: moderate-wide fallback window"
92
+ else:
93
+ num_ctx, temperature, think_every = DEFAULT_NUM_CTX, DEFAULT_TEMPERATURE, DEFAULT_TOOL_THINK_EVERY
94
+ note = "unknown model: conservative fallback defaults"
95
+
96
+ if isinstance(native_ctx, int) and native_ctx > 0:
97
+ if provider == "local":
98
+ num_ctx = min(num_ctx, native_ctx)
99
+ note = f"{note}; local allocation capped by native context"
100
+ else:
101
+ num_ctx = native_ctx
102
+ note = f"{note}; remote native context"
103
+
104
+ return ModelProfile(
105
+ size_class=size_class,
106
+ provider=provider,
107
+ num_ctx=num_ctx,
108
+ temperature=temperature,
109
+ tool_think_every=think_every,
110
+ note=note,
111
+ )
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class EffectiveParams:
116
+ """The values to actually use this turn, after honoring user overrides."""
117
+
118
+ num_ctx: int
119
+ temperature: float
120
+ tool_think_every: int
121
+ adapted_fields: tuple[str, ...]
122
+
123
+
124
+ def effective_params(cfg: Any, model_info: dict[str, Any] | None) -> EffectiveParams:
125
+ """Resolve per-turn params: user overrides win, else the model profile.
126
+
127
+ A Config field still equal to its default is considered untouched and is
128
+ replaced by the profile recommendation. Any field the user changed via
129
+ ``/ctx``, ``/temp``, or ``/thinkevery`` is preserved exactly.
130
+ """
131
+ profile = recommend_profile(cfg, model_info)
132
+ adapted: list[str] = []
133
+
134
+ if int(getattr(cfg, "num_ctx", DEFAULT_NUM_CTX)) == DEFAULT_NUM_CTX:
135
+ num_ctx = profile.num_ctx
136
+ if num_ctx != DEFAULT_NUM_CTX:
137
+ adapted.append("num_ctx")
138
+ else:
139
+ num_ctx = int(cfg.num_ctx)
140
+
141
+ if float(getattr(cfg, "temperature", DEFAULT_TEMPERATURE)) == DEFAULT_TEMPERATURE:
142
+ temperature = profile.temperature
143
+ if temperature != DEFAULT_TEMPERATURE:
144
+ adapted.append("temperature")
145
+ else:
146
+ temperature = float(cfg.temperature)
147
+
148
+ if int(getattr(cfg, "tool_think_every", DEFAULT_TOOL_THINK_EVERY)) == DEFAULT_TOOL_THINK_EVERY:
149
+ think_every = profile.tool_think_every
150
+ if think_every != DEFAULT_TOOL_THINK_EVERY:
151
+ adapted.append("tool_think_every")
152
+ else:
153
+ think_every = int(cfg.tool_think_every)
154
+
155
+ return EffectiveParams(
156
+ num_ctx=max(1, num_ctx),
157
+ temperature=temperature,
158
+ tool_think_every=max(1, think_every),
159
+ adapted_fields=tuple(adapted),
160
+ )
@@ -0,0 +1,74 @@
1
+ """Model/host routing helpers (cloud vs local vs xAI)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from .config import Config, load_runtime_env
8
+ from . import model_info as _model_info_module
9
+
10
+
11
+ def is_cloud_model_name(name: str) -> bool:
12
+ return name.endswith(":cloud") or name.endswith("-cloud") or ":cloud-" in name
13
+
14
+
15
+ def _runtime_ollama_api_key() -> str:
16
+ """Return OLLAMA_API_KEY after loading Algo CLI's runtime env file.
17
+
18
+ Tool/API subprocesses may start without inheriting the user's shell
19
+ environment. Loading here keeps routing/status checks consistent with
20
+ tool clients that already call ``load_runtime_env()``.
21
+ """
22
+ load_runtime_env(override=True)
23
+ return os.environ.get("OLLAMA_API_KEY", "").strip()
24
+
25
+
26
+ def uses_ollama_cloud(cfg: Config) -> bool:
27
+ """Whether chat traffic should route through Ollama Cloud's direct API.
28
+
29
+ A ``:cloud`` model can also be served by a signed-in local Ollama daemon.
30
+ The first-run picker represents that case as ``cloud via local Ollama`` and
31
+ leaves ``cfg.cloud`` false. Even if stale config has ``cfg.cloud`` true, the
32
+ direct API route is active only when ``OLLAMA_API_KEY`` is present.
33
+ """
34
+ if _model_info_module.is_xai_model(cfg.model) or _model_info_module.is_chatgpt_model(cfg.model):
35
+ return False
36
+ return bool(cfg.cloud and _runtime_ollama_api_key())
37
+
38
+
39
+ def effective_runtime_host(cfg: Config) -> str:
40
+ """Provider endpoint label for session_start / ops (not necessarily cfg.host)."""
41
+ if _model_info_module.is_xai_model(cfg.model):
42
+ return "xai"
43
+ if _model_info_module.is_chatgpt_model(cfg.model):
44
+ return "chatgpt"
45
+ if uses_ollama_cloud(cfg):
46
+ return "https://ollama.com"
47
+ return cfg.host
48
+
49
+
50
+ def require_cloud_api_key(cfg: Config) -> None:
51
+ """Fail fast before a direct Cloud API chat call when OLLAMA_API_KEY is missing."""
52
+ if not uses_ollama_cloud(cfg):
53
+ return
54
+ if not _runtime_ollama_api_key():
55
+ raise ValueError(
56
+ "OLLAMA_API_KEY is required for direct Ollama Cloud API mode. "
57
+ "Set it in the process environment or in ~/.algo_cli/env."
58
+ )
59
+
60
+
61
+ def is_embedding_model_name(name: str) -> bool:
62
+ lowered = name.lower()
63
+ return (
64
+ "embed" in lowered
65
+ or "embedding" in lowered
66
+ or lowered.startswith("nomic-embed")
67
+ or "minilm" in lowered
68
+ or "paraphrase" in lowered
69
+ )
70
+
71
+
72
+ def is_vision_model_name(name: str) -> bool:
73
+ lowered = name.lower()
74
+ return any(token in lowered for token in ("vision", "-vl", "llava", "qwen2.5-vl", "qwen3-vl"))
algo_cli/oneshot.py ADDED
@@ -0,0 +1,331 @@
1
+ """One-shot non-interactive JSON event mode.
2
+
3
+ Emits one JSON object per line to stdout, suitable for subprocess consumption
4
+ by an external bridge (Telegram bot, CI, scripts). The agent loop, tool
5
+ execution, policy, and contract code are unchanged; this module only swaps
6
+ the output sink and gates the approval flow.
7
+
8
+ Event schema (one JSON object per line, no embedded raw newlines):
9
+ {"type":"session_start","model":...,"host":...,"cwd":...,"approval_mode":...,"version":...}
10
+ {"type":"thinking","text":...}
11
+ {"type":"content","text":...}
12
+ {"type":"tool_call","call_id":...,"name":...,"args":{...}}
13
+ {"type":"tool_result","call_id":...,"name":...,"status":"ok|failed|denied|skipped",
14
+ "duration_ms":...,"summary":...,"truncated":...}
15
+ {"type":"tool_denied","call_id":...,"name":...,"reason":...}
16
+ {"type":"error","class":"timeout|policy|tool|model|internal","message":...}
17
+ {"type":"done","status":"complete|partial|failed","status_reason":...,
18
+ "tool_calls":...,"duration_ms":...}
19
+
20
+ Invariants:
21
+ - session_start is the first event; done is the last event.
22
+ - tool_result always follows the matching tool_call by call_id.
23
+ - No ANSI escape codes in stdout.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import sys
30
+ import time
31
+ from collections import deque
32
+ from typing import Any
33
+
34
+
35
+ DANGEROUS_TOOLS = {"run_shell", "write_file", "edit_file", "batch_edit", "update_user_profile", "model_delete", "model_create"}
36
+ SUMMARY_LIMIT = 600
37
+
38
+
39
+ def _summarize(text: str, limit: int = SUMMARY_LIMIT) -> tuple[str, bool]:
40
+ s = str(text).strip()
41
+ if len(s) <= limit:
42
+ return s, False
43
+ return s[:limit].rstrip() + "...", True
44
+
45
+
46
+ def _tool_status_from_result(result: str) -> str:
47
+ lowered = str(result).strip().lower()
48
+ if lowered.startswith("user denied"):
49
+ return "denied"
50
+ if lowered.startswith("skipped repeated"):
51
+ return "skipped"
52
+ if lowered.startswith(("error", "tool error", "tool argument error", "unknown tool")):
53
+ return "failed"
54
+ return "ok"
55
+
56
+
57
+ class JsonEventSink:
58
+ """Stdout writer for one-shot JSON events. Thread-safe enough for serial dispatch."""
59
+
60
+ def __init__(self, *, stream=None, approval_mode: str = "never") -> None:
61
+ self._stream = stream if stream is not None else sys.stdout
62
+ self.approval_mode = approval_mode
63
+ self.deny_dangerous = approval_mode == "never"
64
+ self._call_count = 0
65
+ self._pending_call_ids: dict[str, deque[str]] = {}
66
+ self._tool_calls_done = 0
67
+ self._prompt_tokens = 0
68
+ self._completion_tokens = 0
69
+ self._errors: list[dict[str, str]] = []
70
+ self._started_at = time.perf_counter()
71
+
72
+ def _write(self, event: dict[str, Any]) -> None:
73
+ line = json.dumps(event, ensure_ascii=False, default=str)
74
+ # JSON encoding already escapes embedded newlines; one event per line is invariant.
75
+ self._stream.write(line + "\n")
76
+ self._stream.flush()
77
+
78
+ # --- framing ---
79
+
80
+ def session_start(self, *, model: str, host: str, cwd: str, version: str) -> None:
81
+ self._write({
82
+ "type": "session_start",
83
+ "model": model,
84
+ "host": host,
85
+ "cwd": cwd,
86
+ "approval_mode": self.approval_mode,
87
+ "version": version,
88
+ })
89
+
90
+ def done(self, *, status: str, status_reason: str, duration_ms: float) -> None:
91
+ total_tokens = self._prompt_tokens + self._completion_tokens
92
+ self._write({
93
+ "type": "done",
94
+ "status": status,
95
+ "status_reason": status_reason,
96
+ "tool_calls": self._tool_calls_done,
97
+ "duration_ms": round(duration_ms, 2),
98
+ "usage": {
99
+ "prompt_tokens": self._prompt_tokens,
100
+ "completion_tokens": self._completion_tokens,
101
+ "total_tokens": total_tokens,
102
+ },
103
+ })
104
+
105
+ # --- model output ---
106
+
107
+ def thinking(self, text: str) -> None:
108
+ if not text:
109
+ return
110
+ self._write({"type": "thinking", "text": text})
111
+
112
+ def content(self, text: str) -> None:
113
+ if not text:
114
+ return
115
+ self._write({"type": "content", "text": text})
116
+
117
+ def chat_usage(self, *, prompt_tokens: Any, completion_tokens: Any) -> None:
118
+ """Accumulate provider-reported usage once per completed chat turn."""
119
+
120
+ try:
121
+ prompt = int(prompt_tokens or 0)
122
+ completion = int(completion_tokens or 0)
123
+ except (TypeError, ValueError):
124
+ return
125
+ if prompt > 0 or completion > 0:
126
+ self._prompt_tokens += max(0, prompt)
127
+ self._completion_tokens += max(0, completion)
128
+
129
+ # --- tool dispatch ---
130
+
131
+ def next_call_id(self) -> str:
132
+ self._call_count += 1
133
+ return f"oneshot-{self._call_count}"
134
+
135
+ def tool_call(self, *, call_id: str, name: str, args: dict[str, Any]) -> None:
136
+ self._pending_call_ids.setdefault(name, deque()).append(call_id)
137
+ self._write({
138
+ "type": "tool_call",
139
+ "call_id": call_id,
140
+ "name": name,
141
+ "args": args,
142
+ })
143
+
144
+ def tool_result(
145
+ self,
146
+ *,
147
+ call_id: str | None,
148
+ name: str,
149
+ result: str,
150
+ duration_ms: float | None,
151
+ ) -> None:
152
+ call_id = self._matching_call_id(name, call_id)
153
+ status = _tool_status_from_result(result)
154
+ summary, truncated = _summarize(result)
155
+ self._tool_calls_done += 1
156
+ self._write({
157
+ "type": "tool_result",
158
+ "call_id": call_id,
159
+ "name": name,
160
+ "status": status,
161
+ "duration_ms": round(duration_ms, 2) if duration_ms is not None else None,
162
+ "summary": summary,
163
+ "truncated": truncated,
164
+ })
165
+
166
+ def tool_denied(self, *, call_id: str | None, name: str, reason: str) -> None:
167
+ call_id = self._matching_call_id(name, call_id)
168
+ self._tool_calls_done += 1
169
+ self._write({
170
+ "type": "tool_denied",
171
+ "call_id": call_id,
172
+ "name": name,
173
+ "reason": reason,
174
+ })
175
+
176
+ def _matching_call_id(self, name: str, call_id: str | None) -> str:
177
+ """Resolve a result to the oldest unmatched call of the same tool."""
178
+
179
+ pending = self._pending_call_ids.get(name)
180
+ if call_id:
181
+ if pending:
182
+ try:
183
+ pending.remove(call_id)
184
+ except ValueError:
185
+ pass
186
+ if not pending:
187
+ self._pending_call_ids.pop(name, None)
188
+ return call_id
189
+ if pending:
190
+ matched = pending.popleft()
191
+ if not pending:
192
+ self._pending_call_ids.pop(name, None)
193
+ return matched
194
+ return self.next_call_id()
195
+
196
+ # --- errors ---
197
+
198
+ def error(self, *, error_class: str, message: str) -> None:
199
+ self._errors.append({"class": str(error_class), "message": str(message)})
200
+ self._write({
201
+ "type": "error",
202
+ "class": error_class,
203
+ "message": message,
204
+ })
205
+
206
+ @property
207
+ def errors(self) -> tuple[dict[str, str], ...]:
208
+ return tuple(self._errors)
209
+
210
+
211
+ def run_oneshot(
212
+ *,
213
+ prompt: str,
214
+ approval_mode: str = "never",
215
+ cfg_overrides: dict[str, Any] | None = None,
216
+ stream=None,
217
+ ) -> int:
218
+ """Run a single agent turn and emit JSON events to stdout. Returns exit code.
219
+
220
+ - approval_mode="never" (default): dangerous tools are denied; emits tool_denied.
221
+ - approval_mode="auto": equivalent to cfg.auto_mode=True for this run only.
222
+ - cfg_overrides: applied to the loaded Config before the run (e.g., {"model": "qwen3"}).
223
+ """
224
+ # Imports deferred to avoid cycle with display + to keep import cost off the
225
+ # interactive path when --oneshot is not used.
226
+ from . import deliberation, display, harness, main, skills, tool_runtime
227
+ from .config import Config
228
+ from .model_routing import effective_runtime_host
229
+ from .tool_runtime import session_command_requires_approval
230
+
231
+ cfg = Config.load()
232
+ persistent_values: dict[str, Any] = {
233
+ "auto_mode": cfg.auto_mode,
234
+ "skill_crystallize_enabled": cfg.skill_crystallize_enabled,
235
+ "session_summary": cfg.session_summary,
236
+ "show_thinking": cfg.show_thinking,
237
+ "temperature": cfg.temperature,
238
+ }
239
+ if cfg_overrides:
240
+ for key, value in cfg_overrides.items():
241
+ if value is not None and hasattr(cfg, key):
242
+ persistent_values.setdefault(key, getattr(cfg, key))
243
+ setattr(cfg, key, value)
244
+ harness.configure_context_sources(
245
+ external=cfg.external_harness_sources_enabled,
246
+ index_compute_lab=cfg.index_compute_lab_auto_inject,
247
+ )
248
+ if approval_mode not in {"never", "auto"}:
249
+ raise ValueError("approval_mode must be 'never' or 'auto'")
250
+ cfg.auto_mode = approval_mode == "auto"
251
+ cfg.skill_crystallize_enabled = False # subprocess invocation must not mutate skill store
252
+ if not cfg_overrides or "show_thinking" not in cfg_overrides:
253
+ cfg.show_thinking = cfg.show_thinking and deliberation.needs_deliberation(prompt)
254
+ if not cfg_overrides or "temperature" not in cfg_overrides:
255
+ cfg.temperature = min(cfg.temperature, 0.2)
256
+
257
+ # Bridge runs (Telegram, CI) must not inherit interactive session_summary into prompts.
258
+ cfg.session_summary = ""
259
+
260
+ sink = JsonEventSink(stream=stream, approval_mode=approval_mode)
261
+ sink.session_start(
262
+ model=cfg.model,
263
+ host=effective_runtime_host(cfg),
264
+ cwd=cfg.cwd,
265
+ version=_resolve_version(),
266
+ )
267
+
268
+ display.install_json_sink(sink)
269
+ original_ask_approval = main.ask_approval
270
+ original_runtime_ask_approval = tool_runtime.ask_approval
271
+
272
+ def _oneshot_ask_approval(name: str, args: dict[str, Any], cfg: Config, *, force: bool = False) -> bool:
273
+ del cfg
274
+ requires_approval = name in DANGEROUS_TOOLS or (
275
+ name == "session_command"
276
+ and session_command_requires_approval(str(args.get("command") or ""))
277
+ )
278
+ if approval_mode == "never" and requires_approval:
279
+ # Sink's tool_denied is emitted in the agent_loop's denial path via show_tool_result.
280
+ # We just refuse here; the existing "User denied this operation." message flows
281
+ # through show_tool_result → sink.tool_denied conversion.
282
+ return False
283
+ if approval_mode == "auto" and not force:
284
+ return True
285
+ return True
286
+
287
+ main.ask_approval = _oneshot_ask_approval
288
+ tool_runtime.ask_approval = _oneshot_ask_approval
289
+
290
+ started = time.perf_counter()
291
+ status = "complete"
292
+ status_reason = ""
293
+ try:
294
+ client = main.create_client(cfg)
295
+ main.agent_loop(client, cfg, prompt)
296
+ except KeyboardInterrupt:
297
+ status = "failed"
298
+ status_reason = "interrupted"
299
+ sink.error(error_class="internal", message="KeyboardInterrupt")
300
+ except Exception as exc:
301
+ status = "failed"
302
+ status_reason = f"{type(exc).__name__}: {exc}"
303
+ sink.error(error_class="internal", message=status_reason)
304
+ finally:
305
+ main.ask_approval = original_ask_approval
306
+ tool_runtime.ask_approval = original_runtime_ask_approval
307
+ display.uninstall_json_sink()
308
+ skills.ensure_dirs() # restore any deferred dir state
309
+ # agent_loop saves config; restore bridge-only mutations and CLI override fields.
310
+ for key, value in persistent_values.items():
311
+ setattr(cfg, key, value)
312
+ cfg.save()
313
+
314
+ if status == "complete" and sink.errors:
315
+ status = "partial"
316
+ status_reason = sink.errors[-1].get("message", "one-shot run emitted an error event")
317
+
318
+ sink.done(
319
+ status=status,
320
+ status_reason=status_reason,
321
+ duration_ms=(time.perf_counter() - started) * 1000,
322
+ )
323
+ return 0 if status == "complete" else 2
324
+
325
+
326
+ def _resolve_version() -> str:
327
+ try:
328
+ from importlib.metadata import version
329
+ return version("algo-cli-runtime")
330
+ except Exception:
331
+ return "unknown"