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,209 @@
1
+ """Bounded Window TinyLFU cache admission for hot runtime data.
2
+
3
+ The cache keeps a small recency window in front of a larger main LRU segment.
4
+ When the window overflows, a bounded frequency sketch decides whether the
5
+ candidate should displace the main segment's least-recently-used entry. This
6
+ prevents one-off scans from evicting repeatedly used query embeddings.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import math
13
+ import threading
14
+ from collections import OrderedDict
15
+ from dataclasses import asdict, dataclass
16
+ from typing import Generic, Iterator, TypeVar
17
+
18
+ K = TypeVar("K")
19
+ V = TypeVar("V")
20
+
21
+ _MISSING = object()
22
+
23
+
24
+ class FrequencySketch(Generic[K]):
25
+ """Small count-min sketch with periodic aging and stable hashing."""
26
+
27
+ def __init__(self, capacity: int, *, depth: int = 4, sample_multiplier: int = 10) -> None:
28
+ target_width = max(16, max(1, int(capacity)) * 4)
29
+ self.width = 1 << math.ceil(math.log2(target_width))
30
+ self.depth = min(4, max(2, int(depth)))
31
+ self.sample_size = max(10, max(1, int(capacity)) * max(2, int(sample_multiplier)))
32
+ self._rows = [[0] * self.width for _ in range(self.depth)]
33
+ self._samples = 0
34
+ self.decays = 0
35
+
36
+ @staticmethod
37
+ def _digest(key: object) -> bytes:
38
+ return hashlib.blake2b(repr(key).encode("utf-8", errors="replace"), digest_size=16).digest()
39
+
40
+ def _indexes(self, key: K) -> Iterator[tuple[int, int]]:
41
+ digest = self._digest(key)
42
+ for row in range(self.depth):
43
+ offset = row * 4
44
+ value = int.from_bytes(digest[offset:offset + 4], "little", signed=False)
45
+ yield row, value & (self.width - 1)
46
+
47
+ def increment(self, key: K) -> None:
48
+ for row, index in self._indexes(key):
49
+ if self._rows[row][index] < 2_147_483_647:
50
+ self._rows[row][index] += 1
51
+ self._samples += 1
52
+ if self._samples >= self.sample_size:
53
+ self._decay()
54
+
55
+ def estimate(self, key: K) -> int:
56
+ return min(self._rows[row][index] for row, index in self._indexes(key))
57
+
58
+ def _decay(self) -> None:
59
+ for row in self._rows:
60
+ for index, count in enumerate(row):
61
+ row[index] = count // 2
62
+ self._samples //= 2
63
+ self.decays += 1
64
+
65
+ def clear(self) -> None:
66
+ self._rows = [[0] * self.width for _ in range(self.depth)]
67
+ self._samples = 0
68
+ self.decays = 0
69
+
70
+
71
+ @dataclass
72
+ class CacheAdmissionStats:
73
+ hits: int = 0
74
+ misses: int = 0
75
+ admissions: int = 0
76
+ rejections: int = 0
77
+ evictions: int = 0
78
+
79
+ def to_dict(self) -> dict[str, int]:
80
+ return asdict(self)
81
+
82
+
83
+ class WindowTinyLFUCache(Generic[K, V]):
84
+ """Thread-safe, entry-bounded Window TinyLFU cache.
85
+
86
+ This intentionally exposes only the small mapping surface used by Algo CLI:
87
+ ``get``, ``put``/item assignment, ``clear``, membership, and length.
88
+ """
89
+
90
+ def __init__(self, capacity: int, *, window_fraction: float = 0.20) -> None:
91
+ self.window_fraction = min(0.50, max(0.01, float(window_fraction)))
92
+ self._lock = threading.RLock()
93
+ self.stats = CacheAdmissionStats()
94
+ self._set_capacity(capacity)
95
+ self._window: OrderedDict[K, V] = OrderedDict()
96
+ self._main: OrderedDict[K, V] = OrderedDict()
97
+ self._sketch: FrequencySketch[K] = FrequencySketch(self.capacity)
98
+
99
+ def _set_capacity(self, capacity: int) -> None:
100
+ self.capacity = max(1, int(capacity))
101
+ self.window_capacity = max(1, min(self.capacity, round(self.capacity * self.window_fraction)))
102
+ self.main_capacity = self.capacity - self.window_capacity
103
+
104
+ def resize(self, capacity: int) -> None:
105
+ target = max(1, int(capacity))
106
+ with self._lock:
107
+ if target == self.capacity:
108
+ return
109
+ retained = list(self._main.items()) + list(self._window.items())
110
+ self._set_capacity(target)
111
+ self._window.clear()
112
+ self._main.clear()
113
+ self._sketch = FrequencySketch(self.capacity)
114
+ for key, value in retained[-self.capacity:]:
115
+ self.put(key, value)
116
+ self.stats = CacheAdmissionStats()
117
+
118
+ def get(self, key: K, default: V | None = None) -> V | None:
119
+ with self._lock:
120
+ value: object = _MISSING
121
+ if key in self._window:
122
+ value = self._window[key]
123
+ self._window.move_to_end(key)
124
+ elif key in self._main:
125
+ value = self._main[key]
126
+ self._main.move_to_end(key)
127
+ self._sketch.increment(key)
128
+ if value is _MISSING:
129
+ self.stats.misses += 1
130
+ return default
131
+ self.stats.hits += 1
132
+ return value # type: ignore[return-value]
133
+
134
+ def put(self, key: K, value: V) -> bool:
135
+ """Store a value after lookup and report overflow-candidate admission.
136
+
137
+ Lookup records the request in the frequency sketch. ``put`` deliberately
138
+ does not record it again, or every miss/fill would count twice and make
139
+ one-off scans look hotter than they are.
140
+ """
141
+ with self._lock:
142
+ if key in self._window:
143
+ self._window[key] = value
144
+ self._window.move_to_end(key)
145
+ return True
146
+ if key in self._main:
147
+ self._main[key] = value
148
+ self._main.move_to_end(key)
149
+ return True
150
+
151
+ self._window[key] = value
152
+ self.stats.admissions += 1
153
+ if len(self._window) <= self.window_capacity:
154
+ return True
155
+
156
+ candidate_key, candidate_value = self._window.popitem(last=False)
157
+ if self.main_capacity <= 0:
158
+ self.stats.evictions += 1
159
+ return candidate_key != key
160
+ if len(self._main) < self.main_capacity:
161
+ self._main[candidate_key] = candidate_value
162
+ return True
163
+
164
+ victim_key = next(iter(self._main))
165
+ candidate_frequency = self._sketch.estimate(candidate_key)
166
+ victim_frequency = self._sketch.estimate(victim_key)
167
+ if candidate_frequency > victim_frequency:
168
+ self._main.popitem(last=False)
169
+ self._main[candidate_key] = candidate_value
170
+ self.stats.evictions += 1
171
+ return True
172
+
173
+ self.stats.rejections += 1
174
+ self.stats.evictions += 1
175
+ return False
176
+
177
+ def snapshot(self) -> dict[str, int | float]:
178
+ with self._lock:
179
+ requests = self.stats.hits + self.stats.misses
180
+ return {
181
+ **self.stats.to_dict(),
182
+ "size": len(self),
183
+ "capacity": self.capacity,
184
+ "window_size": len(self._window),
185
+ "main_size": len(self._main),
186
+ "sketch_decays": self._sketch.decays,
187
+ "hit_ratio": round(self.stats.hits / requests, 4) if requests else 0.0,
188
+ }
189
+
190
+ def clear(self) -> None:
191
+ with self._lock:
192
+ self._window.clear()
193
+ self._main.clear()
194
+ self._sketch.clear()
195
+ self.stats = CacheAdmissionStats()
196
+
197
+ def __contains__(self, key: object) -> bool:
198
+ with self._lock:
199
+ return key in self._window or key in self._main
200
+
201
+ def __len__(self) -> int:
202
+ with self._lock:
203
+ return len(self._window) + len(self._main)
204
+
205
+ def __setitem__(self, key: K, value: V) -> None:
206
+ self.put(key, value)
207
+
208
+
209
+ __all__ = ["CacheAdmissionStats", "FrequencySketch", "WindowTinyLFUCache"]
@@ -0,0 +1,66 @@
1
+ """Stable capability bit masks (J11).
2
+
3
+ Apple's audit_class file uses stable numeric bit masks. This module gives
4
+ Algo CLI the same ABI-style capability vocabulary for tools/kernels while
5
+ preserving human-readable tier names.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from enum import IntEnum
11
+
12
+
13
+ class Capability(IntEnum):
14
+ READ = 1 << 0
15
+ WRITE = 1 << 1
16
+ SHELL = 1 << 2
17
+ NETWORK = 1 << 3
18
+ MODEL = 1 << 4
19
+ CREDENTIAL = 1 << 5
20
+ MEMORY = 1 << 6
21
+ EXTERNAL_PUBLISH = 1 << 7
22
+ DESTRUCTIVE = 1 << 8
23
+
24
+
25
+ TIER_MASKS: dict[str, int] = {
26
+ "tier0": Capability.READ.value,
27
+ "tier1": Capability.READ.value | Capability.NETWORK.value | Capability.MODEL.value,
28
+ "tier2": Capability.READ.value | Capability.WRITE.value | Capability.SHELL.value | Capability.MODEL.value | Capability.MEMORY.value,
29
+ "tier3": sum(cap.value for cap in Capability),
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class CapabilityMask:
35
+ value: int = 0
36
+
37
+ def has(self, capability: Capability) -> bool:
38
+ return bool(self.value & capability.value)
39
+
40
+ def add(self, capability: Capability) -> "CapabilityMask":
41
+ return CapabilityMask(self.value | capability.value)
42
+
43
+ def remove(self, capability: Capability) -> "CapabilityMask":
44
+ return CapabilityMask(self.value & ~capability.value)
45
+
46
+ def names(self) -> tuple[str, ...]:
47
+ return tuple(cap.name.lower() for cap in Capability if self.has(cap))
48
+
49
+ def to_dict(self) -> dict:
50
+ return {"value": self.value, "capabilities": list(self.names())}
51
+
52
+
53
+ def tier_mask(tier: str) -> int:
54
+ return TIER_MASKS.get((tier or "").strip().lower(), 0)
55
+
56
+
57
+ def mask_from_names(names: list[str] | tuple[str, ...]) -> CapabilityMask:
58
+ value = 0
59
+ for name in names or ():
60
+ key = str(name).strip().upper()
61
+ if key in Capability.__members__:
62
+ value |= Capability[key].value
63
+ return CapabilityMask(value)
64
+
65
+
66
+ __all__ = ["Capability", "CapabilityMask", "TIER_MASKS", "mask_from_names", "tier_mask"]
@@ -0,0 +1,116 @@
1
+ """Message and tool-call normalization for chat providers (Ollama / xAI shapes)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ def get_attr(obj: Any, name: str, default: Any = None) -> Any:
10
+ if isinstance(obj, dict):
11
+ return obj.get(name, default)
12
+ return getattr(obj, name, default)
13
+
14
+
15
+ def normalize_tool_call(call: Any) -> tuple[str, dict[str, Any]]:
16
+ function = get_attr(call, "function", {})
17
+ name = get_attr(function, "name", "")
18
+ args = get_attr(function, "arguments", {}) or {}
19
+ if isinstance(args, str):
20
+ try:
21
+ args = json.loads(args)
22
+ except json.JSONDecodeError:
23
+ args = {"raw": args}
24
+ if not isinstance(args, dict):
25
+ args = {"raw": args}
26
+ return name, dict(args)
27
+
28
+
29
+ def collapse_tool_history_for_gemini(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
30
+ """Rewrite assistant tool_calls + tool result messages into plain content turns.
31
+
32
+ Workaround for Ollama issue #14567 / PR #14676. Remove when the Ollama Python
33
+ SDK exposes thought_signature on tool calls.
34
+ """
35
+ out: list[dict[str, Any]] = []
36
+ i = 0
37
+ while i < len(messages):
38
+ msg = messages[i]
39
+ if msg.get("role") == "assistant" and msg.get("tool_calls"):
40
+ summary_lines: list[str] = []
41
+ if msg.get("content"):
42
+ summary_lines.append(str(msg["content"]))
43
+ for tc in msg.get("tool_calls", []):
44
+ fn = tc.get("function", {}) if isinstance(tc, dict) else {}
45
+ fn_name = fn.get("name", "?")
46
+ fn_args = fn.get("arguments", {})
47
+ try:
48
+ args_preview = json.dumps(fn_args, ensure_ascii=True, default=str)[:120]
49
+ except Exception:
50
+ args_preview = str(fn_args)[:120]
51
+ summary_lines.append(f"[Previously called {fn_name}({args_preview})]")
52
+ j = i + 1
53
+ while j < len(messages) and messages[j].get("role") == "tool":
54
+ tr = messages[j]
55
+ tool_name = tr.get("name", "?")
56
+ tool_content = str(tr.get("content", "")).strip()
57
+ preview = tool_content[:600] + ("…" if len(tool_content) > 600 else "")
58
+ summary_lines.append(f"[{tool_name} result] {preview}")
59
+ j += 1
60
+ out.append({"role": "assistant", "content": "\n".join(summary_lines)})
61
+ i = j
62
+ else:
63
+ out.append(msg)
64
+ i += 1
65
+ return out
66
+
67
+
68
+ def _coerce_arguments_dict(args: Any) -> dict[str, Any]:
69
+ """Tool-call arguments must serialize as a dict: the Ollama SDK's Message
70
+ model rejects string arguments, but OpenAI-style streams (xAI) accumulate
71
+ arguments as a JSON string."""
72
+ if isinstance(args, dict):
73
+ return args
74
+ if isinstance(args, str):
75
+ try:
76
+ parsed = json.loads(args)
77
+ except json.JSONDecodeError:
78
+ return {"raw": args}
79
+ return parsed if isinstance(parsed, dict) else {"raw": parsed}
80
+ return {}
81
+
82
+
83
+ def serialize_tool_call(call: Any) -> dict[str, Any]:
84
+ if isinstance(call, dict):
85
+ out_dict: dict[str, Any] = json.loads(json.dumps(call, ensure_ascii=False, default=str))
86
+ fn = out_dict.get("function")
87
+ if isinstance(fn, dict):
88
+ fn["arguments"] = _coerce_arguments_dict(fn.get("arguments"))
89
+ return out_dict
90
+ function = get_attr(call, "function", {})
91
+ name = get_attr(function, "name", "")
92
+ args_payload = _coerce_arguments_dict(get_attr(function, "arguments", {}) or {})
93
+ out: dict[str, Any] = {"function": {"name": name, "arguments": args_payload}}
94
+ call_id = get_attr(call, "id", None)
95
+ if call_id:
96
+ out["id"] = call_id
97
+ call_type = get_attr(call, "type", None)
98
+ if call_type:
99
+ out["type"] = call_type
100
+ for sig_key in ("thought_signature", "thoughtSignature"):
101
+ sig = get_attr(call, sig_key, None) or get_attr(function, sig_key, None)
102
+ if sig:
103
+ out[sig_key] = sig
104
+ break
105
+ return out
106
+
107
+
108
+ def normalize_message(message: Any) -> dict[str, Any]:
109
+ if isinstance(message, dict):
110
+ return message
111
+ out: dict[str, Any] = {}
112
+ for key in ("role", "content", "thinking", "tool_calls"):
113
+ value = getattr(message, key, None)
114
+ if value:
115
+ out[key] = value
116
+ return out