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,647 @@
1
+ """Tool execution, approval, attempt ledger, and reflection checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ollama import Client
14
+
15
+ from .config import Config
16
+ from . import execution_guardrails
17
+ from . import reflex
18
+ from . import tools as tools_module
19
+ from .chat_protocol import get_attr
20
+ from .display import redact_tool_args, show_info, show_tool_call, show_tool_result, tool_execution_status
21
+ from .perf_telemetry import record_perf_event
22
+ from .runtime_qos import RuntimeHint, classify_tool_runtime
23
+ from .runtime_services import scoped_tool_runtime_env
24
+ from .tools import TOOL_MAP
25
+ from .tool_policy import RuntimeToolPolicyDecision, evaluate_runtime_tool_policy
26
+
27
+ ATTEMPT_LEDGER_LIMIT = 48
28
+ REFLECTION_RECENT_MESSAGES = 8
29
+ TOOL_RESULT_CONTENT_LIMIT = 20_000
30
+ FAILED_ATTEMPT_SKIP_SECONDS = 120.0
31
+ _SHELL_EXIT_CODE_RE = re.compile(r"\[exit code:\s*(-?\d+)\]", re.IGNORECASE)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class RuntimeToolPreflight:
36
+ """Shared policy/QoS decision for a model-invoked tool call."""
37
+
38
+ signature_args: dict[str, Any]
39
+ runtime_hint: RuntimeHint
40
+ policy: RuntimeToolPolicyDecision
41
+ guardrail_allowed: bool = True
42
+ guardrail_reasons: tuple[str, ...] = ()
43
+ queue_position: int | None = None
44
+
45
+ @property
46
+ def allowed(self) -> bool:
47
+ return self.policy.allowed and self.guardrail_allowed
48
+
49
+ @property
50
+ def qos_fields(self) -> dict[str, Any]:
51
+ fields: dict[str, Any] = {
52
+ "spawn_class": self.runtime_hint.spawn_class.value,
53
+ "estimated_cost": self.runtime_hint.estimated_cost,
54
+ "log_path": self.runtime_hint.log_path,
55
+ "log_suppression": self.runtime_hint.log_suppression,
56
+ }
57
+ if self.queue_position is not None:
58
+ fields["queue_position"] = self.queue_position
59
+ return fields
60
+
61
+ @property
62
+ def blocked_result(self) -> str:
63
+ reasons = [*self.policy.reasons, *self.guardrail_reasons]
64
+ reason = "; ".join(reasons) or "runtime policy chain rejected the call"
65
+ return f"Blocked by runtime policy chain: {reason}."
66
+
67
+
68
+ _SAFE_SESSION_COMMANDS = {
69
+ "/actions",
70
+ "/changes",
71
+ "/dashboard",
72
+ "/diff",
73
+ "/doctor",
74
+ "/help",
75
+ "/hread",
76
+ "/hsearch",
77
+ "/identity",
78
+ "/info",
79
+ "/memories",
80
+ "/perf",
81
+ "/selfcheck",
82
+ "/status",
83
+ }
84
+ _SAFE_SESSION_STATUS_COMMANDS = {
85
+ "/auto",
86
+ "/cloud",
87
+ "/cloudauto",
88
+ "/code-rag",
89
+ "/context",
90
+ "/harness",
91
+ "/icl",
92
+ "/intel",
93
+ "/intelagence",
94
+ "/intelligence",
95
+ "/intuition",
96
+ "/lessons",
97
+ "/memory-auto",
98
+ "/mode",
99
+ "/policy",
100
+ "/reason",
101
+ "/reflex",
102
+ "/safe",
103
+ "/skills",
104
+ "/thinking",
105
+ "/verify",
106
+ "/x-account",
107
+ "/xai-status",
108
+ }
109
+ _EMPTY_ARG_TOGGLES = {
110
+ "/auto",
111
+ "/cloud",
112
+ "/cloudauto",
113
+ "/safe",
114
+ "/thinking",
115
+ "/verify",
116
+ }
117
+
118
+
119
+ def session_command_requires_approval(command_line: str) -> bool:
120
+ """Return whether a model-invoked slash command should prompt first."""
121
+ stripped = (command_line or "").strip()
122
+ if not stripped.startswith("/"):
123
+ stripped = f"/{stripped}" if stripped else ""
124
+ if not stripped:
125
+ return True
126
+ parts = stripped.split(maxsplit=1)
127
+ command = parts[0].lower()
128
+ arg = parts[1].strip().lower() if len(parts) > 1 else ""
129
+ if command in _SAFE_SESSION_COMMANDS:
130
+ return False
131
+ if command in {"/read", "/ls", "/cwd"}:
132
+ return False
133
+ if command == "/cd":
134
+ return True
135
+ if command in {"/intelligence", "/intel", "/intelagence"}:
136
+ return not (
137
+ arg in {"", "status", "show", "?", "guide", "help"}
138
+ or arg.startswith("query ")
139
+ )
140
+ if command == "/kernel":
141
+ return not (
142
+ arg in {"", "list", "show", "check", "?", "help"}
143
+ or arg.startswith("show ")
144
+ or arg.startswith("check ")
145
+ )
146
+ if command in _EMPTY_ARG_TOGGLES:
147
+ return arg not in {"status", "show", "?"}
148
+ if command == "/agent":
149
+ return not (
150
+ arg in {"help", "--help", "-h", "?", "threads", "list", "status", "show"}
151
+ or arg.startswith("show ")
152
+ )
153
+ if command in _SAFE_SESSION_STATUS_COMMANDS and arg in {"", "status", "show", "?", "guide", "help"}:
154
+ return False
155
+ if command == "/x-account" and arg == "status":
156
+ return False
157
+ return True
158
+
159
+
160
+ def ask_approval(name: str, args: dict[str, Any], cfg: Config, *, force: bool = False) -> bool:
161
+ from .display import console
162
+
163
+ command_line = str(args.get("command") or "")
164
+ model_cd = name in {"session_command", "session_slash"} and (
165
+ command_line.strip().lower() == "/cd"
166
+ or command_line.strip().lower().startswith("/cd ")
167
+ )
168
+ if cfg.auto_approve_active and not force and not model_cd:
169
+ return True
170
+ from .action_registry import action_requires_approval
171
+
172
+ dangerous = model_cd or action_requires_approval(name) or (
173
+ name == "session_command"
174
+ and session_command_requires_approval(str(args.get("command") or ""))
175
+ )
176
+ if not dangerous:
177
+ return True
178
+ console.print(f"[yellow]Approve {name}?[/] Use y, n, or a to approve all this session.")
179
+ console.print(json.dumps(redact_tool_args(name, args), indent=2))
180
+ try:
181
+ approval = input("Approve? [y/N/a] ").strip().lower()
182
+ except EOFError:
183
+ # No stdin available (e.g., in tests or non-interactive mode) - deny by default
184
+ console.print("[red]No input available, denying operation.[/]")
185
+ return False
186
+ if approval == "a":
187
+ # Session-only: cfg.save() never persists this flag, unlike /auto.
188
+ cfg.session_auto_approve = True
189
+ return True
190
+ return approval == "y"
191
+
192
+
193
+ def tool_runtime_args(name: str, args: dict[str, Any], cfg: Config) -> dict[str, Any]:
194
+ """Return tool args after applying runtime defaults used for execution.
195
+
196
+ Only JSON-serializable defaults belong here: the result feeds
197
+ tool_attempt_signature and the persisted attempt ledger. The live Config
198
+ handle for cfg-bound tools is injected by run_tool at execution time.
199
+ """
200
+ call_args = dict(args)
201
+ if name in {
202
+ "read_file",
203
+ "read_pdf",
204
+ "render_pdf_pages",
205
+ "write_file",
206
+ "edit_file",
207
+ "list_directory",
208
+ "search_files",
209
+ "find_unique_anchor",
210
+ "batch_edit",
211
+ "run_shell",
212
+ "git_status",
213
+ "git_diff",
214
+ }:
215
+ call_args["cwd"] = cfg.cwd
216
+ if name == "run_shell":
217
+ # Preserve the session-level /safe guard. A model may opt into stricter
218
+ # safe_mode, but it may not opt out while cfg.safe_mode is enabled.
219
+ call_args["safe_mode"] = bool(getattr(cfg, "safe_mode", True)) or bool(call_args.get("safe_mode", False))
220
+ return call_args
221
+
222
+
223
+ def _effective_tool_path(args: dict[str, Any]) -> Path | None:
224
+ """Return the exact path candidate implied by a tool's path and cwd args."""
225
+
226
+ raw_path = args.get("path")
227
+ if not isinstance(raw_path, (str, os.PathLike)) or not str(raw_path).strip():
228
+ return None
229
+ try:
230
+ candidate = Path(raw_path).expanduser()
231
+ if not candidate.is_absolute():
232
+ candidate = Path(str(args.get("cwd") or ".")).expanduser() / candidate
233
+ return candidate
234
+ except (OSError, RuntimeError, TypeError, ValueError):
235
+ return None
236
+
237
+
238
+ def preflight_runtime_tool(
239
+ name: str,
240
+ args: dict[str, Any],
241
+ cfg: Config,
242
+ *,
243
+ queue_position: int | None = None,
244
+ ) -> RuntimeToolPreflight:
245
+ """Evaluate and record the policy/QoS preflight used by every chat path."""
246
+
247
+ signature_args = tool_runtime_args(name, args, cfg)
248
+ runtime_hint = classify_tool_runtime(name, signature_args)
249
+ guardrail_reasons: list[str] = []
250
+ if name == "run_shell" and execution_guardrails.masks_verification_exit_status(
251
+ str(signature_args.get("command") or "")
252
+ ):
253
+ guardrail_reasons.append(
254
+ "verification command must preserve a failing exit status; remove the trailing "
255
+ "`; echo ...$?` because run_shell already reports the exit code"
256
+ )
257
+ if name in {"write_file", "edit_file", "batch_edit"}:
258
+ effective_path = _effective_tool_path(signature_args)
259
+ active_workspace = execution_guardrails.active_workspace()
260
+ if effective_path is None:
261
+ guardrail_reasons.append("file mutation requires a path")
262
+ elif active_workspace is None:
263
+ guardrail_reasons.append("no active execution scope")
264
+ else:
265
+ path_decision = execution_guardrails.assess_write_path(
266
+ active_workspace,
267
+ effective_path,
268
+ )
269
+ if not path_decision.allowed:
270
+ guardrail_reasons.append(path_decision.reason)
271
+ else:
272
+ requires_read = name in {"edit_file", "batch_edit"}
273
+ if name == "write_file" and bool(signature_args.get("overwrite")):
274
+ requires_read = bool(
275
+ path_decision.resolved_path is not None
276
+ and path_decision.resolved_path.exists()
277
+ )
278
+ if requires_read:
279
+ read_decision = execution_guardrails.read_before_edit_decision(effective_path)
280
+ if not read_decision.allowed:
281
+ guardrail_reasons.append(read_decision.reason)
282
+ preflight = RuntimeToolPreflight(
283
+ signature_args=signature_args,
284
+ runtime_hint=runtime_hint,
285
+ policy=evaluate_runtime_tool_policy(
286
+ name,
287
+ signature_args,
288
+ safe_mode=bool(getattr(cfg, "safe_mode", True)),
289
+ ),
290
+ guardrail_allowed=not guardrail_reasons,
291
+ guardrail_reasons=tuple(guardrail_reasons),
292
+ queue_position=queue_position,
293
+ )
294
+ record_perf_event(
295
+ "qos",
296
+ tool=name,
297
+ reason=runtime_hint.reason,
298
+ **preflight.qos_fields,
299
+ )
300
+ record_perf_event(
301
+ "policy",
302
+ tool=name,
303
+ status="pass" if preflight.allowed else "blocked",
304
+ tier=preflight.policy.tier,
305
+ capability_mask=preflight.policy.capability_mask,
306
+ capabilities=list(preflight.policy.capability_names),
307
+ fired_rules=list(preflight.policy.fired_rules),
308
+ guardrail_reasons=list(preflight.guardrail_reasons),
309
+ )
310
+ return preflight
311
+
312
+
313
+ def run_tool(name: str, args: dict[str, Any], cfg: Config) -> str:
314
+ call_args = tool_runtime_args(name, args, cfg)
315
+ if name in {"write_file", "edit_file"}:
316
+ from . import reconciliation
317
+
318
+ violation = reconciliation.structured_write_violation(name, call_args, cfg.messages)
319
+ if violation:
320
+ return f"Error: {violation}"
321
+ if name in ("remember", "append_lesson", "session_command"):
322
+ call_args["cfg"] = cfg
323
+ if name == "session_slash":
324
+ from . import session_commands
325
+
326
+ return session_commands.execute(str(call_args.get("command") or ""), cfg)
327
+ fn = TOOL_MAP.get(name)
328
+ if not fn:
329
+ available = ", ".join(sorted(TOOL_MAP)[:40])
330
+ return f"Unknown tool: {name}. Available tools include: {available}."
331
+ try:
332
+ result = str(fn(**call_args))
333
+ except TypeError as exc:
334
+ # Bad/missing/extra args or unparseable JSON: return a corrective hint
335
+ # (real signature + diagnosis) so a weak model can retry correctly.
336
+ from . import tool_contract
337
+
338
+ return tool_contract.correct_tool_error(name, call_args, exc, fn)
339
+ except Exception as exc:
340
+ return f"Tool error for {name}: {exc}"
341
+ # Nudge the model off Unix-in-cmd.exe mistakes when the shell reports them.
342
+ if name == "run_shell":
343
+ from . import tool_contract
344
+
345
+ hint = tool_contract.shell_mistake_hint(str(call_args.get("command", "")), result)
346
+ if hint:
347
+ result = f"{result}\n{hint}"
348
+ return result
349
+
350
+
351
+ def tool_attempt_signature(name: str, args: dict[str, Any]) -> str:
352
+ safe_args = redact_tool_args(name, args)
353
+ try:
354
+ encoded = json.dumps(safe_args, sort_keys=True, ensure_ascii=True, default=str, separators=(",", ":"))
355
+ except TypeError:
356
+ encoded = str(safe_args)
357
+ return f"{name}:{encoded}"
358
+
359
+
360
+ def find_failed_attempt(cfg: Config, signature: str) -> dict[str, Any] | None:
361
+ now = time.time()
362
+ for item in reversed(cfg.attempt_ledger):
363
+ if item.get("signature") != signature:
364
+ continue
365
+ status = item.get("status")
366
+ if status == "skipped":
367
+ return None
368
+ if status != "failed":
369
+ return None
370
+ try:
371
+ age = now - float(item.get("timestamp") or 0)
372
+ except (TypeError, ValueError):
373
+ age = 0.0
374
+ if age <= FAILED_ATTEMPT_SKIP_SECONDS:
375
+ return item
376
+ return None
377
+ return None
378
+
379
+
380
+ def summarize_tool_result(result: str, limit: int = 140) -> str:
381
+ text = " ".join(str(result).split())
382
+ return text[:limit] + ("..." if len(text) > limit else "")
383
+
384
+
385
+ def run_args_preview(args: dict[str, Any], limit: int = 60, *, name: str = "") -> str:
386
+ safe_args = redact_tool_args(name, args)
387
+ try:
388
+ text = json.dumps(safe_args, ensure_ascii=True, default=str, separators=(",", ":"))
389
+ except TypeError:
390
+ text = str(safe_args)
391
+ return text[:limit]
392
+
393
+
394
+ def classify_tool_status(result: str, *, approved: bool = True, skipped: bool = False) -> str:
395
+ if skipped:
396
+ return "skipped"
397
+ if not approved:
398
+ return "denied"
399
+ lowered = str(result).strip().lower()
400
+ if lowered.startswith(("error:", "tool error", "tool argument error", "unknown tool")):
401
+ return "failed"
402
+ exit_matches = _SHELL_EXIT_CODE_RE.findall(str(result))
403
+ if exit_matches and int(exit_matches[-1]) != 0:
404
+ return "failed"
405
+ return "worked"
406
+
407
+
408
+ def augment_tool_result_with_reflex(
409
+ cfg: Config,
410
+ name: str,
411
+ args: dict[str, Any],
412
+ result: str,
413
+ status: str,
414
+ ) -> str:
415
+ augmented, note = reflex.maybe_augment_tool_result(cfg, name, args, result, status)
416
+ if status == "worked":
417
+ from . import reconciliation
418
+
419
+ augmented = reconciliation.augment_read_result(name, augmented, messages=cfg.messages)
420
+ if note:
421
+ show_info(note)
422
+ return augmented
423
+
424
+
425
+ def record_tool_attempt(
426
+ cfg: Config,
427
+ *,
428
+ name: str,
429
+ args: dict[str, Any],
430
+ result: str,
431
+ status: str,
432
+ ) -> None:
433
+ worked = status == "worked"
434
+ workspace_changed = False
435
+ effective_path = _effective_tool_path(args)
436
+ if name == "read_file" and effective_path is not None:
437
+ execution_guardrails.record_read(effective_path, success=worked)
438
+ elif name in {"write_file", "edit_file", "batch_edit"} and effective_path is not None:
439
+ success_prefixes = {
440
+ "write_file": "Wrote ",
441
+ "edit_file": "Edited ",
442
+ "batch_edit": "Batch-edited ",
443
+ }
444
+ mutation_succeeded = worked and str(result).lstrip().startswith(success_prefixes[name])
445
+ workspace_changed = mutation_succeeded
446
+ execution_guardrails.record_mutation(
447
+ effective_path,
448
+ success=mutation_succeeded,
449
+ operation=name,
450
+ )
451
+ elif name == "run_shell":
452
+ command = str(args.get("command") or "")
453
+ exit_matches = _SHELL_EXIT_CODE_RE.findall(str(result))
454
+ returncode = int(exit_matches[-1]) if exit_matches else None
455
+ if returncode == 0 and tools_module.shell_mutates_workspace(command):
456
+ workspace_changed = True
457
+ execution_guardrails.record_workspace_mutation(success=True)
458
+ if returncode is not None:
459
+ execution_guardrails.record_shell_verification(command, returncode=returncode)
460
+ elif name == "git_diff":
461
+ normalized_result = str(result).strip().lower()
462
+ execution_guardrails.record_verification(
463
+ "git_diff",
464
+ success=worked
465
+ and normalized_result not in {"", "(no tracked diff)", "(clean working tree)"},
466
+ )
467
+ signature = tool_attempt_signature(name, args)
468
+ if workspace_changed:
469
+ # A workspace mutation invalidates cached failures: the exact same
470
+ # test/check command is often the correct next action after a fix.
471
+ cfg.attempt_ledger = [
472
+ item for item in cfg.attempt_ledger if item.get("status") not in {"failed", "skipped"}
473
+ ]
474
+ args_preview = json.dumps(
475
+ redact_tool_args(name, args),
476
+ sort_keys=True,
477
+ ensure_ascii=True,
478
+ default=str,
479
+ )[:100]
480
+ cfg.attempt_ledger.append(
481
+ {
482
+ "timestamp": time.time(),
483
+ "signature": signature,
484
+ "tool": name,
485
+ "args_preview": args_preview,
486
+ "status": status,
487
+ "summary": summarize_tool_result(result),
488
+ }
489
+ )
490
+ cfg.attempt_ledger = cfg.attempt_ledger[-ATTEMPT_LEDGER_LIMIT:]
491
+
492
+
493
+ def tool_result_message(name: str, content: str, tool_call_id: str | None = None) -> dict[str, Any]:
494
+ message = {
495
+ "role": "tool",
496
+ "name": name,
497
+ "tool_name": name,
498
+ "content": content[:TOOL_RESULT_CONTENT_LIMIT],
499
+ }
500
+ if tool_call_id:
501
+ message["tool_call_id"] = tool_call_id
502
+ return message
503
+
504
+
505
+ def recent_messages_for_reflection(cfg: Config, user_message: str) -> str:
506
+ snippets: list[str] = []
507
+ if cfg.session_summary.strip():
508
+ snippets.append(f"SESSION SUMMARY:\n{cfg.session_summary.strip()}")
509
+ snippets.append(f"CURRENT USER GOAL:\n{user_message.strip()}")
510
+ if cfg.messages:
511
+ snippets.append("RECENT MESSAGES:")
512
+ for message in cfg.messages[-REFLECTION_RECENT_MESSAGES:]:
513
+ role = str(message.get("role", "message"))
514
+ name = message.get("name")
515
+ label = f"{role}[{name}]" if name else role
516
+ content = (message.get("content") or message.get("thinking") or "").strip()
517
+ if not content:
518
+ continue
519
+ if len(content) > 700:
520
+ content = content[:700] + "..."
521
+ snippets.append(f"- {label}: {content}")
522
+ return "\n".join(snippets)
523
+
524
+
525
+ def reflection_checkpoint(client: Client, cfg: Config, user_message: str, tool_calls_seen: int) -> None:
526
+ checkpoint_prompt = recent_messages_for_reflection(cfg, user_message)
527
+ system = (
528
+ "You are pausing an agentic terminal session for a progress checkpoint.\n"
529
+ "Return compact JSON with keys: objective, completed, evidence, remaining, "
530
+ "alignment_check, web_research_needed, web_research_reason, next_action, "
531
+ "confidence (float 0.0-1.0: how certain you are the completed work is correct), "
532
+ "and unverified_claims (list of specific facts stated but not confirmed by tool results).\n"
533
+ "The alignment_check must state whether completed work and next_action still match the user's objective.\n"
534
+ "Keep each value short. Do not include chain-of-thought or hidden reasoning. "
535
+ "The result will be fed back into the conversation as an internal continuation note."
536
+ )
537
+ try:
538
+ from . import main as _main
539
+
540
+ reflection_client, reflection_model = _main.small_maintenance_client(cfg, client)
541
+ response = reflection_client.chat(
542
+ model=reflection_model,
543
+ messages=[
544
+ {"role": "system", "content": system},
545
+ {"role": "user", "content": checkpoint_prompt},
546
+ ],
547
+ stream=False,
548
+ think=False,
549
+ format="json",
550
+ keep_alive=cfg.keep_alive,
551
+ options={"temperature": 0.1, "num_ctx": min(cfg.num_ctx, 4096), "num_predict": 512},
552
+ )
553
+ content = get_attr(get_attr(response, "message", {}), "content", "").strip()
554
+ except Exception as exc:
555
+ content = json.dumps(
556
+ {
557
+ "objective": "Checkpoint unavailable",
558
+ "completed": "Reflection failed before summary generation.",
559
+ "evidence": "No checkpoint response was produced.",
560
+ "remaining": "Continue from the last verified tool result.",
561
+ "alignment_check": "Use the current user goal and latest tool results as the source of truth.",
562
+ "web_research_needed": False,
563
+ "web_research_reason": f"Reflection error: {exc}",
564
+ "next_action": "Continue without a checkpoint summary.",
565
+ "confidence": 0.5,
566
+ "unverified_claims": [],
567
+ },
568
+ ensure_ascii=False,
569
+ )
570
+ if not content:
571
+ return
572
+ low_confidence_note = ""
573
+ try:
574
+ parsed = json.loads(content)
575
+ confidence = float(parsed.get("confidence", 1.0))
576
+ unverified = parsed.get("unverified_claims", [])
577
+ if confidence < 0.6:
578
+ low_confidence_note = (
579
+ "\n⚠ Low confidence detected. Verify uncertain claims with "
580
+ "read_file, search_files, or harness_search before providing the final answer."
581
+ )
582
+ elif unverified:
583
+ low_confidence_note = (
584
+ f"\n⚠ {len(unverified)} unverified claim(s) flagged. "
585
+ "Consider using tool calls to confirm before stating as fact."
586
+ )
587
+ except Exception:
588
+ pass
589
+ note = (
590
+ f"[Internal checkpoint after {tool_calls_seen} tool calls]\n"
591
+ f"{content}{low_confidence_note}\n\n"
592
+ "Use this only to align the next step with the user's goal. Do not answer this checkpoint directly. "
593
+ "Continue the active task with the next necessary tool call, or provide the final answer only if the task is complete."
594
+ )
595
+ cfg.messages.append({"role": "user", "content": note})
596
+ show_info(f"Checkpoint after {tool_calls_seen} tool calls: progress reviewed.")
597
+
598
+
599
+ def execute_tool_call_for_pipeline(
600
+ name: str,
601
+ args: dict[str, Any],
602
+ cfg: Config,
603
+ *,
604
+ tool_call_id: str | None = None,
605
+ force_approval: bool = False,
606
+ ) -> tuple[dict[str, Any], str]:
607
+ show_tool_call(name, args)
608
+ preflight = preflight_runtime_tool(name, args, cfg)
609
+ signature_args = preflight.signature_args
610
+ qos_fields = preflight.qos_fields
611
+ if not preflight.allowed:
612
+ result = preflight.blocked_result
613
+ show_tool_result(name, result, approved=False)
614
+ record_tool_attempt(cfg, name=name, args=signature_args, result=result, status="denied")
615
+ record_perf_event("tool", tool=name, status="denied", duration_ms=0.0, **qos_fields)
616
+ return tool_result_message(name, result, tool_call_id), result
617
+ signature = tool_attempt_signature(name, signature_args)
618
+ previous_failure = find_failed_attempt(cfg, signature)
619
+ if previous_failure:
620
+ result = (
621
+ "Skipped repeated failed attempt. "
622
+ f"Prior outcome: {previous_failure.get('summary', 'same tool path already failed or was denied')}."
623
+ )
624
+ show_tool_result(name, result, approved=False)
625
+ record_tool_attempt(cfg, name=name, args=signature_args, result=result, status="skipped")
626
+ record_perf_event("tool", tool=name, status="skipped", duration_ms=0.0, **qos_fields)
627
+ return tool_result_message(name, result, tool_call_id), result
628
+ if not ask_approval(name, args, cfg, force=force_approval):
629
+ result = "User denied this operation."
630
+ show_tool_result(name, result, approved=False)
631
+ record_tool_attempt(cfg, name=name, args=signature_args, result=result, status="denied")
632
+ record_perf_event("tool", tool=name, status="denied", duration_ms=0.0, **qos_fields)
633
+ return tool_result_message(name, result, tool_call_id), result
634
+
635
+ started = time.perf_counter()
636
+ with scoped_tool_runtime_env(cfg):
637
+ with tool_execution_status(
638
+ f"[muted]executing {name} · {preflight.runtime_hint.spawn_class.value}...[/]"
639
+ ):
640
+ result = run_tool(name, args, cfg)
641
+ duration_ms = round((time.perf_counter() - started) * 1000, 2)
642
+ status = classify_tool_status(result)
643
+ result = augment_tool_result_with_reflex(cfg, name, signature_args, result, status)
644
+ show_tool_result(name, result, duration_ms=duration_ms)
645
+ record_tool_attempt(cfg, name=name, args=signature_args, result=result, status=status)
646
+ record_perf_event("tool", tool=name, status=status, duration_ms=duration_ms, **qos_fields)
647
+ return tool_result_message(name, result, tool_call_id), result