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.
- algo_cli/__init__.py +3 -0
- algo_cli/__main__.py +7 -0
- algo_cli/_internal/__init__.py +12 -0
- algo_cli/_internal/policy_chain.py +259 -0
- algo_cli/action_registry.py +1047 -0
- algo_cli/agent_blocks.py +550 -0
- algo_cli/agent_pipeline.py +1457 -0
- algo_cli/agent_threads.py +308 -0
- algo_cli/animations.py +316 -0
- algo_cli/cache_admission.py +209 -0
- algo_cli/capability_mask.py +66 -0
- algo_cli/chat_protocol.py +116 -0
- algo_cli/chatgpt_auth.py +510 -0
- algo_cli/chatgpt_client.py +657 -0
- algo_cli/code_rag.py +479 -0
- algo_cli/config.py +651 -0
- algo_cli/context_budget.py +679 -0
- algo_cli/credential_helpers.py +315 -0
- algo_cli/deliberation.py +29 -0
- algo_cli/display.py +1470 -0
- algo_cli/evals/__init__.py +21 -0
- algo_cli/evals/algorithm_effectiveness.py +560 -0
- algo_cli/evals/competitive_harness_rating.py +702 -0
- algo_cli/evals/cot_quality.py +220 -0
- algo_cli/evals/harness_retrieval_benchmark.py +401 -0
- algo_cli/evals/performance_regression.py +136 -0
- algo_cli/evals/scorecard_grading.py +308 -0
- algo_cli/evals/session_distribution.py +84 -0
- algo_cli/execution_guardrails.py +806 -0
- algo_cli/extensions_manifest.py +84 -0
- algo_cli/git_evidence.py +227 -0
- algo_cli/google_workspace.py +407 -0
- algo_cli/google_workspace_auth.py +523 -0
- algo_cli/harness.py +2587 -0
- algo_cli/identity.py +557 -0
- algo_cli/index_compute_lab.py +228 -0
- algo_cli/inference_harness.py +70 -0
- algo_cli/intelligence/__init__.py +1103 -0
- algo_cli/intelligence/acrobat_config.py +307 -0
- algo_cli/intelligence/acrobat_manifests.py +338 -0
- algo_cli/intelligence/acrobat_models.py +195 -0
- algo_cli/intelligence/acrobat_pipeline.py +295 -0
- algo_cli/intelligence/acrobat_runtime.py +302 -0
- algo_cli/intelligence/acrobat_security.py +261 -0
- algo_cli/intelligence/acrobat_workflows.py +226 -0
- algo_cli/intelligence/actionability.py +165 -0
- algo_cli/intelligence/adversarial_audit.py +136 -0
- algo_cli/intelligence/agent_arena.py +92 -0
- algo_cli/intelligence/agent_benchmark.py +236 -0
- algo_cli/intelligence/agent_runtime.py +171 -0
- algo_cli/intelligence/agents_as_tools.py +70 -0
- algo_cli/intelligence/artifact_binding.py +80 -0
- algo_cli/intelligence/autonomous_engineer.py +1976 -0
- algo_cli/intelligence/backpressure.py +99 -0
- algo_cli/intelligence/bloom_filter.py +186 -0
- algo_cli/intelligence/bonferroni.py +66 -0
- algo_cli/intelligence/boundary_compaction.py +98 -0
- algo_cli/intelligence/catalog_verifier.py +172 -0
- algo_cli/intelligence/cavecrew.py +118 -0
- algo_cli/intelligence/changelog.py +176 -0
- algo_cli/intelligence/checkpoint_resume.py +92 -0
- algo_cli/intelligence/circuit_breaker.py +88 -0
- algo_cli/intelligence/clarification_gate.py +101 -0
- algo_cli/intelligence/code_graph.py +180 -0
- algo_cli/intelligence/coderank.py +97 -0
- algo_cli/intelligence/consistent_hash.py +150 -0
- algo_cli/intelligence/consortium_synthesis.py +139 -0
- algo_cli/intelligence/construction/__init__.py +241 -0
- algo_cli/intelligence/construction/common.py +273 -0
- algo_cli/intelligence/construction/documents.py +496 -0
- algo_cli/intelligence/construction/labor_units.py +1395 -0
- algo_cli/intelligence/construction/payments.py +470 -0
- algo_cli/intelligence/construction/risk.py +784 -0
- algo_cli/intelligence/content_extractor.py +132 -0
- algo_cli/intelligence/context_adaptive.py +102 -0
- algo_cli/intelligence/context_ops.py +95 -0
- algo_cli/intelligence/count_min.py +145 -0
- algo_cli/intelligence/cow_state.py +103 -0
- algo_cli/intelligence/critic_loop.py +119 -0
- algo_cli/intelligence/cross_source.py +113 -0
- algo_cli/intelligence/daemon_mode.py +99 -0
- algo_cli/intelligence/dag_orchestration.py +151 -0
- algo_cli/intelligence/deep_research.py +155 -0
- algo_cli/intelligence/degenerate_detector.py +78 -0
- algo_cli/intelligence/delta_report.py +92 -0
- algo_cli/intelligence/discovery_event_log.py +92 -0
- algo_cli/intelligence/document_ingest.py +298 -0
- algo_cli/intelligence/dual_layer_validate.py +151 -0
- algo_cli/intelligence/echo_fidelity.py +73 -0
- algo_cli/intelligence/ema_tuning.py +104 -0
- algo_cli/intelligence/event_log.py +92 -0
- algo_cli/intelligence/evidence_graph.py +114 -0
- algo_cli/intelligence/extension_host.py +162 -0
- algo_cli/intelligence/extension_manifest.py +115 -0
- algo_cli/intelligence/falsification_suite.py +178 -0
- algo_cli/intelligence/finance/__init__.py +169 -0
- algo_cli/intelligence/finance/anomalies.py +135 -0
- algo_cli/intelligence/finance/ap_ar.py +351 -0
- algo_cli/intelligence/finance/cash.py +162 -0
- algo_cli/intelligence/finance/close.py +332 -0
- algo_cli/intelligence/finance/common.py +244 -0
- algo_cli/intelligence/finance/construction.py +135 -0
- algo_cli/intelligence/finance/controls.py +172 -0
- algo_cli/intelligence/finance/evidence.py +119 -0
- algo_cli/intelligence/finance/exceptions.py +157 -0
- algo_cli/intelligence/finance/reconciliations.py +254 -0
- algo_cli/intelligence/finance/revenue.py +109 -0
- algo_cli/intelligence/finance/tax.py +74 -0
- algo_cli/intelligence/finance/workpapers.py +111 -0
- algo_cli/intelligence/finding_record.py +120 -0
- algo_cli/intelligence/flow_dag.py +267 -0
- algo_cli/intelligence/gatherer.py +223 -0
- algo_cli/intelligence/golden_master.py +98 -0
- algo_cli/intelligence/graph_rag.py +195 -0
- algo_cli/intelligence/group_chat.py +143 -0
- algo_cli/intelligence/hash_dedup.py +145 -0
- algo_cli/intelligence/hyperloglog.py +128 -0
- algo_cli/intelligence/incremental_index.py +316 -0
- algo_cli/intelligence/index_store.py +16 -0
- algo_cli/intelligence/iteration_plan.py +133 -0
- algo_cli/intelligence/kernel_plugins.py +167 -0
- algo_cli/intelligence/lesson_catalog.py +135 -0
- algo_cli/intelligence/llm_fallback.py +169 -0
- algo_cli/intelligence/log2_histogram.py +267 -0
- algo_cli/intelligence/lsp_integration.py +147 -0
- algo_cli/intelligence/memory_evolution.py +117 -0
- algo_cli/intelligence/minhash_lsh.py +182 -0
- algo_cli/intelligence/multi_model_score.py +174 -0
- algo_cli/intelligence/multi_tier_grade.py +211 -0
- algo_cli/intelligence/negative_controls.py +113 -0
- algo_cli/intelligence/numeric_clamp.py +63 -0
- algo_cli/intelligence/occ_editor.py +66 -0
- algo_cli/intelligence/output_normalize.py +112 -0
- algo_cli/intelligence/parallel_delegation.py +98 -0
- algo_cli/intelligence/parallel_fanout.py +104 -0
- algo_cli/intelligence/permission_modes.py +105 -0
- algo_cli/intelligence/pre_push_gate.py +68 -0
- algo_cli/intelligence/prefetch.py +171 -0
- algo_cli/intelligence/process_framework.py +217 -0
- algo_cli/intelligence/project_graph.py +387 -0
- algo_cli/intelligence/query_expansion.py +146 -0
- algo_cli/intelligence/ralph_loop.py +117 -0
- algo_cli/intelligence/rate_limiter.py +153 -0
- algo_cli/intelligence/refactor_transaction.py +94 -0
- algo_cli/intelligence/research_workspace.py +108 -0
- algo_cli/intelligence/retraction_ledger.py +72 -0
- algo_cli/intelligence/saga_pattern.py +88 -0
- algo_cli/intelligence/session_fork.py +100 -0
- algo_cli/intelligence/shadow_editor.py +67 -0
- algo_cli/intelligence/shell_session.py +213 -0
- algo_cli/intelligence/source_registry.py +143 -0
- algo_cli/intelligence/spawn_scales.py +99 -0
- algo_cli/intelligence/stat_stability.py +104 -0
- algo_cli/intelligence/structural_validator.py +148 -0
- algo_cli/intelligence/subagent_spawner.py +111 -0
- algo_cli/intelligence/symmetric_verify.py +70 -0
- algo_cli/intelligence/task_classifier.py +129 -0
- algo_cli/intelligence/team_execution.py +122 -0
- algo_cli/intelligence/tiered_access.py +121 -0
- algo_cli/intelligence/utility_registry.py +159 -0
- algo_cli/intuition_engine.py +560 -0
- algo_cli/intuition_injector.py +82 -0
- algo_cli/kernels/__init__.py +5 -0
- algo_cli/kernels/manifest.py +763 -0
- algo_cli/main.py +3903 -0
- algo_cli/memory_candidates.py +541 -0
- algo_cli/memory_echo_veil.py +394 -0
- algo_cli/memory_runtime.py +112 -0
- algo_cli/model_info.py +548 -0
- algo_cli/model_profile.py +160 -0
- algo_cli/model_routing.py +74 -0
- algo_cli/oneshot.py +331 -0
- algo_cli/perf_telemetry.py +389 -0
- algo_cli/plugins.py +245 -0
- algo_cli/private_event_store.py +654 -0
- algo_cli/quantization/__init__.py +24 -0
- algo_cli/quantization/lloyd_max.py +98 -0
- algo_cli/quantization/turbo_quant.py +308 -0
- algo_cli/reasoning/__init__.py +46 -0
- algo_cli/reasoning/combinatorial.py +356 -0
- algo_cli/reasoning/graph_of_thought.py +297 -0
- algo_cli/reasoning/mcts.py +220 -0
- algo_cli/reasoning/neuro_symbolic.py +250 -0
- algo_cli/reasoning/react.py +246 -0
- algo_cli/reasoning/reflexion.py +225 -0
- algo_cli/reasoning/tree_of_thought.py +241 -0
- algo_cli/reasoning_bridge.py +150 -0
- algo_cli/reconciliation.py +284 -0
- algo_cli/reflex.py +385 -0
- algo_cli/resources/docs/ALGO.md +13958 -0
- algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
- algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
- algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
- algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
- algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
- algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
- algo_cli/resources/docs/main-split-map.md +35 -0
- algo_cli/resources/docs/privacy-and-context.md +48 -0
- algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
- algo_cli/resources/skills/README.md +26 -0
- algo_cli/resources/skills/algo-cli.md +59 -0
- algo_cli/resources/skills/edit-file-precision.md +49 -0
- algo_cli/resources/skills/harness-search-first.md +47 -0
- algo_cli/resources/skills/memory-recall-ritual.md +51 -0
- algo_cli/resources/skills/qol-algorithms.md +224 -0
- algo_cli/resources/skills/smart-error-recovery.md +56 -0
- algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
- algo_cli/retrieval_algorithms.py +127 -0
- algo_cli/runtime_qos.py +236 -0
- algo_cli/runtime_services.py +320 -0
- algo_cli/session_commands.py +95 -0
- algo_cli/session_mode.py +113 -0
- algo_cli/skills.py +430 -0
- algo_cli/slash_dispatch.py +1265 -0
- algo_cli/small_context.py +206 -0
- algo_cli/spawn_budget.py +89 -0
- algo_cli/task_ledger.py +84 -0
- algo_cli/task_router.py +197 -0
- algo_cli/tool_context.py +94 -0
- algo_cli/tool_contract.py +99 -0
- algo_cli/tool_policy.py +357 -0
- algo_cli/tool_runtime.py +647 -0
- algo_cli/tools.py +3056 -0
- algo_cli/url_scheme.py +174 -0
- algo_cli/verify.py +154 -0
- algo_cli/version_manifest.py +178 -0
- algo_cli/vision_screenshot_verify.py +76 -0
- algo_cli/workspace_resolver.py +68 -0
- algo_cli/x_account.py +209 -0
- algo_cli/xai_auth.py +374 -0
- algo_cli/xai_client.py +600 -0
- algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
- algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
- algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
- algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
- algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
]
|