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,679 @@
1
+ """Context window estimates, pruning, compaction, and system prompt assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable
11
+
12
+ from ollama import Client
13
+
14
+ from .config import Config, load_runtime_env
15
+ from . import harness
16
+ from . import identity
17
+ from . import model_info as _model_info_module
18
+ from . import reflex
19
+ from .chat_protocol import get_attr
20
+ from .display import json_sink
21
+
22
+ CONTEXT_COMPACT_THRESHOLD = 0.85
23
+ CONTEXT_KEEP_MESSAGES = 12
24
+ SMALL_CONTEXT_COMPACT_THRESHOLD = 0.70
25
+ SMALL_CONTEXT_KEEP_MESSAGES = 8
26
+ MEDIUM_CONTEXT_COMPACT_THRESHOLD = 0.78
27
+ MEDIUM_CONTEXT_KEEP_MESSAGES = 10
28
+ FOOTER_METRICS_FRESHNESS_SECONDS = 30.0
29
+ ATTEMPT_PROMPT_LIMIT = 24
30
+ OPTIONAL_CONTEXT_MIN_TOKENS = 96
31
+ OPTIONAL_CONTEXT_TRUNCATION_SUFFIX = "\n...[truncated by context budget]"
32
+
33
+ _SMALL_MODEL_THRESHOLD_B = 70.0
34
+
35
+ _CALIBRATION_BLOCK = (
36
+ "\n\n## Accuracy Constraints (small-model mode)\n"
37
+ "You are running as a compact model. Apply these rules strictly:\n"
38
+ "- Never invent file paths, function names, version numbers, URLs, or command flags.\n"
39
+ "- If you lack clear evidence for a specific fact, say 'I'm not certain — let me check' "
40
+ "and use search_files, read_file, or harness_search to verify before stating it.\n"
41
+ "- Prefer 'I don't know' over a confident wrong answer.\n"
42
+ "- For code claims, verify with a tool call rather than relying on memory."
43
+ )
44
+
45
+ CONTEXT_USAGE_CACHE: tuple[tuple[Any, ...], int] | None = None
46
+
47
+
48
+ def invalidate_context_usage_cache() -> None:
49
+ global CONTEXT_USAGE_CACHE
50
+ CONTEXT_USAGE_CACHE = None
51
+
52
+
53
+ def _context_usage_cache_key(
54
+ cfg: Config,
55
+ *,
56
+ lessons_fingerprint: int = 0,
57
+ model_info_fingerprint: int = 0,
58
+ user_message_fingerprint: int = 0,
59
+ ) -> tuple[Any, ...]:
60
+ last_message = cfg.messages[-1] if cfg.messages else {}
61
+ return (
62
+ len(cfg.messages),
63
+ len(str(last_message.get("content", ""))),
64
+ len(str(last_message.get("thinking", ""))),
65
+ len(json.dumps(last_message.get("tool_calls", []), ensure_ascii=False, default=str)),
66
+ len(cfg.session_summary),
67
+ len(cfg.attempt_ledger),
68
+ len(cfg.memories),
69
+ cfg.num_ctx,
70
+ cfg.system,
71
+ identity.identity_mtime_key(),
72
+ lessons_fingerprint,
73
+ model_info_fingerprint,
74
+ user_message_fingerprint,
75
+ cfg.session_mode,
76
+ )
77
+
78
+
79
+ def estimate_text_tokens(text: str) -> int:
80
+ if not text:
81
+ return 0
82
+ return max(1, (len(text) + 3) // 4)
83
+
84
+
85
+ def estimate_message_tokens(message: dict[str, Any]) -> int:
86
+ total = 12
87
+ total += estimate_text_tokens(str(message.get("role", "")))
88
+ total += estimate_text_tokens(str(message.get("content", "")))
89
+ total += estimate_text_tokens(str(message.get("thinking", "")))
90
+ total += estimate_text_tokens(json.dumps(message.get("tool_calls", []), ensure_ascii=False, default=str))
91
+ total += estimate_text_tokens(str(message.get("tool_name", "")))
92
+ return total
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class OptionalContextBlock:
97
+ name: str
98
+ title: str
99
+ body: str
100
+
101
+
102
+ def context_compaction_policy(model_info: dict[str, Any] | None = None) -> tuple[float, int]:
103
+ """Return (threshold, keep_messages) for the active model/window."""
104
+ info = model_info or {}
105
+ size_b = _model_info_module.parameter_size_billions(info)
106
+ native_ctx = _model_info_module.get_context_length(info)
107
+ if (size_b is not None and size_b <= 9.0) or (native_ctx is not None and native_ctx <= 8192):
108
+ return SMALL_CONTEXT_COMPACT_THRESHOLD, SMALL_CONTEXT_KEEP_MESSAGES
109
+ if size_b is not None and size_b <= 32.0:
110
+ return MEDIUM_CONTEXT_COMPACT_THRESHOLD, MEDIUM_CONTEXT_KEEP_MESSAGES
111
+ return CONTEXT_COMPACT_THRESHOLD, CONTEXT_KEEP_MESSAGES
112
+
113
+
114
+ def context_response_reserve(runtime_cap: int, model_info: dict[str, Any] | None = None) -> int:
115
+ """Reserve room for the assistant reply and tool-call metadata."""
116
+ cap = max(1, int(runtime_cap or 1))
117
+ size_b = _model_info_module.parameter_size_billions(model_info or {})
118
+ if cap <= 4096 or (size_b is not None and size_b <= 9.0):
119
+ reserve = max(384, cap // 6)
120
+ elif size_b is not None and size_b <= 32.0:
121
+ reserve = max(768, cap // 8)
122
+ else:
123
+ reserve = max(1024, cap // 10)
124
+ return min(reserve, max(1, cap // 3))
125
+
126
+
127
+ def render_optional_context_block(block: OptionalContextBlock) -> str:
128
+ body = (block.body or "").strip()
129
+ if not body:
130
+ return ""
131
+ title = (block.title or "").strip()
132
+ return f"## {title}\n{body}" if title else body
133
+
134
+
135
+ def _truncate_to_token_budget(text: str, token_budget: int) -> str:
136
+ if token_budget < OPTIONAL_CONTEXT_MIN_TOKENS:
137
+ return ""
138
+ if estimate_text_tokens(text) <= token_budget:
139
+ return text
140
+ suffix_tokens = estimate_text_tokens(OPTIONAL_CONTEXT_TRUNCATION_SUFFIX)
141
+ char_limit = max(0, (token_budget - suffix_tokens) * 4)
142
+ if char_limit < 120:
143
+ return ""
144
+ candidate = text[:char_limit].rstrip() + OPTIONAL_CONTEXT_TRUNCATION_SUFFIX
145
+ while estimate_text_tokens(candidate) > token_budget and char_limit > 120:
146
+ char_limit -= 80
147
+ candidate = text[:char_limit].rstrip() + OPTIONAL_CONTEXT_TRUNCATION_SUFFIX
148
+ if estimate_text_tokens(candidate) > token_budget:
149
+ return ""
150
+ return candidate
151
+
152
+
153
+ def fit_optional_context_blocks(
154
+ base_message: str,
155
+ blocks: list[OptionalContextBlock],
156
+ *,
157
+ base_used_tokens: int,
158
+ runtime_cap: int,
159
+ model_info: dict[str, Any] | None = None,
160
+ ) -> tuple[str, list[str], list[str], int]:
161
+ """Append optional context blocks only while the request stays within budget."""
162
+ budget = int(runtime_cap) - int(base_used_tokens) - context_response_reserve(runtime_cap, model_info)
163
+ budget = max(0, budget)
164
+ included: list[str] = []
165
+ omitted: list[str] = []
166
+ rendered_parts: list[str] = []
167
+ used_tokens = 0
168
+ for block in blocks:
169
+ rendered = render_optional_context_block(block)
170
+ if not rendered:
171
+ continue
172
+ cost = estimate_text_tokens("\n\n" + rendered)
173
+ if cost <= budget:
174
+ rendered_parts.append(rendered)
175
+ included.append(block.name)
176
+ budget -= cost
177
+ used_tokens += cost
178
+ continue
179
+ truncated = _truncate_to_token_budget(rendered, budget)
180
+ if truncated:
181
+ truncated_cost = estimate_text_tokens("\n\n" + truncated)
182
+ rendered_parts.append(truncated)
183
+ included.append(block.name)
184
+ budget = max(0, budget - truncated_cost)
185
+ used_tokens += truncated_cost
186
+ else:
187
+ omitted.append(block.name)
188
+ if not rendered_parts:
189
+ return base_message, included, omitted, used_tokens
190
+ return f"{base_message}\n\n" + "\n\n".join(rendered_parts), included, omitted, used_tokens
191
+
192
+
193
+ def build_system_prompt(
194
+ cfg: Config,
195
+ *,
196
+ retrieved_lessons: list[str] | None = None,
197
+ active_model_info: dict[str, Any] | None = None,
198
+ user_message: str | None = None,
199
+ ) -> str:
200
+ identity_block = identity.build_identity_block(retrieved_lessons=retrieved_lessons)
201
+ prompt = (identity_block + "\n\n" if identity_block else "") + cfg.system
202
+ load_runtime_env(override=True)
203
+ if _model_info_module.is_xai_model(cfg.model):
204
+ provider = "xAI Grok OAuth"
205
+ elif cfg.cloud and os.environ.get("OLLAMA_API_KEY", "").strip():
206
+ provider = "Ollama Cloud direct API"
207
+ elif _model_info_module.is_cloud_model_name(cfg.model):
208
+ provider = "local Ollama (cloud model via login)"
209
+ else:
210
+ provider = "local Ollama"
211
+ external_harness_guidance = (
212
+ "External local agent stores are enabled for this session; harness tools may search Codex, Claude, "
213
+ "OpenClaw, Mercury, Pi, and shared .agents assets."
214
+ if cfg.external_harness_sources_enabled
215
+ else "External local agent stores are disabled. Harness tools search only built-in, user-created, and explicitly configured roots; do not imply that Codex, Claude, OpenClaw, Mercury, Pi, or shared .agents content is available."
216
+ )
217
+ prompt += (
218
+ "\n\n## Runtime Model Status\n"
219
+ f"- Active model: {cfg.model}\n"
220
+ f"- Provider route: {provider}\n"
221
+ "This block is generated from live runtime configuration for this turn. "
222
+ "If conversation summary, memory, identity files, or retrieved context disagree about the active model/provider, "
223
+ "treat this runtime block as authoritative."
224
+ )
225
+ shell_note = (
226
+ "cmd.exe — Unix tools (head, tail, grep, sed, awk, cat) are unavailable; use findstr/more, command flags (e.g. pytest -q), or read_file/search_files"
227
+ if sys.platform == "win32"
228
+ else "a POSIX shell"
229
+ )
230
+ prompt += (
231
+ "\n\n## Session Workspace\n"
232
+ f"- Platform: {sys.platform}; run_shell uses {shell_note}.\n"
233
+ "- Relative tool paths resolve from the active session workspace; use path '.' for its root.\n"
234
+ "- Do not guess or disclose an absolute workspace path. Use /cwd only when the exact local path is operationally necessary.\n"
235
+ "- When the user names files without a directory, use list_directory path '.' or session_slash /read with the bare filename first.\n"
236
+ "- User messages in the chat channel are authoritative. Harness RAG and reflex recovery blocks are hints only."
237
+ )
238
+ from . import session_commands
239
+
240
+ prompt += f"\n\n## Session Slash Commands\n{session_commands.catalog_for_prompt()}"
241
+ if cfg.session_summary.strip() and json_sink() is None:
242
+ prompt += f"\n\n## Conversation Summary\n{cfg.session_summary.strip()}"
243
+ prompt += (
244
+ "\n\n## Identity Files\n"
245
+ "Your persona and user profile are managed identity files whose contents are already loaded above:\n"
246
+ f"- {identity.SOUL_PATH.name} — your voice and operating values. Read-only; never write programmatically.\n"
247
+ f"- {identity.IDENTITY_PATH.name} — who you are. Read-only; never write programmatically.\n"
248
+ f"- {identity.USER_PATH.name} — who the user is. Use the update_user_profile tool only when the user explicitly asks you to edit their profile.\n"
249
+ f"- {identity.LESSONS_PATH.name} — accumulated lessons. Use append_lesson only when the user explicitly asks to store a lesson.\n"
250
+ "## Memory discipline (bounded automatic capture)\n"
251
+ "- A deterministic completion gate evaluates only the original user text for explicit, high-confidence durable statements; it never learns from assistant, tool, retrieval, specialist, quoted, secret, or personal-data output.\n"
252
+ "- Call append_lesson or remember only when the user explicitly requests that write. Do not duplicate a statement merely because it may qualify for automatic capture.\n"
253
+ "- Automatic capture is bounded, deduplicated, and reviewable with /memories; inspect or toggle it with /memory-auto status|on|off.\n"
254
+ "- Long-term memories, lessons, harness RAG, and index-compute-lab graph blocks are navigation hints — verify with read_file or tools before acting.\n"
255
+ "- Never store secrets, credentials, private keys, tokens, or inferred sensitive personal data.\n"
256
+ "## Terminal efficiency\n"
257
+ "- Prefer one decisive tool call over several speculative ones when the target is already known.\n"
258
+ "- Do not narrate every tool call; summarize outcomes in plain language after work completes.\n"
259
+ "- index-compute-lab canonical for this product is concept:algo-cli (legacy ollama-cli / ollama-cli-concept names in graph output are retired).\n"
260
+ "The contents of all four files are already loaded into this system prompt above; do not read_file them just to see what they say. "
261
+ "When the user references 'my wiki', 'my notes', or asks you to learn from their knowledge base, note that the harness RAG layer already injected the most relevant entries into the Relevant Context section (if present); use harness_search/harness_read only for explicit deep dives the retrieval missed."
262
+ "\n\n## Local Harness Bridge\n"
263
+ f"{external_harness_guidance} Use available_actions, harness_search, harness_read, harness_stats, and harness_refresh. "
264
+ "These tools are read-only and should be preferred before broad filesystem scans when the user asks about skills, tools, prompts, memory, or wiki context."
265
+ "\n\n## index-compute-lab (knowledge graph)\n"
266
+ "When enabled, each user turn may include a ## Knowledge Graph (index-compute-lab) block: "
267
+ "ranked associations from the user's configured index-compute-lab sources. "
268
+ "Treat it like harness RAG — navigation and relationship hints, not proof that files exist. "
269
+ "Use query_knowledge_graph for ranked co-occurrence (not prose biographies), and use harness_search for supporting documents. "
270
+ "To persist a correction or contact before the next full reindex: write_knowledge_graph_note then harness_refresh. "
271
+ "Use reindex_knowledge_graph only when the user explicitly asks to rebuild configured graph sources."
272
+ "\n\n## Grok / xAI model compatibility\n"
273
+ "When the user asks whether a grok-* model works in this harness, do not scan the repo blindly: "
274
+ "tell them to run /model-check NAME (or run it yourself via session slash if available), "
275
+ "and search the currently enabled harness for Algo CLI xAI/X-account guidance; do not assume external record IDs exist. "
276
+ "Multi-agent grok models use xAI /v1/responses; other Grok models use /v1/chat/completions. "
277
+ "Auth is optional subscription OAuth only: the user must provide XAI_CLIENT_ID, and tokens live in "
278
+ "~/.algo_cli/xai_auth.json. Algo CLI bundles no xAI client identity."
279
+ "\n\n## Capability Awareness\n"
280
+ "When the user asks what you can do, what actions are available, what internal stats exist, or what tools/skills/memory/wiki you can access, call available_actions first."
281
+ "\n\n## PDF Handling\n"
282
+ "For PDF files, call read_pdf first. Do not claim Python is unavailable and do not improvise shell-based PDF extraction before trying read_pdf. "
283
+ "If read_pdf says the document is scanned or image-only, call render_pdf_pages next and then pass one returned PNG path to vision_describe or another OCR-capable path. "
284
+ "If a vision/OCR model returns unsupported-image or insufficient-memory errors, do not repeat the same model across more PDF pages or sibling PDFs without changing the model or approach."
285
+ )
286
+ prompt += (
287
+ "\n\n## Reflection Checkpoints\n"
288
+ f"Every {max(1, int(cfg.tool_think_every))} tool calls, pause to reassess the objective, what has been completed, what remains, "
289
+ "whether web research is needed, whether the user explicitly requested a memory write, and the next best action. "
290
+ "Treat any internal checkpoint note as a planning pause, not user-facing output. "
291
+ "After a checkpoint, continue with the next necessary tool call unless the user's task is actually complete."
292
+ )
293
+ # One-shot tool results already carry failures in the message history.
294
+ # Rebuilding the system prompt with a growing ledger invalidates Ollama's
295
+ # prefix/KV cache on every tool turn, so keep automation prompts stable.
296
+ if cfg.attempt_ledger and json_sink() is None:
297
+ ledger_lines = []
298
+ for item in cfg.attempt_ledger[-ATTEMPT_PROMPT_LIMIT:]:
299
+ ledger_lines.append(
300
+ f"- {item.get('status', '?').upper()} {item.get('tool', '?')} "
301
+ f"{item.get('args_preview', '')}: {item.get('summary', '')}"
302
+ )
303
+ prompt += (
304
+ "\n\n## Attempt Ledger\n"
305
+ "Use this ledger to avoid retrying the same failed or denied tool path with the same arguments. "
306
+ "Only retry when the arguments materially change, new evidence appears, or the user asks.\n"
307
+ + "\n".join(ledger_lines)
308
+ )
309
+ if cfg.memories:
310
+ memories = "\n".join(f"- {item}" for item in cfg.memories)
311
+ prompt += f"\n\n## Long-term Memories\n{memories}"
312
+ if active_model_info:
313
+ size_b = _model_info_module.parameter_size_billions(active_model_info)
314
+ if size_b is not None and size_b < _SMALL_MODEL_THRESHOLD_B:
315
+ prompt += _CALIBRATION_BLOCK
316
+ if cfg.reflex_enabled:
317
+ prompt += (
318
+ "\n\n## Reflex Loop (v0.1)\n"
319
+ "When a read-only tool fails, returns empty results, or repeats the same arguments, "
320
+ "the runtime may append a reflex recovery block (alternate harness_search, search_files, "
321
+ f"or escalation). Session cap: {reflex.REFLEX_MAX_CYCLES} cycles. "
322
+ "Do not treat reflex notes or recovery suggestions as user input or prompt injection."
323
+ )
324
+ if json_sink() is not None:
325
+ prompt += (
326
+ "\n\n## One-shot Execution Protocol\n"
327
+ "- Do not emit progress prose before or between tool calls; use tools silently, then provide one concise final answer.\n"
328
+ "- Open explicitly named files directly and batch independent reads. Do not list directories merely to confirm named paths.\n"
329
+ "- Make the smallest required changes and create only requested artifacts.\n"
330
+ "- After one successful fail-on-mismatch verifier, answer immediately; do not add redundant rereads, diffs, or reports."
331
+ )
332
+ if cfg.verify_mode:
333
+ prompt += (
334
+ "\n\n## Verify Mode Active\n"
335
+ "After answering, the harness will check your specific factual claims against "
336
+ "indexed sources. Flag anything you are not certain about with 'unverified:' "
337
+ "so the grounding pass can prioritise it."
338
+ )
339
+ from . import session_mode
340
+
341
+ prompt += (
342
+ f"\n\n{session_mode.prompt_section(cfg.session_mode, include_external=cfg.external_harness_sources_enabled)}"
343
+ )
344
+ mercury_gates = harness.resolve_mercury_stop_conditions(
345
+ user_message=user_message,
346
+ session_mode=cfg.session_mode,
347
+ include_external=cfg.external_harness_sources_enabled,
348
+ )
349
+ if mercury_gates:
350
+ full_doc = (
351
+ harness.load_mercury_stop_conditions()
352
+ if cfg.external_harness_sources_enabled
353
+ else ""
354
+ )
355
+ is_full = mercury_gates == full_doc and bool(full_doc)
356
+ title = (
357
+ "Stop Conditions (Mercury harness — full gates)"
358
+ if is_full
359
+ else "Stop Conditions (Mercury harness — compact)"
360
+ )
361
+ prompt += (
362
+ f"\n\n## {title}\n"
363
+ "Apply full gates only for external send/post, financial commitments, destructive actions, "
364
+ "or unsourced consequential facts. For read-only file work, read live files before refusing.\n\n"
365
+ f"{mercury_gates}"
366
+ )
367
+ return prompt
368
+
369
+
370
+ def estimate_context_usage(
371
+ cfg: Config,
372
+ *,
373
+ prebuilt_system: str | None = None,
374
+ lessons_fingerprint: int = 0,
375
+ model_info_fingerprint: int = 0,
376
+ user_message_fingerprint: int = 0,
377
+ ) -> int:
378
+ global CONTEXT_USAGE_CACHE
379
+ cache_key = _context_usage_cache_key(
380
+ cfg,
381
+ lessons_fingerprint=lessons_fingerprint,
382
+ model_info_fingerprint=model_info_fingerprint,
383
+ user_message_fingerprint=user_message_fingerprint,
384
+ )
385
+ if CONTEXT_USAGE_CACHE and CONTEXT_USAGE_CACHE[0] == cache_key:
386
+ return CONTEXT_USAGE_CACHE[1]
387
+ system_text = prebuilt_system if prebuilt_system is not None else build_system_prompt(cfg)
388
+ total = estimate_text_tokens(system_text)
389
+ for message in cfg.messages:
390
+ total += estimate_message_tokens(message)
391
+ CONTEXT_USAGE_CACHE = (cache_key, total)
392
+ return total
393
+
394
+
395
+ def estimate_usage_with_system_prompt(system_prompt: str, cfg: Config) -> int:
396
+ """Token estimate for a fully-built system prompt plus current messages."""
397
+ total = estimate_text_tokens(system_prompt)
398
+ for message in cfg.messages:
399
+ total += estimate_message_tokens(message)
400
+ return total
401
+
402
+
403
+ def _last_chat_token_usage(runtime_status: dict[str, Any]) -> int | None:
404
+ metrics = runtime_status.get("last_metrics") or {}
405
+ if not isinstance(metrics, dict):
406
+ return None
407
+ timestamp = metrics.get("timestamp")
408
+ if not timestamp or (time.time() - float(timestamp)) > FOOTER_METRICS_FRESHNESS_SECONDS:
409
+ return None
410
+ try:
411
+ prompt = int(metrics.get("prompt_eval_count") or 0)
412
+ completion = int(metrics.get("eval_count") or 0)
413
+ except (TypeError, ValueError):
414
+ return None
415
+ total = prompt + completion
416
+ return total if total > 0 else None
417
+
418
+
419
+ def context_status(
420
+ cfg: Config,
421
+ *,
422
+ client: Any | None = None,
423
+ model_info: dict[str, Any] | None = None,
424
+ runtime_status: dict[str, Any] | None = None,
425
+ usage_override: int | None = None,
426
+ ) -> tuple[int, int, int, int, int | None]:
427
+ """Return (used, display_total, remaining, runtime_cap, native_ctx)."""
428
+ if runtime_status is None:
429
+ from . import main as _main
430
+
431
+ runtime_status = _main.RUNTIME_STATUS
432
+ if model_info is None:
433
+ model_info = runtime_status.get("model_info")
434
+ if not isinstance(model_info, dict) or not model_info:
435
+ model_info = _model_info_module.resolve_model_info(cfg, client)
436
+
437
+ runtime_cap, native_ctx = _model_info_module.effective_context_limits(cfg, model_info)
438
+ display_total = runtime_cap
439
+ display_total = max(int(display_total), 1)
440
+
441
+ estimated = usage_override if usage_override is not None else estimate_context_usage(cfg)
442
+ api_used = _last_chat_token_usage(runtime_status)
443
+ used = api_used if api_used is not None else estimated
444
+ if api_used is not None:
445
+ used = max(api_used, estimated)
446
+ used = min(max(int(used), 0), display_total)
447
+ remaining = max(display_total - used, 0)
448
+ return used, display_total, remaining, runtime_cap, native_ctx
449
+
450
+
451
+ def summarize_message_batch(
452
+ cfg: Config,
453
+ batch: list[dict[str, Any]],
454
+ fallback_client: Client | None = None,
455
+ *,
456
+ maintenance_client_fn: Callable[[Config, Client | None], tuple[Client, str]] | None = None,
457
+ ) -> str:
458
+ if not batch:
459
+ return cfg.session_summary
460
+ source_lines = []
461
+ if cfg.session_summary.strip():
462
+ source_lines.append("CURRENT SUMMARY:")
463
+ source_lines.append(cfg.session_summary.strip())
464
+ source_lines.append("")
465
+ source_lines.append("MESSAGES TO COMPRESS:")
466
+ for item in batch:
467
+ role = item.get("role", "message")
468
+ content = item.get("content") or item.get("thinking") or ""
469
+ tool_name = item.get("tool_name")
470
+ if tool_name:
471
+ source_lines.append(f"- {role} [{tool_name}]: {content}")
472
+ else:
473
+ source_lines.append(f"- {role}: {content}")
474
+ prompt = "\n".join(source_lines)
475
+ system = (
476
+ "You compress conversation state for a terminal coding assistant.\n"
477
+ 'Return JSON with a single key "summary" whose value is a concise continuation '
478
+ "with only stable facts, decisions, file paths, tool results, and unresolved tasks. "
479
+ "Omit filler and repeated content. Keep it under 500 words."
480
+ )
481
+ try:
482
+ if maintenance_client_fn is None:
483
+ from . import main as _main
484
+
485
+ maintenance_client_fn = _main.small_maintenance_client
486
+ summary_client, summary_model = maintenance_client_fn(cfg, fallback_client)
487
+ response = summary_client.chat(
488
+ model=summary_model,
489
+ messages=[
490
+ {"role": "system", "content": system},
491
+ {"role": "user", "content": prompt},
492
+ ],
493
+ stream=False,
494
+ think=False,
495
+ format="json",
496
+ keep_alive=cfg.keep_alive,
497
+ options={"temperature": 0.1, "num_ctx": min(cfg.num_ctx, 4096), "num_predict": 600},
498
+ )
499
+ content = get_attr(get_attr(response, "message", {}), "content", "")
500
+ if content:
501
+ try:
502
+ return str(json.loads(content).get("summary", content)).strip()
503
+ except (json.JSONDecodeError, AttributeError):
504
+ return str(content).strip()
505
+ except Exception:
506
+ pass
507
+ fallback = []
508
+ if cfg.session_summary.strip():
509
+ fallback.append(cfg.session_summary.strip())
510
+ for item in batch[-4:]:
511
+ role = item.get("role", "message")
512
+ content = (item.get("content") or item.get("thinking") or "")[:240]
513
+ fallback.append(f"{role}: {content}")
514
+ return "\n".join(fallback).strip()
515
+
516
+
517
+ def _tool_call_id(call: Any) -> str | None:
518
+ if isinstance(call, dict):
519
+ return call.get("id") or (call.get("function") or {}).get("id")
520
+ return getattr(call, "id", None)
521
+
522
+
523
+ def _strip_tool_call_at(
524
+ messages: list[dict[str, Any]],
525
+ assistant_index: int,
526
+ *,
527
+ call_id: str | None = None,
528
+ ) -> bool:
529
+ if assistant_index < 0 or assistant_index >= len(messages):
530
+ return False
531
+ prev = messages[assistant_index]
532
+ calls = list(prev.get("tool_calls") or [])
533
+ if not calls:
534
+ return False
535
+ if call_id:
536
+ filtered = [c for c in calls if _tool_call_id(c) != call_id]
537
+ if len(filtered) == len(calls):
538
+ return False
539
+ else:
540
+ filtered = calls[1:]
541
+ new_prev = dict(prev)
542
+ if filtered:
543
+ new_prev["tool_calls"] = filtered
544
+ else:
545
+ new_prev.pop("tool_calls", None)
546
+ messages[assistant_index] = new_prev
547
+ return True
548
+
549
+
550
+ def prune_stale_tool_messages(cfg: Config) -> int:
551
+ total = len(cfg.messages)
552
+ if total <= cfg.prune_after_messages:
553
+ return 0
554
+ keep_from = max(0, total - cfg.prune_keep_recent)
555
+ if keep_from == 0:
556
+ return 0
557
+
558
+ removed = 0
559
+ new_messages: list[dict[str, Any]] = []
560
+ call_id_to_assistant: dict[str, int] = {}
561
+ pending_assistants: list[int] = []
562
+
563
+ for index, message in enumerate(cfg.messages):
564
+ if index >= keep_from:
565
+ new_messages.append(message)
566
+ continue
567
+ role = message.get("role")
568
+ if role == "assistant":
569
+ tool_calls = message.get("tool_calls") or []
570
+ if tool_calls:
571
+ pending_assistants.append(len(new_messages))
572
+ for call in tool_calls:
573
+ cid = _tool_call_id(call)
574
+ if cid:
575
+ call_id_to_assistant[cid] = len(new_messages)
576
+ new_messages.append(message)
577
+ continue
578
+ if role != "tool":
579
+ new_messages.append(message)
580
+ continue
581
+ call_id = message.get("tool_call_id")
582
+ if call_id and call_id in call_id_to_assistant:
583
+ idx = call_id_to_assistant.pop(call_id)
584
+ _strip_tool_call_at(new_messages, idx, call_id=call_id)
585
+ elif not call_id:
586
+ while pending_assistants:
587
+ idx = pending_assistants[0]
588
+ if idx >= len(new_messages) or not (new_messages[idx].get("tool_calls") or []):
589
+ pending_assistants.pop(0)
590
+ continue
591
+ _strip_tool_call_at(new_messages, idx)
592
+ break
593
+ removed += 1
594
+
595
+ if removed:
596
+ cfg.messages = new_messages
597
+ from . import perf_telemetry
598
+
599
+ perf_telemetry.record_perf_event(
600
+ "prune",
601
+ removed=removed,
602
+ kept=len(new_messages),
603
+ threshold=cfg.prune_after_messages,
604
+ )
605
+ return removed
606
+
607
+
608
+ def _split_for_compaction(messages: list[dict[str, Any]], keep_count: int) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
609
+ keep_from = max(0, len(messages) - keep_count)
610
+ while keep_from > 0 and messages[keep_from].get("role") == "tool":
611
+ keep_from -= 1
612
+ while keep_from > 0 and messages[keep_from].get("role") == "tool":
613
+ keep_from -= 1
614
+ return messages[:keep_from], messages[keep_from:]
615
+
616
+
617
+ def maybe_compact_context(
618
+ client: Client,
619
+ cfg: Config,
620
+ *,
621
+ precomputed_used: int | None = None,
622
+ model_info: dict[str, Any] | None = None,
623
+ ) -> bool:
624
+ used, total, _remaining, _runtime_cap, _native = context_status(
625
+ cfg,
626
+ client=client,
627
+ model_info=model_info,
628
+ usage_override=precomputed_used,
629
+ )
630
+ if total <= 0:
631
+ return False
632
+ compact_threshold, keep_messages = context_compaction_policy(model_info)
633
+ if used < total * compact_threshold:
634
+ return False
635
+ if len(cfg.messages) <= keep_messages:
636
+ return False
637
+ batch, kept = _split_for_compaction(cfg.messages, keep_messages)
638
+ if not batch:
639
+ return False
640
+ started = time.perf_counter()
641
+ from . import main as _main
642
+ from . import perf_telemetry
643
+
644
+ summary = summarize_message_batch(
645
+ cfg, batch, client, maintenance_client_fn=_main.small_maintenance_client
646
+ )
647
+ duration_ms = round((time.perf_counter() - started) * 1000, 2)
648
+ cfg.session_summary = summary
649
+ cfg.messages = kept
650
+ cfg.save()
651
+ perf_telemetry.record_perf_event(
652
+ "compaction",
653
+ duration_ms=duration_ms,
654
+ messages_compacted=len(batch),
655
+ keep_messages=keep_messages,
656
+ threshold=compact_threshold,
657
+ )
658
+ return True
659
+
660
+
661
+ def rebuild_context_summary(client: Client, cfg: Config) -> tuple[bool, str]:
662
+ if len(cfg.messages) <= CONTEXT_KEEP_MESSAGES:
663
+ return False, f"Need more than {CONTEXT_KEEP_MESSAGES} messages to compact."
664
+ batch, kept = _split_for_compaction(cfg.messages, CONTEXT_KEEP_MESSAGES)
665
+ if not batch:
666
+ return False, "No safe message boundary found for compaction."
667
+ started = time.perf_counter()
668
+ from . import main as _main
669
+ from . import perf_telemetry
670
+
671
+ summary = summarize_message_batch(
672
+ cfg, batch, client, maintenance_client_fn=_main.small_maintenance_client
673
+ )
674
+ duration_ms = round((time.perf_counter() - started) * 1000, 2)
675
+ cfg.session_summary = summary
676
+ cfg.messages = kept
677
+ cfg.save()
678
+ perf_telemetry.record_perf_event("compaction", duration_ms=duration_ms, messages_compacted=len(batch), manual=True)
679
+ return True, f"Context summary rebuilt from {len(batch)} messages; kept {len(kept)} recent messages."