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/runtime_qos.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Runtime QoS and named log helpers (J6 + J7).
|
|
2
|
+
|
|
3
|
+
Inspired by macOS launchd ``POSIXSpawnType`` and ``StandardErrorPath``:
|
|
4
|
+
classify tools by expected runtime posture and give each tool a stable log path.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Generic, TypeVar
|
|
15
|
+
|
|
16
|
+
from .config import CONFIG_DIR
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpawnClass(str, Enum):
|
|
20
|
+
ADAPTIVE = "adaptive"
|
|
21
|
+
INTERACTIVE = "interactive"
|
|
22
|
+
BACKGROUND = "background"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class RuntimeHint:
|
|
27
|
+
tool_name: str
|
|
28
|
+
spawn_class: SpawnClass
|
|
29
|
+
log_path: str
|
|
30
|
+
log_suppression: bool
|
|
31
|
+
reason: str
|
|
32
|
+
estimated_cost: float
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict:
|
|
35
|
+
return {
|
|
36
|
+
"tool_name": self.tool_name,
|
|
37
|
+
"spawn_class": self.spawn_class.value,
|
|
38
|
+
"log_path": self.log_path,
|
|
39
|
+
"log_suppression": self.log_suppression,
|
|
40
|
+
"reason": self.reason,
|
|
41
|
+
"estimated_cost": self.estimated_cost,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ScheduledJob(Generic[T]):
|
|
50
|
+
key: str
|
|
51
|
+
payload: T
|
|
52
|
+
spawn_class: SpawnClass
|
|
53
|
+
estimated_cost: float
|
|
54
|
+
enqueued_at: float
|
|
55
|
+
virtual_finish: float
|
|
56
|
+
sequence: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class WeightedFairQueue(Generic[T]):
|
|
60
|
+
"""Small deterministic weighted-fair queue with starvation-safe aging."""
|
|
61
|
+
|
|
62
|
+
DEFAULT_WEIGHTS = {
|
|
63
|
+
SpawnClass.INTERACTIVE: 4.0,
|
|
64
|
+
SpawnClass.ADAPTIVE: 2.0,
|
|
65
|
+
SpawnClass.BACKGROUND: 1.0,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
weights: dict[SpawnClass, float] | None = None,
|
|
72
|
+
aging_rate: float = 0.05,
|
|
73
|
+
) -> None:
|
|
74
|
+
configured = dict(self.DEFAULT_WEIGHTS)
|
|
75
|
+
configured.update(weights or {})
|
|
76
|
+
self.weights = {key: max(0.01, float(value)) for key, value in configured.items()}
|
|
77
|
+
self.aging_rate = max(0.0, float(aging_rate))
|
|
78
|
+
self._class_finish = {spawn: 0.0 for spawn in SpawnClass}
|
|
79
|
+
self._jobs: list[ScheduledJob[T]] = []
|
|
80
|
+
self._sequence = 0
|
|
81
|
+
self._lock = threading.RLock()
|
|
82
|
+
|
|
83
|
+
def enqueue(
|
|
84
|
+
self,
|
|
85
|
+
key: str,
|
|
86
|
+
payload: T,
|
|
87
|
+
spawn_class: SpawnClass,
|
|
88
|
+
*,
|
|
89
|
+
estimated_cost: float = 1.0,
|
|
90
|
+
now: float | None = None,
|
|
91
|
+
) -> ScheduledJob[T]:
|
|
92
|
+
with self._lock:
|
|
93
|
+
if not self._jobs:
|
|
94
|
+
self._class_finish = {spawn: 0.0 for spawn in SpawnClass}
|
|
95
|
+
cost = max(0.01, float(estimated_cost))
|
|
96
|
+
finish = self._class_finish[spawn_class] + cost / self.weights[spawn_class]
|
|
97
|
+
self._class_finish[spawn_class] = finish
|
|
98
|
+
job = ScheduledJob(
|
|
99
|
+
key=str(key),
|
|
100
|
+
payload=payload,
|
|
101
|
+
spawn_class=spawn_class,
|
|
102
|
+
estimated_cost=cost,
|
|
103
|
+
enqueued_at=time.monotonic() if now is None else float(now),
|
|
104
|
+
virtual_finish=finish,
|
|
105
|
+
sequence=self._sequence,
|
|
106
|
+
)
|
|
107
|
+
self._sequence += 1
|
|
108
|
+
self._jobs.append(job)
|
|
109
|
+
return job
|
|
110
|
+
|
|
111
|
+
def pop(self, *, now: float | None = None) -> ScheduledJob[T] | None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
if not self._jobs:
|
|
114
|
+
return None
|
|
115
|
+
current = time.monotonic() if now is None else float(now)
|
|
116
|
+
|
|
117
|
+
def priority(job: ScheduledJob[T]) -> tuple[float, int]:
|
|
118
|
+
waited = max(0.0, current - job.enqueued_at)
|
|
119
|
+
return job.virtual_finish - self.aging_rate * waited, job.sequence
|
|
120
|
+
|
|
121
|
+
selected = min(self._jobs, key=priority)
|
|
122
|
+
self._jobs.remove(selected)
|
|
123
|
+
return selected
|
|
124
|
+
|
|
125
|
+
def __len__(self) -> int:
|
|
126
|
+
with self._lock:
|
|
127
|
+
return len(self._jobs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
SENSITIVE_TOOL_NAMES = frozenset({
|
|
131
|
+
"credential_helpers_get",
|
|
132
|
+
"credential_helpers_store",
|
|
133
|
+
"x_account_post",
|
|
134
|
+
"x_account_reply",
|
|
135
|
+
"x_account_post_action",
|
|
136
|
+
})
|
|
137
|
+
BACKGROUND_TOOL_NAMES = frozenset({
|
|
138
|
+
"harness_refresh",
|
|
139
|
+
"reindex_knowledge_graph",
|
|
140
|
+
"model_pull",
|
|
141
|
+
})
|
|
142
|
+
INTERACTIVE_TOOL_NAMES = frozenset({
|
|
143
|
+
"session_command",
|
|
144
|
+
"session_slash",
|
|
145
|
+
"available_actions",
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
_LOW_COST_TOOL_NAMES = frozenset({
|
|
149
|
+
"read_file",
|
|
150
|
+
"list_directory",
|
|
151
|
+
"search_files",
|
|
152
|
+
"find_unique_anchor",
|
|
153
|
+
"git_status",
|
|
154
|
+
"available_actions",
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _safe_tool_name(tool_name: str) -> str:
|
|
159
|
+
return re.sub(r"[^A-Za-z0-9_.-]+", "_", tool_name or "tool").strip("_") or "tool"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def named_tool_log_path(tool_name: str, *, log_root: Path | str | None = None, suppress: bool = False) -> Path:
|
|
163
|
+
"""Return a stable named log path for a tool, or /dev/null when explicitly suppressed."""
|
|
164
|
+
if suppress:
|
|
165
|
+
return Path("/dev/null")
|
|
166
|
+
root = Path(log_root).expanduser() if log_root is not None else CONFIG_DIR / "logs" / "tools"
|
|
167
|
+
safe = _safe_tool_name(tool_name)
|
|
168
|
+
stamp = time.strftime("%Y%m%d")
|
|
169
|
+
return root / safe / f"{stamp}.log"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def classify_tool_runtime(tool_name: str, args: dict[str, Any] | None = None) -> RuntimeHint:
|
|
173
|
+
"""Classify a tool call into a launchd-style runtime posture."""
|
|
174
|
+
name = (tool_name or "").strip()
|
|
175
|
+
args = args or {}
|
|
176
|
+
suppress = name in SENSITIVE_TOOL_NAMES or bool(args.get("suppress_log"))
|
|
177
|
+
if name in BACKGROUND_TOOL_NAMES:
|
|
178
|
+
spawn = SpawnClass.BACKGROUND
|
|
179
|
+
reason = "known long-running/background maintenance tool"
|
|
180
|
+
estimated_cost = 8.0
|
|
181
|
+
elif name in INTERACTIVE_TOOL_NAMES:
|
|
182
|
+
spawn = SpawnClass.INTERACTIVE
|
|
183
|
+
reason = "session/user-facing command"
|
|
184
|
+
estimated_cost = 1.0
|
|
185
|
+
elif name == "run_shell":
|
|
186
|
+
command = str(args.get("command", "")).lower()
|
|
187
|
+
if any(token in command for token in ("pytest", "ruff", "mypy", "npm test", "cargo test")):
|
|
188
|
+
spawn = SpawnClass.ADAPTIVE
|
|
189
|
+
reason = "verification command; adaptive runtime"
|
|
190
|
+
estimated_cost = 4.0
|
|
191
|
+
else:
|
|
192
|
+
spawn = SpawnClass.INTERACTIVE
|
|
193
|
+
reason = "shell command may require user-facing output"
|
|
194
|
+
estimated_cost = 2.0
|
|
195
|
+
elif name in _LOW_COST_TOOL_NAMES:
|
|
196
|
+
spawn = SpawnClass.ADAPTIVE
|
|
197
|
+
reason = "bounded read-only operation"
|
|
198
|
+
estimated_cost = 1.0
|
|
199
|
+
else:
|
|
200
|
+
spawn = SpawnClass.BACKGROUND if suppress else SpawnClass.ADAPTIVE
|
|
201
|
+
reason = "default runtime classification"
|
|
202
|
+
estimated_cost = 2.0
|
|
203
|
+
log_path = named_tool_log_path(name, suppress=suppress)
|
|
204
|
+
return RuntimeHint(name, spawn, str(log_path), suppress, reason, estimated_cost)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def order_tool_batch_by_qos(calls: list[tuple[str, dict[str, Any]]]) -> list[int]:
|
|
208
|
+
"""Return call indexes in weighted-fair dispatch order."""
|
|
209
|
+
scheduler: WeightedFairQueue[int] = WeightedFairQueue()
|
|
210
|
+
enqueued_at = time.monotonic()
|
|
211
|
+
for index, (name, args) in enumerate(calls):
|
|
212
|
+
hint = classify_tool_runtime(name, args)
|
|
213
|
+
scheduler.enqueue(
|
|
214
|
+
str(index),
|
|
215
|
+
index,
|
|
216
|
+
hint.spawn_class,
|
|
217
|
+
estimated_cost=hint.estimated_cost,
|
|
218
|
+
now=enqueued_at,
|
|
219
|
+
)
|
|
220
|
+
ordered: list[int] = []
|
|
221
|
+
while len(scheduler):
|
|
222
|
+
job = scheduler.pop(now=enqueued_at)
|
|
223
|
+
if job is not None:
|
|
224
|
+
ordered.append(job.payload)
|
|
225
|
+
return ordered
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
__all__ = [
|
|
229
|
+
"RuntimeHint",
|
|
230
|
+
"ScheduledJob",
|
|
231
|
+
"SpawnClass",
|
|
232
|
+
"WeightedFairQueue",
|
|
233
|
+
"classify_tool_runtime",
|
|
234
|
+
"named_tool_log_path",
|
|
235
|
+
"order_tool_batch_by_qos",
|
|
236
|
+
]
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Ollama / xAI clients, local server startup, and harness-gateway lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.error import URLError
|
|
16
|
+
from urllib.parse import urljoin
|
|
17
|
+
from urllib.request import Request, urlopen
|
|
18
|
+
|
|
19
|
+
from ollama import Client
|
|
20
|
+
|
|
21
|
+
from .config import Config, load_runtime_env
|
|
22
|
+
from . import harness
|
|
23
|
+
from . import model_info as _model_info_module
|
|
24
|
+
from . import xai_auth
|
|
25
|
+
from . import chatgpt_auth
|
|
26
|
+
from .display import show_error, show_info
|
|
27
|
+
from .model_routing import require_cloud_api_key, uses_ollama_cloud
|
|
28
|
+
|
|
29
|
+
LOCAL_STARTUP_TIMEOUT_SECONDS = 12
|
|
30
|
+
SERVER_READY_TTL_SECONDS = 5.0
|
|
31
|
+
GATEWAY_READY_TTL_SECONDS = 5.0
|
|
32
|
+
|
|
33
|
+
SERVER_READY_CACHE: dict[str, tuple[float, bool]] = {}
|
|
34
|
+
GATEWAY_PROCESS: subprocess.Popen[Any] | None = None
|
|
35
|
+
GATEWAY_READY_CACHE: dict[str, tuple[float, bool]] = {}
|
|
36
|
+
_TOOL_ENV_KEYS = ("OLLAMA_HOST", "ALGO_CLI_GATEWAY_URL", "OLLAMA_CLI_GATEWAY_URL")
|
|
37
|
+
_TOOL_ENV_CONDITION = threading.Condition(threading.RLock())
|
|
38
|
+
_TOOL_ENV_ACTIVE_VALUES: tuple[str, str, str] | None = None
|
|
39
|
+
_TOOL_ENV_ACTIVE_COUNT = 0
|
|
40
|
+
_TOOL_ENV_PREVIOUS: dict[str, str | None] = {}
|
|
41
|
+
_TOOL_ENV_THREAD_STATE = threading.local()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_client(cfg: Config) -> Any:
|
|
45
|
+
load_runtime_env(override=True)
|
|
46
|
+
if _model_info_module.is_xai_model(cfg.model):
|
|
47
|
+
from . import xai_client
|
|
48
|
+
|
|
49
|
+
return xai_client.active_xai_client()
|
|
50
|
+
if _model_info_module.is_chatgpt_model(cfg.model):
|
|
51
|
+
from . import chatgpt_client
|
|
52
|
+
|
|
53
|
+
return chatgpt_client.active_chatgpt_client()
|
|
54
|
+
timeout = max(1.0, float(cfg.chat_stream_timeout_seconds))
|
|
55
|
+
if uses_ollama_cloud(cfg):
|
|
56
|
+
require_cloud_api_key(cfg)
|
|
57
|
+
api_key = os.environ.get("OLLAMA_API_KEY", "").strip()
|
|
58
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
59
|
+
return Client(host="https://ollama.com", headers=headers, timeout=timeout)
|
|
60
|
+
return Client(host=cfg.host, timeout=timeout)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def client_for_model(model: str, cfg: Config, active_client: Any) -> Any:
|
|
64
|
+
if not model or model == cfg.model:
|
|
65
|
+
return active_client
|
|
66
|
+
load_runtime_env(override=True)
|
|
67
|
+
if _model_info_module.is_xai_model(model):
|
|
68
|
+
if not xai_auth.get_valid_token():
|
|
69
|
+
show_info(f"Agent block model {model} needs xAI OAuth; falling back to {cfg.model}.")
|
|
70
|
+
return active_client
|
|
71
|
+
from . import xai_client
|
|
72
|
+
|
|
73
|
+
return xai_client.active_xai_client()
|
|
74
|
+
if _model_info_module.is_chatgpt_model(model):
|
|
75
|
+
if not chatgpt_auth.get_valid_token():
|
|
76
|
+
show_info(f"Agent block model {model} needs ChatGPT OAuth; falling back to {cfg.model}.")
|
|
77
|
+
return active_client
|
|
78
|
+
from . import chatgpt_client
|
|
79
|
+
|
|
80
|
+
return chatgpt_client.active_chatgpt_client()
|
|
81
|
+
timeout = max(1.0, float(cfg.chat_stream_timeout_seconds))
|
|
82
|
+
if uses_ollama_cloud(cfg):
|
|
83
|
+
require_cloud_api_key(cfg)
|
|
84
|
+
api_key = os.environ.get("OLLAMA_API_KEY", "").strip()
|
|
85
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
86
|
+
return Client(host="https://ollama.com", headers=headers, timeout=timeout)
|
|
87
|
+
return Client(host=cfg.host, timeout=timeout)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def apply_tool_runtime_env(cfg: Config) -> None:
|
|
91
|
+
os.environ["OLLAMA_HOST"] = cfg.host
|
|
92
|
+
url = gateway_url()
|
|
93
|
+
os.environ["ALGO_CLI_GATEWAY_URL"] = url
|
|
94
|
+
os.environ["OLLAMA_CLI_GATEWAY_URL"] = url
|
|
95
|
+
if cfg.cloud:
|
|
96
|
+
load_runtime_env(override=True)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def scoped_tool_runtime_env(cfg: Config):
|
|
101
|
+
"""Lease process-global tool environment safely across agent threads.
|
|
102
|
+
|
|
103
|
+
Equal configurations share a reference-counted lease and remain concurrent.
|
|
104
|
+
Different configurations serialize because ``os.environ`` cannot represent
|
|
105
|
+
both values at once.
|
|
106
|
+
"""
|
|
107
|
+
global _TOOL_ENV_ACTIVE_VALUES, _TOOL_ENV_ACTIVE_COUNT, _TOOL_ENV_PREVIOUS
|
|
108
|
+
|
|
109
|
+
url = gateway_url()
|
|
110
|
+
desired = (str(cfg.host), url, url)
|
|
111
|
+
thread_depth = int(getattr(_TOOL_ENV_THREAD_STATE, "depth", 0))
|
|
112
|
+
with _TOOL_ENV_CONDITION:
|
|
113
|
+
if thread_depth and _TOOL_ENV_ACTIVE_VALUES != desired:
|
|
114
|
+
raise RuntimeError("nested tool runtime environments must use the same configuration")
|
|
115
|
+
while _TOOL_ENV_ACTIVE_COUNT and _TOOL_ENV_ACTIVE_VALUES != desired:
|
|
116
|
+
_TOOL_ENV_CONDITION.wait()
|
|
117
|
+
if _TOOL_ENV_ACTIVE_COUNT == 0:
|
|
118
|
+
_TOOL_ENV_PREVIOUS = {key: os.environ.get(key) for key in _TOOL_ENV_KEYS}
|
|
119
|
+
try:
|
|
120
|
+
apply_tool_runtime_env(cfg)
|
|
121
|
+
except BaseException:
|
|
122
|
+
for key, value in _TOOL_ENV_PREVIOUS.items():
|
|
123
|
+
if value is None:
|
|
124
|
+
os.environ.pop(key, None)
|
|
125
|
+
else:
|
|
126
|
+
os.environ[key] = value
|
|
127
|
+
_TOOL_ENV_PREVIOUS = {}
|
|
128
|
+
_TOOL_ENV_CONDITION.notify_all()
|
|
129
|
+
raise
|
|
130
|
+
_TOOL_ENV_ACTIVE_VALUES = desired
|
|
131
|
+
_TOOL_ENV_ACTIVE_COUNT += 1
|
|
132
|
+
_TOOL_ENV_THREAD_STATE.depth = thread_depth + 1
|
|
133
|
+
try:
|
|
134
|
+
yield
|
|
135
|
+
finally:
|
|
136
|
+
with _TOOL_ENV_CONDITION:
|
|
137
|
+
_TOOL_ENV_THREAD_STATE.depth = max(0, int(getattr(_TOOL_ENV_THREAD_STATE, "depth", 1)) - 1)
|
|
138
|
+
_TOOL_ENV_ACTIVE_COUNT -= 1
|
|
139
|
+
if _TOOL_ENV_ACTIVE_COUNT == 0:
|
|
140
|
+
for key, value in _TOOL_ENV_PREVIOUS.items():
|
|
141
|
+
if value is None:
|
|
142
|
+
os.environ.pop(key, None)
|
|
143
|
+
else:
|
|
144
|
+
os.environ[key] = value
|
|
145
|
+
_TOOL_ENV_PREVIOUS = {}
|
|
146
|
+
_TOOL_ENV_ACTIVE_VALUES = None
|
|
147
|
+
_TOOL_ENV_CONDITION.notify_all()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def host_is_local(host: str) -> bool:
|
|
151
|
+
normalized = host.lower()
|
|
152
|
+
return "localhost" in normalized or "127.0.0.1" in normalized or "[::1]" in normalized
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def ollama_server_ready(host: str) -> bool:
|
|
156
|
+
cached = SERVER_READY_CACHE.get(host)
|
|
157
|
+
now = time.time()
|
|
158
|
+
if cached and now - cached[0] <= SERVER_READY_TTL_SECONDS:
|
|
159
|
+
return cached[1]
|
|
160
|
+
try:
|
|
161
|
+
request = Request(urljoin(host.rstrip("/") + "/", "api/version"), method="GET")
|
|
162
|
+
with urlopen(request, timeout=1.5) as response:
|
|
163
|
+
ready = 200 <= response.status < 500
|
|
164
|
+
except (OSError, URLError, ValueError):
|
|
165
|
+
ready = False
|
|
166
|
+
SERVER_READY_CACHE[host] = (now, ready)
|
|
167
|
+
return ready
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def start_ollama_server(cfg: Config) -> bool:
|
|
171
|
+
if (
|
|
172
|
+
uses_ollama_cloud(cfg)
|
|
173
|
+
or _model_info_module.is_xai_model(cfg.model)
|
|
174
|
+
or _model_info_module.is_chatgpt_model(cfg.model)
|
|
175
|
+
or not host_is_local(cfg.host)
|
|
176
|
+
):
|
|
177
|
+
return True
|
|
178
|
+
return start_local_ollama_host(cfg.host)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def start_local_ollama_host(host: str) -> bool:
|
|
182
|
+
if not host_is_local(host):
|
|
183
|
+
return True
|
|
184
|
+
if ollama_server_ready(host):
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
show_info("Local Ollama server is not responding; starting `ollama serve`.")
|
|
188
|
+
try:
|
|
189
|
+
kwargs: dict[str, Any] = {
|
|
190
|
+
"stdout": subprocess.DEVNULL,
|
|
191
|
+
"stderr": subprocess.DEVNULL,
|
|
192
|
+
"stdin": subprocess.DEVNULL,
|
|
193
|
+
"start_new_session": True,
|
|
194
|
+
}
|
|
195
|
+
if sys.platform.startswith("win"):
|
|
196
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS
|
|
197
|
+
subprocess.Popen(["ollama", "serve"], **kwargs)
|
|
198
|
+
SERVER_READY_CACHE.pop(host, None)
|
|
199
|
+
except FileNotFoundError:
|
|
200
|
+
show_error("Could not find `ollama` on PATH. Install Ollama or add it to PATH.")
|
|
201
|
+
return False
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
show_error(f"Could not start Ollama server: {exc}")
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
deadline = time.time() + LOCAL_STARTUP_TIMEOUT_SECONDS
|
|
207
|
+
while time.time() < deadline:
|
|
208
|
+
if ollama_server_ready(host):
|
|
209
|
+
show_info("Local Ollama server is running.")
|
|
210
|
+
return True
|
|
211
|
+
time.sleep(0.5)
|
|
212
|
+
|
|
213
|
+
show_error(f"Ollama server did not become ready at {host}.")
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def gateway_url() -> str:
|
|
218
|
+
return (
|
|
219
|
+
os.environ.get("ALGO_CLI_GATEWAY_URL")
|
|
220
|
+
or os.environ.get("OLLAMA_CLI_GATEWAY_URL")
|
|
221
|
+
or "http://127.0.0.1:8765"
|
|
222
|
+
).rstrip("/")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def gateway_ready(url: str | None = None) -> bool:
|
|
226
|
+
url = (url or gateway_url()).rstrip("/")
|
|
227
|
+
cached = GATEWAY_READY_CACHE.get(url)
|
|
228
|
+
now = time.time()
|
|
229
|
+
if cached and now - cached[0] <= GATEWAY_READY_TTL_SECONDS:
|
|
230
|
+
return cached[1]
|
|
231
|
+
try:
|
|
232
|
+
request = Request(url + "/healthz", method="GET")
|
|
233
|
+
with urlopen(request, timeout=1.0) as response:
|
|
234
|
+
ready = 200 <= response.status < 500
|
|
235
|
+
except (OSError, URLError, ValueError):
|
|
236
|
+
ready = False
|
|
237
|
+
GATEWAY_READY_CACHE[url] = (now, ready)
|
|
238
|
+
return ready
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def gateway_source_dir() -> Path:
|
|
242
|
+
return Path(__file__).resolve().parents[1] / "harness-gateway"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def gateway_command() -> tuple[list[str], Path] | None:
|
|
246
|
+
configured = os.environ.get("ALGO_CLI_GATEWAY_BIN") or os.environ.get("OLLAMA_CLI_GATEWAY_BIN")
|
|
247
|
+
if configured:
|
|
248
|
+
path = Path(configured).expanduser()
|
|
249
|
+
if path.exists():
|
|
250
|
+
return [str(path)], path.parent
|
|
251
|
+
source_dir = gateway_source_dir()
|
|
252
|
+
exe_name = "harness-gateway.exe" if sys.platform.startswith("win") else "harness-gateway"
|
|
253
|
+
built = source_dir / exe_name
|
|
254
|
+
if built.exists():
|
|
255
|
+
return [str(built)], source_dir
|
|
256
|
+
go = shutil.which("go") or (r"C:\Program Files\Go\bin\go.exe" if Path(r"C:\Program Files\Go\bin\go.exe").exists() else "")
|
|
257
|
+
if go and source_dir.exists():
|
|
258
|
+
return [go, "run", "."], source_dir
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def start_supplemental_gateway(cfg: Config) -> bool:
|
|
263
|
+
global GATEWAY_PROCESS
|
|
264
|
+
if not uses_ollama_cloud(cfg):
|
|
265
|
+
return True
|
|
266
|
+
if not start_local_ollama_host(cfg.host):
|
|
267
|
+
return False
|
|
268
|
+
url = gateway_url()
|
|
269
|
+
if gateway_ready(url):
|
|
270
|
+
return True
|
|
271
|
+
command_info = gateway_command()
|
|
272
|
+
if command_info is None:
|
|
273
|
+
show_info(
|
|
274
|
+
"Supplemental gateway unavailable. Build `harness-gateway` or install Go "
|
|
275
|
+
"for local embedding/OCR helpers in cloud mode."
|
|
276
|
+
)
|
|
277
|
+
return False
|
|
278
|
+
command, cwd = command_info
|
|
279
|
+
startup_timeout = 45 if len(command) >= 2 and Path(command[0]).name.lower().startswith("go") and command[1] == "run" else 20
|
|
280
|
+
addr = url.removeprefix("http://").removeprefix("https://")
|
|
281
|
+
args = command + ["-addr", addr, "-index", str(harness.INDEX_PATH), "-ollama", cfg.host]
|
|
282
|
+
try:
|
|
283
|
+
kwargs: dict[str, Any] = {
|
|
284
|
+
"cwd": str(cwd),
|
|
285
|
+
"stdout": subprocess.DEVNULL,
|
|
286
|
+
"stderr": subprocess.DEVNULL,
|
|
287
|
+
"stdin": subprocess.DEVNULL,
|
|
288
|
+
}
|
|
289
|
+
if sys.platform.startswith("win"):
|
|
290
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
291
|
+
GATEWAY_PROCESS = subprocess.Popen(args, **kwargs)
|
|
292
|
+
GATEWAY_READY_CACHE.pop(url, None)
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
show_error(f"Could not start supplemental gateway: {exc}")
|
|
295
|
+
return False
|
|
296
|
+
deadline = time.time() + startup_timeout
|
|
297
|
+
while time.time() < deadline:
|
|
298
|
+
if gateway_ready(url):
|
|
299
|
+
show_info(f"Supplemental gateway is running at {url}.")
|
|
300
|
+
return True
|
|
301
|
+
time.sleep(0.5)
|
|
302
|
+
show_error(f"Supplemental gateway did not become ready at {url}.")
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def shutdown_supplemental_gateway() -> None:
|
|
307
|
+
global GATEWAY_PROCESS
|
|
308
|
+
if GATEWAY_PROCESS and GATEWAY_PROCESS.poll() is None:
|
|
309
|
+
try:
|
|
310
|
+
GATEWAY_PROCESS.terminate()
|
|
311
|
+
GATEWAY_PROCESS.wait(timeout=3)
|
|
312
|
+
except Exception:
|
|
313
|
+
try:
|
|
314
|
+
GATEWAY_PROCESS.kill()
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
GATEWAY_PROCESS = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
atexit.register(shutdown_supplemental_gateway)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Model-callable session slash commands (same behavior as TUI /read, /ls, /cd)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import tools as tools_module
|
|
9
|
+
from .workspace_resolver import parse_path_arg
|
|
10
|
+
|
|
11
|
+
MODEL_SLASH_COMMANDS: tuple[tuple[str, str], ...] = (
|
|
12
|
+
("/read", "Read a file relative to session cwd (deterministic). Args: /read PATH"),
|
|
13
|
+
("/ls", "List directory under cwd. Args: /ls or /ls SUBPATH"),
|
|
14
|
+
("/cd", "Change session cwd (saved). Args: /cd PATH"),
|
|
15
|
+
("/cwd", "Show current session cwd. No args."),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_ALLOWED = frozenset(cmd for cmd, _ in MODEL_SLASH_COMMANDS)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def catalog_for_prompt() -> str:
|
|
22
|
+
lines = [
|
|
23
|
+
"Slash commands are session controls, not normal prose. If you merely write '/command' in a reply, nothing runs.",
|
|
24
|
+
"Choose the right path:",
|
|
25
|
+
" 1. User typed a slash command: the CLI handles it before you see it; do not re-run it unless asked.",
|
|
26
|
+
" 2. Need deterministic cwd file navigation/read: call session_slash with the full line:",
|
|
27
|
+
*[f" - {cmd} — {desc}" for cmd, desc in MODEL_SLASH_COMMANDS],
|
|
28
|
+
" 3. Need to change/check Algo CLI session state (/status, /mode, /context, /harness refresh, /agent, etc.): call session_command with the full line. Read-only/status commands run without approval; state-changing commands may require approval.",
|
|
29
|
+
" 4. Need actual work: prefer model-callable tools (read_file/write_file/search_files/run_shell/web_search/etc.) over slash commands.",
|
|
30
|
+
"When to use common slash commands:",
|
|
31
|
+
" - /status or /info: check active model, cwd, context, and toggles before giving configuration advice.",
|
|
32
|
+
" - /mode execute|explore|publish: switch operating posture only when the user's task clearly needs it or they ask.",
|
|
33
|
+
" - /context status|rebuild|clear: inspect or repair compressed context when context quality/length is the issue.",
|
|
34
|
+
" - /reason status|guide: inspect the active reasoning posture or show mode guidance before changing it.",
|
|
35
|
+
" - /reason react|reflexion|tot|got|mcts|qcr|neuro_symbolic: change reasoning posture for complex work only; do not switch modes for routine reads/edits.",
|
|
36
|
+
" Use react for normal tool loops, reflexion after failed/partial attempts, tot/got/mcts for ambiguous multi-path planning, qcr for comparing candidate solutions, and neuro_symbolic for verification-heavy logic/code claims.",
|
|
37
|
+
" - /harness refresh: refresh indexed skills/wiki/memory after local harness files changed.",
|
|
38
|
+
" - /intel query TERM: inspect the local repository intelligence graph before code/navigation decisions.",
|
|
39
|
+
" Use /intel status to check availability; /intel reindex requires approval because it writes the graph index.",
|
|
40
|
+
" - Google Workspace commands are read-only and auth-gated; call available_actions(topic='google') before using /google-login, /google-callback, /google-status, or /google subcommands.",
|
|
41
|
+
" - /route TASK: preview the recommended Agent Blocks route before a complex task.",
|
|
42
|
+
" - /agent [--pipeline NAME] TASK: run a traceable multi-block pipeline for larger implementation/research/review work.",
|
|
43
|
+
" - /agent team [--roles ROLE,ROLE[,ROLE,ROLE]] TASK: delegate independent analysis to 2-4 read-only child threads, then integrate once with normal write and verification gates.",
|
|
44
|
+
" - /agent threads or /agent show THREAD: inspect run history; /agent resume THREAD [TASK] continues it and /agent fork THREAD TASK branches it.",
|
|
45
|
+
" Delegate only when work has genuinely independent angles. Keep mutations in the integration pipeline; never ask child threads to edit the same workspace.",
|
|
46
|
+
" - /kernel check [NAME]: verify kernel imports, slash routes, and active action contracts without executing workloads.",
|
|
47
|
+
"Do not use slash commands for file edits; use write_file. Do not use slash commands for build/test; use run_shell.",
|
|
48
|
+
"For the full command/tool catalog and focused examples, call available_actions(topic='slash').",
|
|
49
|
+
]
|
|
50
|
+
return "\n".join(lines)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def execute(command_line: str, cfg: Any, *, max_read_chars: int | None = None) -> str:
|
|
54
|
+
"""Run one whitelisted slash command against cfg.cwd."""
|
|
55
|
+
stripped = (command_line or "").strip()
|
|
56
|
+
if not stripped.startswith("/"):
|
|
57
|
+
stripped = f"/{stripped}"
|
|
58
|
+
if not stripped:
|
|
59
|
+
return "Error: empty command. Allowed: /read, /ls, /cd, /cwd"
|
|
60
|
+
|
|
61
|
+
parts = stripped.split(None, 1)
|
|
62
|
+
command = parts[0].lower()
|
|
63
|
+
remainder = stripped[len(parts[0]):].strip() if len(parts) > 1 else ""
|
|
64
|
+
|
|
65
|
+
if command not in _ALLOWED:
|
|
66
|
+
allowed = ", ".join(sorted(_ALLOWED))
|
|
67
|
+
return f"Error: {command} is not model-invokable. Allowed: {allowed}"
|
|
68
|
+
|
|
69
|
+
if command == "/cwd" or (command == "/cd" and not remainder):
|
|
70
|
+
return f"cwd: {getattr(cfg, 'cwd', '.')}"
|
|
71
|
+
|
|
72
|
+
if command == "/cd":
|
|
73
|
+
target = parse_path_arg(remainder)
|
|
74
|
+
path = Path(target).expanduser()
|
|
75
|
+
if not path.is_absolute():
|
|
76
|
+
path = Path(getattr(cfg, "cwd", ".")) / path
|
|
77
|
+
if path.exists() and path.is_dir():
|
|
78
|
+
cfg.cwd = str(path.resolve())
|
|
79
|
+
if hasattr(cfg, "save"):
|
|
80
|
+
cfg.save()
|
|
81
|
+
return f"cwd: {cfg.cwd}"
|
|
82
|
+
return f"Error: not a directory: {path}"
|
|
83
|
+
|
|
84
|
+
if command == "/ls":
|
|
85
|
+
rel = parse_path_arg(remainder) or "."
|
|
86
|
+
return tools_module.list_directory(rel, cwd=getattr(cfg, "cwd", None), limit=40)
|
|
87
|
+
|
|
88
|
+
if command == "/read":
|
|
89
|
+
rel = parse_path_arg(remainder)
|
|
90
|
+
if not rel:
|
|
91
|
+
return "Error: usage: /read PATH"
|
|
92
|
+
limit = max_read_chars if max_read_chars is not None else tools_module.MAX_READ_CHARS
|
|
93
|
+
return tools_module.read_file(rel, cwd=getattr(cfg, "cwd", None), max_chars=limit)
|
|
94
|
+
|
|
95
|
+
return f"Error: unhandled command {command}"
|