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,241 @@
1
+ """Tree-of-Thoughts (ToT) Reasoning.
2
+
3
+ Explores multiple reasoning paths as a tree with:
4
+ - BFS: Breadth-first exploration of K best thoughts per level
5
+ - DFS: Depth-first with backtracking on dead ends
6
+ - MCTS: Monte Carlo Tree Search integration (delegates to mcts.py)
7
+
8
+ Each thought is evaluated by a value function (LLM-based or heuristic).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+ from typing import Any
18
+
19
+ from ..chat_protocol import get_attr
20
+
21
+
22
+ class SearchStrategy(Enum):
23
+ BFS = "bfs"
24
+ DFS = "dfs"
25
+
26
+
27
+ @dataclass
28
+ class ThoughtNode:
29
+ """A node in the Tree-of-Thought."""
30
+ thought: str
31
+ value: float # 0.0-1.0 evaluation score
32
+ depth: int
33
+ children: list["ThoughtNode"] = field(default_factory=list)
34
+ parent: "ThoughtNode | None" = field(default=None, repr=False)
35
+ state: str = "" # accumulated reasoning state
36
+ visited: bool = False
37
+ pruned: bool = False
38
+ timestamp: float = field(default_factory=time.time)
39
+
40
+ @property
41
+ def is_leaf(self) -> bool:
42
+ return len(self.children) == 0
43
+
44
+ @property
45
+ def path_from_root(self) -> list["ThoughtNode"]:
46
+ """Walk up to root, return path from root to this node."""
47
+ path = []
48
+ node: ThoughtNode | None = self
49
+ while node is not None:
50
+ path.append(node)
51
+ node = node.parent
52
+ path.reverse()
53
+ return path
54
+
55
+ def reasoning_chain(self) -> str:
56
+ """Human-readable chain from root to this node."""
57
+ nodes = self.path_from_root
58
+ return "\n".join(f"Step {i+1} (val={n.value:.2f}): {n.thought}" for i, n in enumerate(nodes))
59
+
60
+
61
+ EVALUATION_PROMPT = """Evaluate this reasoning step on a scale of 0.0 to 1.0.
62
+
63
+ Consider:
64
+ - Logical soundness: Is this step valid reasoning?
65
+ - Progress: Does it advance toward solving the task?
66
+ - Novelty: Does it add new information or just restate?
67
+
68
+ Respond with just a number between 0.0 and 1.0."""
69
+
70
+ GENERATION_PROMPT = """Given the current reasoning state, generate {n} distinct next thoughts.
71
+
72
+ Current state:
73
+ {state}
74
+
75
+ Task: {task}
76
+
77
+ Generate {n} different reasoning steps. Each should pursue a different approach.
78
+ Format as JSON array: [{{"thought": "...", "state": "updated reasoning state"}}]"""
79
+
80
+
81
+ @dataclass
82
+ class TreeOfThought:
83
+ """Tree-of-Thought reasoner."""
84
+ strategy: SearchStrategy = SearchStrategy.BFS
85
+ max_depth: int = 4
86
+ branch_factor: int = 3
87
+ value_threshold: float = 0.3 # Prune nodes below this score
88
+
89
+ root: ThoughtNode | None = None
90
+ best_leaf: ThoughtNode | None = None
91
+ best_score: float = 0.0
92
+ nodes_explored: int = 0
93
+
94
+ def _evaluate(self, thought: str, client: Any, model: str) -> float:
95
+ """Use LLM to evaluate a thought's quality."""
96
+ messages = [
97
+ {"role": "system", "content": EVALUATION_PROMPT},
98
+ {"role": "user", "content": f"Reasoning step: {thought}\n\nRate this step."},
99
+ ]
100
+ try:
101
+ response = client.chat(model=model, messages=messages, stream=False)
102
+ text = get_attr(get_attr(response, "message", {}), "content", "").strip()
103
+ # Parse score
104
+ import re
105
+ m = re.search(r"([0-9]*\.?[0-9]+)", text)
106
+ if m:
107
+ return max(0.0, min(1.0, float(m.group(1))))
108
+ except Exception:
109
+ pass
110
+ return 0.5
111
+
112
+ def _generate_thoughts(
113
+ self, task: str, state: str, n: int, client: Any, model: str,
114
+ ) -> list[tuple[str, str]]:
115
+ """Generate n candidate next thoughts using LLM."""
116
+ prompt = GENERATION_PROMPT.format(n=n, state=state[:2000], task=task)
117
+ messages = [
118
+ {"role": "system", "content": "You are a reasoning step generator. Be diverse and creative."},
119
+ {"role": "user", "content": prompt},
120
+ ]
121
+ try:
122
+ response = client.chat(model=model, messages=messages, stream=False, format="json")
123
+ text = get_attr(get_attr(response, "message", {}), "content", "")
124
+ parsed = json.loads(text)
125
+ if isinstance(parsed, list):
126
+ results = []
127
+ for item in parsed[:n]:
128
+ if isinstance(item, dict):
129
+ results.append((str(item.get("thought", "")), str(item.get("state", state))))
130
+ return results
131
+ except (json.JSONDecodeError, ValueError, TypeError):
132
+ pass
133
+ # Fallback: single thought
134
+ return [(state + " (continue reasoning)", state)]
135
+
136
+ def _bfs(self, task: str, client: Any, model: str) -> ThoughtNode | None:
137
+ """BFS search: expand K best nodes at each depth level."""
138
+ if not self.root:
139
+ return None
140
+ current_level = [self.root]
141
+ for depth in range(self.max_depth):
142
+ if not current_level:
143
+ break
144
+ next_level: list[ThoughtNode] = []
145
+ for node in current_level:
146
+ if node.pruned:
147
+ continue
148
+ candidates = self._generate_thoughts(
149
+ task, node.state, self.branch_factor, client, model,
150
+ )
151
+ for thought, state in candidates:
152
+ value = self._evaluate(thought, client, model)
153
+ child = ThoughtNode(
154
+ thought=thought, value=value, depth=depth + 1,
155
+ parent=node, state=state,
156
+ )
157
+ node.children.append(child)
158
+ self.nodes_explored += 1
159
+ if value >= self.value_threshold:
160
+ next_level.append(child)
161
+ else:
162
+ child.pruned = True
163
+ if value > self.best_score:
164
+ self.best_score = value
165
+ self.best_leaf = child
166
+ # Keep top-K for next level
167
+ next_level.sort(key=lambda n: n.value, reverse=True)
168
+ current_level = next_level[:self.branch_factor]
169
+ return self.best_leaf
170
+
171
+ def _dfs(self, task: str, client: Any, model: str) -> ThoughtNode | None:
172
+ """DFS with backtracking: explore promising paths first."""
173
+ if not self.root:
174
+ return None
175
+ stack = [self.root]
176
+ while stack and self.nodes_explored < self.max_depth * self.branch_factor * 2:
177
+ node = stack.pop()
178
+ if node.depth >= self.max_depth or node.pruned:
179
+ continue
180
+ candidates = self._generate_thoughts(
181
+ task, node.state, self.branch_factor, client, model,
182
+ )
183
+ children = []
184
+ for thought, state in candidates:
185
+ value = self._evaluate(thought, client, model)
186
+ child = ThoughtNode(
187
+ thought=thought, value=value, depth=node.depth + 1,
188
+ parent=node, state=state,
189
+ )
190
+ node.children.append(child)
191
+ self.nodes_explored += 1
192
+ if value > self.best_score:
193
+ self.best_score = value
194
+ self.best_leaf = child
195
+ if value >= self.value_threshold:
196
+ children.append(child)
197
+ # Push best children first for DFS
198
+ children.sort(key=lambda n: n.value, reverse=True)
199
+ stack.extend(children)
200
+ return self.best_leaf
201
+
202
+
203
+ def run_tot(
204
+ *,
205
+ task: str,
206
+ client: Any,
207
+ model: str,
208
+ strategy: str = "bfs",
209
+ max_depth: int = 4,
210
+ branch_factor: int = 3,
211
+ value_threshold: float = 0.3,
212
+ initial_state: str = "",
213
+ ) -> TreeOfThought:
214
+ """Run a Tree-of-Thought search.
215
+
216
+ Args:
217
+ task: The reasoning task.
218
+ client: Ollama client.
219
+ model: Model name for evaluation and generation.
220
+ strategy: "bfs" or "dfs".
221
+ max_depth: Maximum tree depth.
222
+ branch_factor: Number of candidate thoughts per node.
223
+ value_threshold: Minimum score to keep a thought.
224
+ initial_state: Starting reasoning state.
225
+
226
+ Returns:
227
+ The TreeOfThought with best_leaf set to the highest-scoring path.
228
+ """
229
+ strat = SearchStrategy(strategy.lower())
230
+ tot = TreeOfThought(
231
+ strategy=strat,
232
+ max_depth=max_depth,
233
+ branch_factor=branch_factor,
234
+ value_threshold=value_threshold,
235
+ )
236
+ tot.root = ThoughtNode(thought="(root)", value=1.0, depth=0, state=initial_state or task)
237
+ if strat == SearchStrategy.BFS:
238
+ tot._bfs(task, client, model)
239
+ else:
240
+ tot._dfs(task, client, model)
241
+ return tot
@@ -0,0 +1,150 @@
1
+ """Bridge the reasoning/* algorithms into the everyday chat loop.
2
+
3
+ Historically the Tree/Graph/MCTS/Reflexion/QCR algorithms were only reachable
4
+ from the agent *pipeline*; the chat loop the user lives in was plain ReAct, so
5
+ ``/reason tot`` (etc.) changed a flag nothing in chat read. This module runs a
6
+ "reasoning preflight": when reasoning chat mode is enabled and the active
7
+ ``reasoning_mode`` is not plain react, it runs the selected algorithm to produce
8
+ a short strategy artifact and returns it as a guidance block. The chat loop
9
+ injects that block into the turn (same pattern as RAG/intuition/ICL injection)
10
+ so the model executes against an algorithm-derived plan.
11
+
12
+ Design constraints:
13
+ - Best-effort: any failure returns "" and the turn proceeds as plain ReAct.
14
+ - These algorithms make several model calls, so the preflight is gated on the
15
+ user explicitly opting in (reasoning_chat_enabled) AND choosing a non-react
16
+ mode. It produces a *plan*, not tool execution — the main loop still owns
17
+ tool calls, approvals, and safety.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from typing import Any
24
+
25
+ from .config import Config
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ _PLAN_HEADER = "## Reasoning Plan ({mode})"
30
+ _PLAN_FOOTER = (
31
+ "\nUse the plan above as strategy guidance only. Verify with tools before "
32
+ "acting; it is not proof of any fact."
33
+ )
34
+ _MAX_PLAN_CHARS = 2000
35
+
36
+
37
+ def _safe_chain(obj: Any, *attrs: str) -> str:
38
+ """Pull the first available human-readable chain/text off a result object."""
39
+ for attr in attrs:
40
+ value = getattr(obj, attr, None)
41
+ if callable(value):
42
+ try:
43
+ value = value()
44
+ except Exception:
45
+ continue
46
+ if isinstance(value, str) and value.strip():
47
+ return value.strip()
48
+ # node-like with reasoning_chain()
49
+ chain = getattr(value, "reasoning_chain", None)
50
+ if callable(chain):
51
+ try:
52
+ text = chain()
53
+ if isinstance(text, str) and text.strip():
54
+ return text.strip()
55
+ except Exception:
56
+ continue
57
+ return ""
58
+
59
+
60
+ def _plan_for_mode(mode: str, task: str, client: Any, model: str, cfg: Config) -> str:
61
+ from . import reasoning
62
+
63
+ depth = max(1, int(getattr(cfg, "reasoning_depth", 4)))
64
+ branches = max(1, int(getattr(cfg, "reasoning_branches", 3)))
65
+
66
+ if mode == "reflexion":
67
+ episodes = reasoning.run_reflexion_loop(
68
+ task=task, client=client, model=model,
69
+ max_attempts=max(1, int(getattr(cfg, "reasoning_reflexion_attempts", 3))),
70
+ )
71
+ if episodes:
72
+ best_episode = max(episodes, key=lambda episode: getattr(episode, "score", 0.0))
73
+ return (
74
+ f"Best attempt (score {getattr(best_episode, 'score', 0.0):.2f}):\n"
75
+ f"{getattr(best_episode, 'output', '')}"
76
+ )
77
+ return ""
78
+
79
+ if mode == "tot":
80
+ tot = reasoning.run_tot(
81
+ task=task, client=client, model=model,
82
+ max_depth=depth, branch_factor=branches,
83
+ )
84
+ return _safe_chain(tot, "best_leaf")
85
+
86
+ if mode == "got":
87
+ got = reasoning.run_got(
88
+ task=task, client=client, model=model,
89
+ max_rounds=depth, branch_factor=branches,
90
+ )
91
+ return _safe_chain(got, "best_vertex", "best_leaf")
92
+
93
+ if mode == "mcts":
94
+ reasoner = reasoning.run_mcts(
95
+ task=task, client=client, model=model,
96
+ max_depth=depth, branch_factor=branches,
97
+ )
98
+ # MCTSReasoner exposes root; best leaf chain is the robust conclusion.
99
+ root = getattr(reasoner, "root", None)
100
+ if root is not None and hasattr(root, "best_leaf"):
101
+ try:
102
+ return root.best_leaf().reasoning_chain()
103
+ except Exception:
104
+ pass
105
+ return _safe_chain(reasoner, "best_leaf")
106
+
107
+ if mode == "qcr":
108
+ result = reasoning.run_qcr_aggregation(
109
+ task=task, client=client, model=model,
110
+ n_samples=max(1, int(getattr(cfg, "reasoning_qcr_samples", 5))),
111
+ )
112
+ # returns (best, candidates, meta)
113
+ if isinstance(result, tuple) and result:
114
+ best = result[0]
115
+ return str(best) if best else ""
116
+ return ""
117
+
118
+ # neuro_symbolic / hybrid: no generic symbolic verifier to supply here, so
119
+ # we skip rather than fabricate one. The pipeline path still supports them.
120
+ return ""
121
+
122
+
123
+ def maybe_reasoning_plan(cfg: Config, client: Any, user_message: str) -> str:
124
+ """Return a guidance block for the active reasoning mode, or "" to skip.
125
+
126
+ Skips when: chat reasoning is disabled, mode is react/neuro_symbolic/hybrid,
127
+ the message is trivial, or the algorithm errors/returns nothing.
128
+ """
129
+ if not getattr(cfg, "reasoning_chat_enabled", False):
130
+ return ""
131
+ mode = str(getattr(cfg, "reasoning_mode", "react") or "react").lower()
132
+ if mode in {"react", "neuro_symbolic", "hybrid"}:
133
+ return ""
134
+ task = (user_message or "").strip()
135
+ if len(task) < 12: # trivial prompt; not worth a multi-call preflight
136
+ return ""
137
+ model = str(getattr(cfg, "model", "") or "")
138
+ if not model:
139
+ return ""
140
+ try:
141
+ plan = _plan_for_mode(mode, task, client, model, cfg)
142
+ except Exception as exc:
143
+ logger.debug("Reasoning preflight (%s) failed: %s", mode, exc)
144
+ return ""
145
+ plan = (plan or "").strip()
146
+ if not plan:
147
+ return ""
148
+ if len(plan) > _MAX_PLAN_CHARS:
149
+ plan = plan[:_MAX_PLAN_CHARS].rstrip() + "…"
150
+ return f"{_PLAN_HEADER.format(mode=mode)}\n{plan}{_PLAN_FOOTER}"
@@ -0,0 +1,284 @@
1
+ """Task-local reasoning guidance for conflicting structured sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from collections import deque
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .chat_protocol import normalize_tool_call
13
+
14
+
15
+ _AUTHORITY_CUES = ("authoritative", "source of truth", "live files", "live manifest")
16
+ _STALE_CUES = ("stale", "retrieved context", "retrieved memory", "rag", "lower authority")
17
+ _STRUCTURED_CUES = ("settings", "config", "manifest", "json", "yaml", "structured")
18
+ _RECONCILIATION_MESSAGE_WINDOW = 128
19
+
20
+
21
+ def guidance_for_prompt(prompt: str) -> str | None:
22
+ """Return a compact reconciliation algorithm only for clear conflict tasks."""
23
+
24
+ lowered = (prompt or "").casefold()
25
+ if not all(
26
+ any(cue in lowered for cue in family)
27
+ for family in (_AUTHORITY_CUES, _STALE_CUES, _STRUCTURED_CUES)
28
+ ):
29
+ return None
30
+ return (
31
+ "Use provenance-aware structured reconciliation:\n"
32
+ "1. Establish source priority from the task and inspect the authoritative, target, and lower-authority sources.\n"
33
+ "2. Preserve the target schema. Trace each existing target value to matching facts in the stale and authoritative sources; key names may differ across schemas.\n"
34
+ "3. Replace every stale semantic slot with its authoritative counterpart. Do not leave the stale value in place or merely append a source-named duplicate.\n"
35
+ "4. Turn every required field, objective assertion, and artifact-content requirement into a checklist. Confirm each exact authoritative value appears where required, then run fail-on-mismatch verification."
36
+ "\n5. Treat negative artifact requirements strictly: if stale values must not be included or presented as facts, omit the literal stale values even from comparison sections; describe the overridden categories without quoting obsolete values."
37
+ )
38
+
39
+
40
+ def _normalized_field(value: str) -> str:
41
+ return re.sub(r"[^a-z0-9]+", "_", value.casefold()).strip("_")
42
+
43
+
44
+ def _read_records(messages: list[dict[str, Any]]) -> list[tuple[str, str]]:
45
+ """Pair read results with their model-requested paths."""
46
+
47
+ pending: deque[tuple[str, str | None]] = deque()
48
+ records: list[tuple[str, str]] = []
49
+ for message in messages[-_RECONCILIATION_MESSAGE_WINDOW:]:
50
+ if message.get("role") == "assistant":
51
+ for call in message.get("tool_calls") or ():
52
+ name, args = normalize_tool_call(call)
53
+ if name == "read_file":
54
+ pending.append((str(args.get("path") or ""), str(call.get("id") or "") or None))
55
+ continue
56
+ if message.get("role") != "tool" or message.get("name") != "read_file":
57
+ continue
58
+ call_id = str(message.get("tool_call_id") or "") or None
59
+ path = ""
60
+ if call_id:
61
+ for index, (candidate_path, candidate_id) in enumerate(pending):
62
+ if candidate_id == call_id:
63
+ path = candidate_path
64
+ del pending[index]
65
+ break
66
+ elif pending:
67
+ path, _candidate_id = pending.popleft()
68
+ records.append((path, str(message.get("content") or "")))
69
+ return records
70
+
71
+
72
+ def _json_objects(messages: list[dict[str, Any]]) -> list[tuple[str, dict[str, Any]]]:
73
+ objects: list[tuple[str, dict[str, Any]]] = []
74
+ for path, content in _read_records(messages):
75
+ try:
76
+ value = json.loads(content)
77
+ except (TypeError, json.JSONDecodeError):
78
+ continue
79
+ if isinstance(value, dict):
80
+ objects.append((path, value))
81
+ return objects
82
+
83
+
84
+ def _stale_label(text: str, value: str) -> str | None:
85
+ pattern = re.compile(
86
+ rf"([A-Za-z][A-Za-z _/-]{{1,40}}?)\s+[`\"']?{re.escape(value)}(?:[`\"']|\b)",
87
+ re.IGNORECASE,
88
+ )
89
+ matches = list(pattern.finditer(text))
90
+ if not matches:
91
+ return None
92
+ label = matches[-1].group(1).strip().rsplit(" and ", 1)[-1]
93
+ return _normalized_field(label)
94
+
95
+
96
+ def _lineage_replacements(
97
+ messages: list[dict[str, Any]],
98
+ stale_text: str,
99
+ ) -> list[tuple[str, str, str, str, str]]:
100
+ objects = _json_objects(messages)
101
+ authoritative = next(
102
+ (
103
+ (path, obj)
104
+ for path, obj in objects
105
+ if any(
106
+ isinstance(value, str)
107
+ and ("source of truth" in value.casefold() or "authoritative" in value.casefold())
108
+ for value in obj.values()
109
+ )
110
+ ),
111
+ None,
112
+ )
113
+ if authoritative is None:
114
+ return []
115
+ _authoritative_path, authoritative_object = authoritative
116
+ authority_by_field = {
117
+ _normalized_field(str(key)): value for key, value in authoritative_object.items()
118
+ }
119
+ replacements: list[tuple[str, str, str, str, str]] = []
120
+ for target_path, target in objects:
121
+ if target is authoritative_object:
122
+ continue
123
+ for target_key, old_value in target.items():
124
+ if not isinstance(old_value, str) or old_value not in stale_text:
125
+ continue
126
+ label = _stale_label(stale_text, old_value)
127
+ if not label:
128
+ continue
129
+ authority_key = next(
130
+ (
131
+ key
132
+ for key in authority_by_field
133
+ if key == label or key.endswith(f"_{label}") or label.endswith(f"_{key}")
134
+ ),
135
+ None,
136
+ )
137
+ if authority_key is None:
138
+ continue
139
+ new_value = authority_by_field[authority_key]
140
+ if isinstance(new_value, str) and new_value != old_value:
141
+ replacements.append(
142
+ (target_path, str(target_key), old_value, authority_key, new_value)
143
+ )
144
+ return list(dict.fromkeys(replacements))
145
+
146
+
147
+ def lineage_guidance(messages: list[dict[str, Any]], stale_text: str) -> str | None:
148
+ """Infer cross-schema replacements by tracing target values to stale labels."""
149
+
150
+ replacements = _lineage_replacements(messages, stale_text)
151
+ if not replacements:
152
+ return None
153
+ lines = [
154
+ f"- Keep target key `{target_key}` and set its value to authoritative `{new_value}` "
155
+ f"because its current `{old_value}` value matches the stale `{authority_key}` fact. "
156
+ f"Do not remove or rename `{target_key}`, and do not add `{authority_key}` as a duplicate."
157
+ for _target_path, target_key, old_value, authority_key, new_value in replacements
158
+ ]
159
+ return (
160
+ "[Algo provenance inference]\n"
161
+ "Cross-schema value lineage found:\n"
162
+ + "\n".join(lines)
163
+ + "\nDo not retain those stale target values or add duplicate source-named keys."
164
+ )
165
+
166
+
167
+ def lineage_constraints(
168
+ messages: list[dict[str, Any]],
169
+ ) -> list[tuple[str, str, str, str, str]]:
170
+ """Recover unique provenance constraints from recent read results."""
171
+
172
+ constraints: list[tuple[str, str, str, str, str]] = []
173
+ for _path, text in _read_records(messages):
174
+ constraints.extend(_lineage_replacements(messages, text))
175
+ return list(dict.fromkeys(constraints))
176
+
177
+
178
+ _STALE_VALUE_PATTERNS = tuple(
179
+ re.compile(pattern, re.IGNORECASE)
180
+ for pattern in (
181
+ r"\bproject(?:\s+name)?(?:\s+is)?\s+([^,.\n]+)",
182
+ r"\bapproval\s+ticket\s+([^,.\n]+)",
183
+ r"\bstatus\s+endpoint\s+([^,.\n]+)",
184
+ r"\bfeature\s+flag\s+([^,.\n]+)",
185
+ r"\boperations\s+contact\s+([^,.\n]+)",
186
+ r"\bgo-live\s+date(?:/window)?\s+([^,.\n]+)",
187
+ )
188
+ )
189
+
190
+
191
+ def _stale_fact_values(messages: list[dict[str, Any]]) -> list[str]:
192
+ values: list[str] = []
193
+ for _path, text in _read_records(messages):
194
+ source_text = text.split("\n\n[Algo ", 1)[0]
195
+ lowered = source_text.casefold()
196
+ if (
197
+ "stale" not in lowered
198
+ or "claim" not in lowered
199
+ or not any(cue in lowered for cue in ("rag", "cache", "lower authority"))
200
+ ):
201
+ continue
202
+ for pattern in _STALE_VALUE_PATTERNS:
203
+ for match in pattern.finditer(source_text):
204
+ value = match.group(1).strip().strip(".`'\"")
205
+ if value:
206
+ values.append(value)
207
+ return list(dict.fromkeys(values))
208
+
209
+
210
+ def structured_write_violation(
211
+ name: str,
212
+ args: dict[str, Any],
213
+ messages: list[dict[str, Any]],
214
+ ) -> str | None:
215
+ """Reject full-object JSON writes that violate inferred target-schema lineage."""
216
+
217
+ if name not in {"write_file", "edit_file"}:
218
+ return None
219
+ raw = args.get("content") if name == "write_file" else args.get("new_string")
220
+ write_path = str(args.get("path") or "")
221
+ write_name = Path(write_path).name.casefold()
222
+ if name == "write_file" and write_name:
223
+ explicit_omission = any(
224
+ "do not include stale" in str(message.get("content") or "").casefold()
225
+ and write_name in str(message.get("content") or "").casefold()
226
+ for message in messages[-_RECONCILIATION_MESSAGE_WINDOW:]
227
+ )
228
+ if explicit_omission:
229
+ stale_present = [value for value in _stale_fact_values(messages) if value in str(raw or "")]
230
+ if stale_present:
231
+ return (
232
+ "Artifact omission invariant blocked this write: the task says not to include stale "
233
+ "values in the summary. Omit the literal stale values entirely, including in comparison "
234
+ f"or 'not facts' sections. Present: {', '.join(stale_present[:6])}. Describe the "
235
+ "overridden categories generically and include only authoritative values."
236
+ )
237
+ try:
238
+ candidate = json.loads(str(raw or ""))
239
+ except json.JSONDecodeError:
240
+ return None
241
+ if not isinstance(candidate, dict):
242
+ return None
243
+ normalized_write_path = os.path.normcase(os.path.normpath(write_path))
244
+ for target_path, target_key, old_value, authority_key, new_value in lineage_constraints(messages):
245
+ if not target_path or os.path.normcase(os.path.normpath(target_path)) != normalized_write_path:
246
+ continue
247
+ if candidate.get(target_key) != new_value:
248
+ return (
249
+ "Provenance invariant blocked this structured write: preserve target schema by "
250
+ f"keeping `{target_key}` and setting its value to `{new_value}`. Do not remove or "
251
+ f"rename `{target_key}`, and do not substitute a new `{authority_key}` key."
252
+ )
253
+ return None
254
+
255
+
256
+ def augment_read_result(
257
+ name: str,
258
+ result: str,
259
+ *,
260
+ messages: list[dict[str, Any]] | None = None,
261
+ ) -> str:
262
+ """Attach guidance when a newly read task/file reveals a source conflict."""
263
+
264
+ if name != "read_file":
265
+ return result
266
+ additions = []
267
+ guidance = guidance_for_prompt(result)
268
+ if guidance is not None:
269
+ additions.append(f"[Algo reasoning strategy]\n{guidance}")
270
+ lineage = lineage_guidance(messages or [], result)
271
+ if lineage is not None:
272
+ additions.append(lineage)
273
+ if not additions:
274
+ return result
275
+ return f"{result}\n\n" + "\n\n".join(additions)
276
+
277
+
278
+ __all__ = [
279
+ "augment_read_result",
280
+ "guidance_for_prompt",
281
+ "lineage_constraints",
282
+ "lineage_guidance",
283
+ "structured_write_violation",
284
+ ]