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
algo_cli/reflex.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Minimal reflex loop v0.1 — session recovery behind ``reflex_enabled`` (default off).
|
|
2
|
+
|
|
3
|
+
Implements a subset of docs/reflex-loop-v0.2.md: detect → one safe ACT → inject
|
|
4
|
+
result. No background tasks, no index mutation, no verification-loop integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import tools as tools_module
|
|
15
|
+
from .display import redact_tool_args
|
|
16
|
+
|
|
17
|
+
REFLEX_MAX_CYCLES = 3
|
|
18
|
+
REFLEX_LEDGER_CONTEXT_KEY = "reflex_cycles"
|
|
19
|
+
REFLEX_CAP_NOTIFIED_KEY = "reflex_cap_notified"
|
|
20
|
+
|
|
21
|
+
REFLEX_SAFE_TOOLS = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"harness_search",
|
|
24
|
+
"harness_read",
|
|
25
|
+
"harness_stats",
|
|
26
|
+
"available_actions",
|
|
27
|
+
"session_slash",
|
|
28
|
+
"read_file",
|
|
29
|
+
"search_files",
|
|
30
|
+
"list_directory",
|
|
31
|
+
"git_status",
|
|
32
|
+
"git_diff",
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_EMPTY_RESULT_MARKERS = (
|
|
37
|
+
"no harness matches",
|
|
38
|
+
"error:",
|
|
39
|
+
"not found",
|
|
40
|
+
"no matches",
|
|
41
|
+
"no such file",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def tool_signature(name: str, args: dict[str, Any]) -> str:
|
|
46
|
+
safe_args = redact_tool_args(name, args)
|
|
47
|
+
try:
|
|
48
|
+
encoded = json.dumps(safe_args, sort_keys=True, ensure_ascii=True, default=str, separators=(",", ":"))
|
|
49
|
+
except TypeError:
|
|
50
|
+
encoded = str(safe_args)
|
|
51
|
+
return f"{name}:{encoded}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _reflex_cycles(cfg: Any) -> int:
|
|
55
|
+
state = getattr(cfg, "context_state", None) or {}
|
|
56
|
+
if not isinstance(state, dict):
|
|
57
|
+
return 0
|
|
58
|
+
try:
|
|
59
|
+
return max(0, int(state.get(REFLEX_LEDGER_CONTEXT_KEY, 0)))
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _increment_reflex_cycles(cfg: Any) -> None:
|
|
65
|
+
state = getattr(cfg, "context_state", None)
|
|
66
|
+
if not isinstance(state, dict):
|
|
67
|
+
cfg.context_state = {}
|
|
68
|
+
state = cfg.context_state
|
|
69
|
+
state[REFLEX_LEDGER_CONTEXT_KEY] = _reflex_cycles(cfg) + 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def reset_reflex_cycles(cfg: Any) -> None:
|
|
73
|
+
state = getattr(cfg, "context_state", None)
|
|
74
|
+
if isinstance(state, dict):
|
|
75
|
+
state.pop(REFLEX_LEDGER_CONTEXT_KEY, None)
|
|
76
|
+
state.pop(REFLEX_CAP_NOTIFIED_KEY, None)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def begin_agent_pipeline(cfg: Any) -> None:
|
|
80
|
+
"""Isolate reflex budget per /agent run (avoids burning cap during exploration)."""
|
|
81
|
+
reset_reflex_cycles(cfg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def count_signature_in_ledger(cfg: Any, signature: str) -> int:
|
|
85
|
+
ledger = getattr(cfg, "attempt_ledger", None) or []
|
|
86
|
+
return sum(1 for item in ledger if item.get("signature") == signature)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _result_is_empty_or_failed(result: str, status: str) -> bool:
|
|
90
|
+
if status == "failed":
|
|
91
|
+
return True
|
|
92
|
+
lowered = str(result).strip().lower()
|
|
93
|
+
if not lowered:
|
|
94
|
+
return True
|
|
95
|
+
return any(marker in lowered for marker in _EMPTY_RESULT_MARKERS)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class ReflexTrigger:
|
|
100
|
+
label: str
|
|
101
|
+
reason: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_benign_exploration_failure(name: str, args: dict[str, Any], result: str) -> bool:
|
|
105
|
+
"""Expected misses while exploring — reflex should not burn cycles here."""
|
|
106
|
+
lowered = str(result).lower()
|
|
107
|
+
if name in {"git_status", "git_diff"}:
|
|
108
|
+
return "not a git repository" in lowered or "fatal: not a git repository" in lowered
|
|
109
|
+
if name == "list_directory":
|
|
110
|
+
return "directory not found" in lowered or "error: directory not found" in lowered
|
|
111
|
+
if name == "read_file":
|
|
112
|
+
if "file not found" not in lowered and "error: file not found" not in lowered:
|
|
113
|
+
return False
|
|
114
|
+
path = str(args.get("path") or "").replace("\\", "/")
|
|
115
|
+
# Wrong relative path from home (e.g. algo_cli/foo vs ollama-cli/algo_cli/foo).
|
|
116
|
+
if path.startswith("algo_cli/") and not path.startswith("ollama-cli/"):
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def detect_trigger(cfg: Any, name: str, args: dict[str, Any], result: str, status: str) -> ReflexTrigger | None:
|
|
122
|
+
if _is_benign_exploration_failure(name, args, result):
|
|
123
|
+
return None
|
|
124
|
+
signature = tool_signature(name, args)
|
|
125
|
+
prior = count_signature_in_ledger(cfg, signature)
|
|
126
|
+
if prior >= 2:
|
|
127
|
+
return ReflexTrigger("loop_detected", f"same tool path attempted {prior + 1} times")
|
|
128
|
+
if status == "failed":
|
|
129
|
+
return ReflexTrigger("tool_failed", summarize_tool_result(result))
|
|
130
|
+
if (
|
|
131
|
+
name in {"harness_search", "search_files"}
|
|
132
|
+
and prior >= 1
|
|
133
|
+
and _result_is_empty_or_failed(result, status)
|
|
134
|
+
):
|
|
135
|
+
return ReflexTrigger("search_miss", "search returned no actionable hits")
|
|
136
|
+
if prior >= 1 and _result_is_empty_or_failed(result, status):
|
|
137
|
+
return ReflexTrigger("empty_repeat", "tool returned no useful output on repeat")
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def summarize_tool_result(result: str, limit: int = 140) -> str:
|
|
142
|
+
text = " ".join(str(result).split())
|
|
143
|
+
return text[:limit] + ("..." if len(text) > limit else "")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _broaden_harness_query(args: dict[str, Any]) -> str:
|
|
147
|
+
raw = str(args.get("query") or args.get("q") or "").strip()
|
|
148
|
+
if not raw:
|
|
149
|
+
return ""
|
|
150
|
+
tokens = [t for t in re.findall(r"[\w.-]+", raw) if len(t) > 2]
|
|
151
|
+
return " ".join(tokens[:6]) if tokens else raw
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _recovery_query_for_tool(name: str, args: dict[str, Any]) -> str:
|
|
155
|
+
if name == "harness_search":
|
|
156
|
+
broadened = _broaden_harness_query(args)
|
|
157
|
+
return broadened or "algo-cli harness"
|
|
158
|
+
if name == "read_file":
|
|
159
|
+
path = str(args.get("path") or "")
|
|
160
|
+
stem = path.replace("\\", "/").split("/")[-1]
|
|
161
|
+
return stem or path
|
|
162
|
+
if name == "search_files":
|
|
163
|
+
return str(args.get("pattern") or args.get("query") or "project")
|
|
164
|
+
return _broaden_harness_query(args) or "workspace context"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _run_safe_act(cfg: Any, trigger: ReflexTrigger, name: str, args: dict[str, Any]) -> tuple[str, bool]:
|
|
168
|
+
"""Return (supplemental text, resolved)."""
|
|
169
|
+
if trigger.label == "loop_detected":
|
|
170
|
+
return (
|
|
171
|
+
"Reflex: loop detected (same tool + args). Stop retrying this path. "
|
|
172
|
+
"Change strategy, ask the user, or use a different read-only tool.",
|
|
173
|
+
False,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
query = _recovery_query_for_tool(name, args)
|
|
177
|
+
if "harness_search" in REFLEX_SAFE_TOOLS:
|
|
178
|
+
fallback = tools_module.harness_search(query=query, limit=5)
|
|
179
|
+
if fallback and "no harness matches" not in fallback.lower():
|
|
180
|
+
return f"Reflex recovery — harness_search:\n{fallback}", True
|
|
181
|
+
|
|
182
|
+
if name == "read_file" and "search_files" in REFLEX_SAFE_TOOLS:
|
|
183
|
+
pattern = str(args.get("path", "")).split("/")[-1].split("\\")[-1]
|
|
184
|
+
if pattern:
|
|
185
|
+
alt = tools_module.search_files(
|
|
186
|
+
pattern=pattern,
|
|
187
|
+
cwd=str(getattr(cfg, "cwd", ".")),
|
|
188
|
+
limit=5,
|
|
189
|
+
)
|
|
190
|
+
if alt and "error" not in alt.lower()[:20]:
|
|
191
|
+
return f"Reflex recovery — search_files for {pattern}:\n{alt}", True
|
|
192
|
+
|
|
193
|
+
actions = tools_module.available_actions(topic=query[:40])
|
|
194
|
+
return (
|
|
195
|
+
f"Reflex ({trigger.label}): {trigger.reason}\n"
|
|
196
|
+
f"Suggested next reads:\n{actions[:1200]}",
|
|
197
|
+
False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def maybe_augment_tool_result(
|
|
202
|
+
cfg: Any,
|
|
203
|
+
name: str,
|
|
204
|
+
args: dict[str, Any],
|
|
205
|
+
result: str,
|
|
206
|
+
status: str,
|
|
207
|
+
) -> tuple[str, str | None]:
|
|
208
|
+
"""Append a reflex recovery block when enabled and triggered.
|
|
209
|
+
|
|
210
|
+
Returns (possibly augmented result, user-visible reflex note or None).
|
|
211
|
+
"""
|
|
212
|
+
if not getattr(cfg, "reflex_enabled", False):
|
|
213
|
+
return result, None
|
|
214
|
+
if name not in REFLEX_SAFE_TOOLS and status != "failed":
|
|
215
|
+
# Only auto-intervene on unsafe tools when they hard-failed.
|
|
216
|
+
if status != "failed":
|
|
217
|
+
return result, None
|
|
218
|
+
cycles = _reflex_cycles(cfg)
|
|
219
|
+
if cycles >= REFLEX_MAX_CYCLES:
|
|
220
|
+
state = getattr(cfg, "context_state", None)
|
|
221
|
+
if not isinstance(state, dict):
|
|
222
|
+
cfg.context_state = {}
|
|
223
|
+
state = cfg.context_state
|
|
224
|
+
if not state.get(REFLEX_CAP_NOTIFIED_KEY):
|
|
225
|
+
state[REFLEX_CAP_NOTIFIED_KEY] = True
|
|
226
|
+
return result, (
|
|
227
|
+
f"Reflex: cap ({REFLEX_MAX_CYCLES}) reached for this /agent run — "
|
|
228
|
+
"auto-recovery disabled until /reflex reset or next /agent."
|
|
229
|
+
)
|
|
230
|
+
return result, None
|
|
231
|
+
|
|
232
|
+
trigger = detect_trigger(cfg, name, args, result, status)
|
|
233
|
+
if trigger is None:
|
|
234
|
+
return result, None
|
|
235
|
+
|
|
236
|
+
_increment_reflex_cycles(cfg)
|
|
237
|
+
supplement, resolved = _run_safe_act(cfg, trigger, name, args)
|
|
238
|
+
verdict = "resolved" if resolved else "escalate"
|
|
239
|
+
note = f"↻ reflex [{trigger.label}] cycle {_reflex_cycles(cfg)}/{REFLEX_MAX_CYCLES} — {verdict}"
|
|
240
|
+
block = f"\n\n---\n{supplement}"
|
|
241
|
+
return f"{result}{block}", note
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def status_line(cfg: Any) -> str:
|
|
245
|
+
enabled = getattr(cfg, "reflex_enabled", False)
|
|
246
|
+
cycles = _reflex_cycles(cfg)
|
|
247
|
+
return f"reflex: {'ON' if enabled else 'OFF'} ({cycles}/{REFLEX_MAX_CYCLES} cycles this session)"
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Reflex Error-Interceptor Loop (v0.2)
|
|
250
|
+
# Catches tool errors, generates a diagnostic prompt, retries with modified
|
|
251
|
+
# arguments up to REFLEX_ERROR_MAX_RETRIES times. Integrates with the existing
|
|
252
|
+
# attempt_ledger and reflex cycle tracker.
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
REFLEX_ERROR_MAX_RETRIES = 3
|
|
256
|
+
REFLEX_ERROR_LEDGER_KEY = "reflex_error_retries"
|
|
257
|
+
|
|
258
|
+
# Tools that are safe to auto-retry on failure (read-only or idempotent).
|
|
259
|
+
REFLEX_ERROR_SAFE_RETRY_TOOLS = frozenset({
|
|
260
|
+
"read_file",
|
|
261
|
+
"read_pdf",
|
|
262
|
+
"list_directory",
|
|
263
|
+
"search_files",
|
|
264
|
+
"git_status",
|
|
265
|
+
"git_diff",
|
|
266
|
+
"harness_search",
|
|
267
|
+
"harness_read",
|
|
268
|
+
"harness_stats",
|
|
269
|
+
"harness_refresh",
|
|
270
|
+
"available_actions",
|
|
271
|
+
"web_search",
|
|
272
|
+
"web_fetch",
|
|
273
|
+
"x_search",
|
|
274
|
+
"embed_text",
|
|
275
|
+
"vision_describe",
|
|
276
|
+
"model_show",
|
|
277
|
+
"query_knowledge_graph",
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
# Known error patterns and suggested argument fixes.
|
|
281
|
+
_REFLEX_ERROR_FIXES: dict[str, dict[str, Any]] = {
|
|
282
|
+
"file not found": {"action": "broaden_path", "description": "Try broader search or alternative paths"},
|
|
283
|
+
"not a directory": {"action": "fix_path_type", "description": "Target is a file, not directory"},
|
|
284
|
+
"is a directory": {"action": "fix_path_type", "description": "Target is a directory, not file"},
|
|
285
|
+
"permission denied": {"action": "skip_retry", "description": "Permission issue; retry unlikely to help"},
|
|
286
|
+
"timeout": {"action": "increase_timeout", "description": "Try again with longer timeout"},
|
|
287
|
+
"no harness matches": {"action": "broaden_query", "description": "Broaden or rephrase harness search query"},
|
|
288
|
+
"no matches": {"action": "broaden_query", "description": "Broaden or rephrase search"},
|
|
289
|
+
"tool error": {"action": "diagnose", "description": "Generic tool error; run diagnostic"},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _reflex_error_retries(cfg: Any, tool_name: str, args: dict[str, Any]) -> int:
|
|
294
|
+
"""Return how many error-retries have been used for this specific call signature."""
|
|
295
|
+
state = getattr(cfg, "context_state", None)
|
|
296
|
+
if not isinstance(state, dict):
|
|
297
|
+
return 0
|
|
298
|
+
key = (REFLEX_ERROR_LEDGER_KEY, tool_name, tool_signature(tool_name, args))
|
|
299
|
+
retries = state.get(str(key), 0)
|
|
300
|
+
return int(retries) if isinstance(retries, (int, float)) else 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _increment_reflex_error_retry(cfg: Any, tool_name: str, args: dict[str, Any]) -> None:
|
|
304
|
+
"""Increment the retry counter for this specific tool call signature."""
|
|
305
|
+
state = getattr(cfg, "context_state", None)
|
|
306
|
+
if not isinstance(state, dict):
|
|
307
|
+
cfg.context_state = {}
|
|
308
|
+
state = cfg.context_state
|
|
309
|
+
key = str((REFLEX_ERROR_LEDGER_KEY, tool_name, tool_signature(tool_name, args)))
|
|
310
|
+
state[key] = _reflex_error_retries(cfg, tool_name, args) + 1
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _suggest_fix(name: str, args: dict[str, Any], result: str) -> dict[str, Any] | None:
|
|
314
|
+
"""Based on the error pattern, suggest argument modifications for retry."""
|
|
315
|
+
lowered = str(result).strip().lower()
|
|
316
|
+
for pattern, fix in _REFLEX_ERROR_FIXES.items():
|
|
317
|
+
if pattern in lowered:
|
|
318
|
+
if fix["action"] == "skip_retry":
|
|
319
|
+
return None
|
|
320
|
+
if fix["action"] == "broaden_path" and name == "read_file":
|
|
321
|
+
# Suggest using search_files instead
|
|
322
|
+
return {"action": "search_files", "pattern": str(args.get("path", "")), "description": fix["description"]}
|
|
323
|
+
if fix["action"] == "broaden_query" and name in {"harness_search", "search_files"}:
|
|
324
|
+
return {"action": "broaden_query", "description": fix["description"]}
|
|
325
|
+
if fix["action"] == "increase_timeout" and name == "run_shell":
|
|
326
|
+
return {"action": "increase_timeout", "description": fix["description"]}
|
|
327
|
+
return {"action": fix["action"], "description": fix["description"]}
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def intercept_tool_error(
|
|
332
|
+
cfg: Any,
|
|
333
|
+
name: str,
|
|
334
|
+
args: dict[str, Any],
|
|
335
|
+
result: str,
|
|
336
|
+
) -> tuple[str, bool]:
|
|
337
|
+
"""Error-interceptor: check if a failed tool call should be retried.
|
|
338
|
+
|
|
339
|
+
Returns (diagnostic_message, should_retry).
|
|
340
|
+
The diagnostic_message explains why the error occurred and what to try next.
|
|
341
|
+
should_retry is True only when the tool is retry-safe and retries remain.
|
|
342
|
+
"""
|
|
343
|
+
if not getattr(cfg, "reflex_enabled", False):
|
|
344
|
+
return "", False
|
|
345
|
+
|
|
346
|
+
# Only intercept actual errors (status "failed" is handled by the caller)
|
|
347
|
+
lowered = str(result).strip().lower()
|
|
348
|
+
is_error = lowered.startswith("error") or "tool error" in lowered
|
|
349
|
+
if not is_error:
|
|
350
|
+
return "", False
|
|
351
|
+
|
|
352
|
+
# Skip tools that shouldn't be auto-retried
|
|
353
|
+
if name not in REFLEX_ERROR_SAFE_RETRY_TOOLS:
|
|
354
|
+
return "", False
|
|
355
|
+
|
|
356
|
+
retries_so_far = _reflex_error_retries(cfg, name, args)
|
|
357
|
+
if retries_so_far >= REFLEX_ERROR_MAX_RETRIES:
|
|
358
|
+
return (
|
|
359
|
+
f"Reflex: max error-retries ({REFLEX_ERROR_MAX_RETRIES}) reached for {name}. "
|
|
360
|
+
"Escalate to user or change strategy.",
|
|
361
|
+
False,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Generate diagnostic
|
|
365
|
+
fix = _suggest_fix(name, args, result)
|
|
366
|
+
if fix is None:
|
|
367
|
+
return "", False
|
|
368
|
+
|
|
369
|
+
_increment_reflex_error_retry(cfg, name, args)
|
|
370
|
+
suggestion = fix.get("description", "Try again with modified approach")
|
|
371
|
+
return (
|
|
372
|
+
f"Reflex error-interceptor: {name} failed with: {summarize_tool_result(result)}\n"
|
|
373
|
+
f"Diagnostic: {suggestion}\n"
|
|
374
|
+
f"Retry {_reflex_error_retries(cfg, name, args)}/{REFLEX_ERROR_MAX_RETRIES}",
|
|
375
|
+
True,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def reset_error_retries(cfg: Any) -> None:
|
|
380
|
+
"""Clear all error-retry counters for a fresh session or /agent run."""
|
|
381
|
+
state = getattr(cfg, "context_state", None)
|
|
382
|
+
if isinstance(state, dict):
|
|
383
|
+
keys_to_remove = [k for k in state if k.startswith(str((REFLEX_ERROR_LEDGER_KEY,)))]
|
|
384
|
+
for key in keys_to_remove:
|
|
385
|
+
state.pop(key, None)
|