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/tool_runtime.py
ADDED
|
@@ -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
|