Make test suite runnable from clean checkout + cross-platform vram_detect
CI / build (push) Has been cancelled
CI / build (push) Has been cancelled
runnable-test-suite (parent) — complete. Three sub-tasks all complete: - make-tests-runnable: add requirements.txt pinning pytest==7.4.4, sweep all docs/prompts from bare 'python' to 'python3' (stock macOS/Windows ships python3), add idempotent .venv install block to scripts/install.sh, and add 'from __future__ import annotations' to 3 dashboard modules using PEP 604 union syntax at definition time so they import on Python 3.9+. The PEP 604 bug was caught by the streak verifier itself during implementation. - vram-detect-cross-platform: scripts/vram_detect.py now branches on platform.system() for Linux/Darwin/Windows. macOS path uses system_profiler SPDisplaysDataType (Apple Silicon unified memory via sysctl hw.memsize; Intel Macs via 'VRAM (Total):'). Windows uses wmic path win32_VideoController get AdapterRAM with PowerShell fallback. Linux /proc/meminfo and nvidia-smi/lspci paths unchanged (regression test locks them). Added 14 local-LLM context-window entries (llama-3.1, qwen2.5, mistral, deepseek-r1/v3, glm-4/4.5, gemma-2, phi-3/4) with source-cited model cards. Added _probe_ollama_model() that runs 'ollama list' as a last-resort fallback. run_command() now wraps PowerShell cmdlets on Windows (['powershell', '-NoProfile', '-NoLogo', '-Command', ...]). - vram-detect-cross-platform-tests: 11 new monkeypatched tests in tests/test_vram_detect.py covering Linux/Darwin/Windows branches for detect_ram and detect_gpu_vram, prefix-match for unknown model names, ollama probe, Windows PowerShell wrapper, and a LOCKED regression test for _detect_ram_linux(). All external subprocess/sysctl/wmic calls are mocked; no live hardware probes. Suite total: 235 passed, 0 errors. Verified on this box: gpu_vram_gb 0 -> 32 on Apple M5 (32GB unified memory), target context correctly jumped 12k -> 42k. Subtask-2 implementation was authored by local LLM (gemma-4-26B-A4B-it via headroom proxy @ localhost:8787). The 10-consecutive-clean-pass streak verifier ran as the independent checker model (article #2/#9/#13 in 'WTF Is a Loop? Part 2'). One anti-spin rail fired: local LLM produced inline branches where subtask-3 tests expected private _detect_ram_linux() helper; extracted helper to match the test contract without weakening tests. Parent + all 3 subtasks complete. Prior opencode-subagent implementation of subtask-2 preserved in git stash for reference.
This commit is contained in:
+12
-5
@@ -6,10 +6,8 @@ FRAMEWORK_DIR="$HOME/.automaton"
|
||||
if [ -d "$FRAMEWORK_DIR" ]; then
|
||||
echo "automaton already installed at $FRAMEWORK_DIR"
|
||||
echo "Run './update.sh' to update."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Cloning automaton to $FRAMEWORK_DIR..."
|
||||
else
|
||||
echo "Cloning automaton to $FRAMEWORK_DIR..."
|
||||
git clone http://10.37.0.86:3003/hermes/automaton "$FRAMEWORK_DIR"
|
||||
|
||||
echo ""
|
||||
@@ -69,4 +67,13 @@ echo "These hooks block commits and pushes when no task is in an edit-allowed ph
|
||||
echo ""
|
||||
|
||||
# Register pre-edit guards for detected harnesses
|
||||
bash "$FRAMEWORK_DIR/scripts/register-guards.sh"
|
||||
bash "$FRAMEWORK_DIR/scripts/register-guards.sh"
|
||||
|
||||
fi
|
||||
|
||||
# --- Python deps (idempotent) ---
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
.venv/bin/pip install --quiet --upgrade pip
|
||||
.venv/bin/pip install --quiet -r requirements.txt
|
||||
+194
-33
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -44,6 +45,34 @@ MODEL_CONTEXT_WINDOWS: dict[str, int] = {
|
||||
"claude-3-haiku-20240307": 200_000,
|
||||
"claude-2": 200_000,
|
||||
"claude-2.1": 200_000,
|
||||
# Source: https://huggingface.co/meta-llama/Meta-Llama-3.1-8B (128k context, RoPE scaling)
|
||||
"llama-3.1-8b": 128_000,
|
||||
# Source: https://huggingface.co/meta-llama/Llama-3.3-70B-Instruct (128k context, RoPE scaling)
|
||||
"llama-3.3-70b": 128_000,
|
||||
# Source: https://huggingface.co/Qwen/Qwen2.5-7B-Instruct (128k context via YaRN)
|
||||
"qwen2.5-7b": 128_000,
|
||||
# Source: https://huggingface.co/Qwen/Qwen2.5-72B-Instruct (128k context via YaRN)
|
||||
"qwen2.5-72b": 128_000,
|
||||
# Source: https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3 (32k context)
|
||||
"mistral-7b": 32_000,
|
||||
# Source: https://huggingface.co/mistralai/Mistral-Large-Instruct-2411 (128k context)
|
||||
"mistral-large": 128_000,
|
||||
# Source: https://huggingface.co/deepseek-ai/DeepSeek-R1 (64k context, 128k claimed via YaRN; using conservative 64k)
|
||||
"deepseek-r1": 64_000,
|
||||
# Source: https://huggingface.co/deepseek-ai/DeepSeek-V3 (64k context, 128k claimed via YaRN; using conservative 64k)
|
||||
"deepseek-v3": 64_000,
|
||||
# Source: https://huggingface.co/THUDM/glm-4-9b-chat (128k context)
|
||||
"glm-4": 128_000,
|
||||
# Source: https://huggingface.co/THUDM/glm-4.5 (('128K-1M') context; using safe 128k)
|
||||
"glm-4.5": 128_000,
|
||||
# Source: https://huggingface.co/google/gemma-2-2b (8k context)
|
||||
"gemma-2": 8_000,
|
||||
# Source: https://huggingface.co/google/gemma-2-27b (8k context)
|
||||
"gemma-2-27b": 8_000,
|
||||
# Source: https://huggingface.co/microsoft/Phi-3-medium-128k-instruct (128k context)
|
||||
"phi-3": 128_000,
|
||||
# Source: https://huggingface.co/microsoft/phi-4 (16k context)
|
||||
"phi-4": 16_000,
|
||||
}
|
||||
|
||||
DEFAULT_FALLBACK_CONTEXT_TOKENS = 128_000
|
||||
@@ -53,7 +82,12 @@ MAX_CONFIG_READ_BYTES = 10 * 1024 # 10KB limit per prompt requirement
|
||||
|
||||
def run_command(cmd: list[str], timeout: float = 5.0) -> Optional[str]:
|
||||
"""Run a command and return stdout, or None on failure."""
|
||||
if not shutil.which(cmd[0]):
|
||||
if platform.system() == 'Windows':
|
||||
first_arg = cmd[0] if cmd else ''
|
||||
if first_arg.startswith('Get-') or 'CimInstance' in first_arg:
|
||||
cmd = ['powershell', '-NoProfile', '-NoLogo', '-Command', ' '.join(cmd)]
|
||||
|
||||
if not cmd or not shutil.which(cmd[0]):
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -74,35 +108,89 @@ def detect_gpu_vram() -> tuple[int, int, int]:
|
||||
"""Detect GPU VRAM and return (total_vram_kb, vram_per_gpu_kb, num_gpus)."""
|
||||
total_vram_kb = 0
|
||||
num_gpus = 0
|
||||
system = platform.system()
|
||||
|
||||
# Try nvidia-smi first.
|
||||
nvidia_output = run_command(
|
||||
["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
)
|
||||
if nvidia_output:
|
||||
lines = [line.strip() for line in nvidia_output.splitlines() if line.strip()]
|
||||
if lines:
|
||||
try:
|
||||
vram_mb = int(lines[0])
|
||||
if system == "Linux":
|
||||
nvidia_output = run_command(
|
||||
["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
)
|
||||
if nvidia_output:
|
||||
lines = [line.strip() for line in nvidia_output.splitlines() if line.strip()]
|
||||
if lines:
|
||||
try:
|
||||
vram_mb = int(lines[0])
|
||||
if vram_mb > 0:
|
||||
total_vram_kb = vram_mb * 1024
|
||||
num_gpus = len(lines)
|
||||
print("GPU: NVIDIA (nvidia-smi available)")
|
||||
print(f"VRAM per GPU: {vram_mb // 1024}GB ({vram_mb} MB)")
|
||||
print(f"Num GPUs: {num_gpus}")
|
||||
except ValueError:
|
||||
print("GPU: NVIDIA (nvidia-smi available but driver not responding)")
|
||||
|
||||
if total_vram_kb == 0:
|
||||
lspci_output = run_command(["lspci"])
|
||||
if lspci_output and re.search(r"VGA|3D|Display", lspci_output, re.IGNORECASE):
|
||||
print("GPU detected via lspci")
|
||||
vram_mb = _detect_amd_vram_from_lspci()
|
||||
if vram_mb > 0:
|
||||
total_vram_kb = vram_mb * 1024
|
||||
num_gpus = len(lines)
|
||||
print("GPU: NVIDIA (nvidia-smi available)")
|
||||
print(f"VRAM per GPU: {vram_mb // 1024}GB ({vram_mb} MB)")
|
||||
print(f"Num GPUs: {num_gpus}")
|
||||
except ValueError:
|
||||
print("GPU: NVIDIA (nvidia-smi available but driver not responding)")
|
||||
num_gpus = 1
|
||||
print(f"VRAM: {vram_mb} MB ({vram_mb // 1024}GB)")
|
||||
|
||||
# Fallback: lspci for AMD/others.
|
||||
if total_vram_kb == 0:
|
||||
lspci_output = run_command(["lspci"])
|
||||
if lspci_output and re.search(r"VGA|3D|Display", lspci_output, re.IGNORECASE):
|
||||
print("GPU detected via lspci")
|
||||
vram_mb = _detect_amd_vram_from_lspci()
|
||||
if vram_mb > 0:
|
||||
total_vram_kb = vram_mb * 1024
|
||||
num_gpus = 1
|
||||
print(f"VRAM: {vram_mb} MB ({vram_mb // 1024}GB)")
|
||||
elif system == "Darwin":
|
||||
sp_output = run_command(["system_profiler", "SPDisplaysDataType"])
|
||||
if sp_output:
|
||||
if re.search(r"Chipset Model: Apple M\d", sp_output):
|
||||
mem_output = run_command(["sysctl", "-n", "hw.memsize"])
|
||||
if mem_output:
|
||||
bytes_val = int(mem_output.strip())
|
||||
total_vram_kb = (bytes_val // 1024)
|
||||
num_gpus = 1
|
||||
print("GPU: Apple Silicon (unified memory)")
|
||||
print(f"Total System RAM (Shared VRAM): {total_vram_kb // 1024} MB")
|
||||
else:
|
||||
vram_match = re.search(r"VRAM \(Total\):\s*(\d+)\s*(GB|MB)", sp_output, re.IGNORECASE)
|
||||
if vram_match:
|
||||
val = int(vram_match.group(1))
|
||||
unit = vram_match.group(2).upper()
|
||||
if unit == "GB":
|
||||
total_vram_kb = val * 1024 * 1024
|
||||
else:
|
||||
total_vram_kb = val * 1024
|
||||
num_gpus = 1
|
||||
print("GPU: Intel Mac (dedicated VRAM)")
|
||||
print(f"VRAM: {val} {unit}")
|
||||
|
||||
elif system == "Windows":
|
||||
wmic_output = run_command(["wmic", "path", "win32_VideoController", "get", "AdapterRAM,Name", "/format:list"])
|
||||
if wmic_output:
|
||||
total_bytes = 0
|
||||
count = 0
|
||||
entries = re.split(r'(?=AdapterRAM=)', wmic_output)
|
||||
for entry in entries:
|
||||
if not entry.strip():
|
||||
continue
|
||||
ram_match = re.search(r"AdapterRAM=(\d+)", entry)
|
||||
if ram_match:
|
||||
total_bytes += int(ram_match.group(1))
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
total_vram_kb = total_bytes // 1024
|
||||
num_gpus = count
|
||||
print(f"GPU: Windows (WMIC detected {count} GPUs)")
|
||||
print(f"Total VRAM: {total_vram_kb // 1024} MB")
|
||||
else:
|
||||
ps_output = run_command(["powershell", "-NoProfile", "-Command", "Get-CimInstance Win32_VideoController -Property AdapterRAM"])
|
||||
if ps_output:
|
||||
ram_matches = re.findall(r"AdapterRAM=(\d+)", ps_output)
|
||||
if ram_matches:
|
||||
total_bytes = sum(int(x) for x in ram_matches)
|
||||
total_vram_kb = total_bytes // 1024
|
||||
num_gpus = len(ram_matches)
|
||||
print(f"GPU: Windows (PowerShell fallback detected {num_gpus} GPUs)")
|
||||
print(f"Total VRAM: {total_vram_kb // 1024} MB")
|
||||
|
||||
vram_per_gpu_kb = total_vram_kb // num_gpus if num_gpus > 0 else 0
|
||||
return total_vram_kb, vram_per_gpu_kb, num_gpus
|
||||
@@ -134,8 +222,8 @@ def _detect_amd_vram_from_lspci() -> int:
|
||||
return total_mb
|
||||
|
||||
|
||||
def detect_ram() -> tuple[int, int]:
|
||||
"""Detect total and available RAM in KB."""
|
||||
def _detect_ram_linux() -> tuple[int, int]:
|
||||
"""Linux-only RAM detection via /proc/meminfo. Returns (total_kb, available_kb)."""
|
||||
meminfo = Path("/proc/meminfo")
|
||||
if meminfo.exists():
|
||||
try:
|
||||
@@ -147,16 +235,59 @@ def detect_ram() -> tuple[int, int]:
|
||||
return total_kb, available_kb
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return 0, 0
|
||||
|
||||
sysctl_output = run_command(["sysctl", "-n", "hw.memsize"])
|
||||
if sysctl_output:
|
||||
try:
|
||||
|
||||
def _detect_ram_darwin() -> tuple[int, int]:
|
||||
"""macOS RAM detection via sysctl. Returns (total_kb, total_kb)."""
|
||||
try:
|
||||
sysctl_output = run_command(["sysctl", "-n", "hw.memsize"])
|
||||
if sysctl_output:
|
||||
total_kb = int(sysctl_output) // 1024
|
||||
if total_kb > 0:
|
||||
print("available RAM detection not supported on macOS, reporting total")
|
||||
print(f"RAM: {total_kb // 1024 // 1024}GB total")
|
||||
return total_kb, total_kb
|
||||
except ValueError:
|
||||
pass
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return 0, 0
|
||||
|
||||
|
||||
def _detect_ram_windows() -> tuple[int, int]:
|
||||
"""Windows RAM detection via wmic / PowerShell. Returns (total_kb, total_kb)."""
|
||||
try:
|
||||
wmic_output = run_command(["wmic", "ComputerSystem", "get", "TotalPhysicalMemory", "/format:list"])
|
||||
total_bytes = 0
|
||||
if wmic_output:
|
||||
for line in wmic_output.splitlines():
|
||||
if line.startswith("TotalPhysicalMemory="):
|
||||
total_bytes = int(line.split("=")[1])
|
||||
break
|
||||
|
||||
if total_bytes == 0:
|
||||
ps_output = run_command(["powershell", "-NoProfile", "-Command", "(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory"])
|
||||
if ps_output:
|
||||
total_bytes = int(ps_output.strip())
|
||||
|
||||
if total_bytes > 0:
|
||||
total_kb = total_bytes // 1024
|
||||
print(f"RAM: {total_kb // 1024 // 1024}GB total, {total_kb // 1024 // 1024}GB available")
|
||||
return total_kb, total_kb
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return 0, 0
|
||||
|
||||
|
||||
def detect_ram() -> tuple[int, int]:
|
||||
"""Detect total and available RAM in KB."""
|
||||
system = platform.system()
|
||||
|
||||
if system == "Linux":
|
||||
return _detect_ram_linux()
|
||||
elif system == "Darwin":
|
||||
return _detect_ram_darwin()
|
||||
elif system == "Windows":
|
||||
return _detect_ram_windows()
|
||||
|
||||
print("RAM: Could not detect")
|
||||
return 0, 0
|
||||
@@ -172,6 +303,30 @@ def _parse_meminfo_value(text: str, key: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _probe_ollama_model() -> Optional[str]:
|
||||
"""Probe local ollama list for the first running model name. Returns None on any failure."""
|
||||
try:
|
||||
if not shutil.which('ollama'):
|
||||
return None
|
||||
output = run_command(['ollama', 'list'], timeout=5.0)
|
||||
if not output:
|
||||
return None
|
||||
lines = output.strip().splitlines()
|
||||
data_rows = [line for line in lines if line.strip() and not line.startswith('NAME')]
|
||||
if not data_rows:
|
||||
return None
|
||||
first_row = data_rows[0]
|
||||
parts = first_row.split()
|
||||
if not parts:
|
||||
return None
|
||||
model_name = parts[0]
|
||||
if model_name.endswith(':latest'):
|
||||
model_name = model_name[:-len(':latest')]
|
||||
return model_name
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def detect_model_context(
|
||||
model_name: Optional[str] = None,
|
||||
project_dir: Optional[Path] = None,
|
||||
@@ -224,6 +379,12 @@ def detect_model_context(
|
||||
return override_context
|
||||
return _lookup_model_context(model)
|
||||
|
||||
# Try ollama probe (local LLMs via ollama).
|
||||
ollama_model = _probe_ollama_model()
|
||||
if ollama_model:
|
||||
print(f"Found model via ollama: {ollama_model}")
|
||||
return _lookup_model_context(ollama_model)
|
||||
|
||||
print("Model: Unknown (could not detect from .agent.md or config files)")
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user