alpiecode 10.7.0__tar.gz → 10.7.2__tar.gz
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.
- {alpiecode-10.7.0 → alpiecode-10.7.2}/PKG-INFO +1 -1
- {alpiecode-10.7.0 → alpiecode-10.7.2}/pyproject.toml +1 -1
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/local_backend.py +27 -13
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/openai_backend.py +1 -1
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/cli.py +3 -2
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/config.py +7 -3
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/context.py +2 -2
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/discovery.py +23 -3
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/orchestrator.py +16 -24
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_non_thinking_and_local_stream.py +120 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/README.md +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/setup.cfg +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/agent.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/assessment.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/cache.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/client.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/dream_rsi.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/executor.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/github.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/local_model.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/media.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/memory.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/metrics.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/paths.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/progress.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/prompt.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/server.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/session.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/system1.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/tools.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/updater.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/verifier.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_adaptive_resilience.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_cli_ui.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_context_and_trajectory.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_dream_rsi.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_local_model_download.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_task_assessment.py +0 -0
- {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_verifier.py +0 -0
|
@@ -17,7 +17,7 @@ Supports:
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any, Dict
|
|
19
19
|
|
|
20
|
-
__version__ = "10.
|
|
20
|
+
__version__ = "10.7.2"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -439,7 +439,7 @@ class LocalBackend:
|
|
|
439
439
|
"max_tokens": max_tokens,
|
|
440
440
|
"stream": False,
|
|
441
441
|
}
|
|
442
|
-
if tools:
|
|
442
|
+
if tools and getattr(self._cfg, "enable_native_tools", False):
|
|
443
443
|
payload["tools"] = tools
|
|
444
444
|
payload["tool_choice"] = "auto"
|
|
445
445
|
client = httpx.Client(http2=False, timeout=120.0)
|
|
@@ -496,7 +496,7 @@ class LocalBackend:
|
|
|
496
496
|
"max_tokens": max_tokens or 2048,
|
|
497
497
|
"stream": True,
|
|
498
498
|
}
|
|
499
|
-
if tools:
|
|
499
|
+
if tools and getattr(self._cfg, "enable_native_tools", False):
|
|
500
500
|
payload["tools"] = tools
|
|
501
501
|
payload["tool_choice"] = "auto"
|
|
502
502
|
client = httpx.Client(http2=False, timeout=120.0)
|
|
@@ -874,10 +874,10 @@ class iOSBackend(BasePlatformBackend):
|
|
|
874
874
|
"max_tokens": max_tokens,
|
|
875
875
|
"stream": False,
|
|
876
876
|
}
|
|
877
|
-
if tools:
|
|
877
|
+
if tools and getattr(self._cfg, "enable_native_tools", False):
|
|
878
878
|
payload["tools"] = tools
|
|
879
879
|
payload["tool_choice"] = "auto"
|
|
880
|
-
resp = httpx.post(f"{bridge_url}/v1/chat/completions", json=payload, timeout=
|
|
880
|
+
resp = httpx.post(f"{bridge_url}/v1/chat/completions", json=payload, timeout=300.0)
|
|
881
881
|
if resp.status_code != 200:
|
|
882
882
|
raise RuntimeError(f"iOS bridge HTTP {resp.status_code}: {resp.text}")
|
|
883
883
|
data = json.loads(resp.text)
|
|
@@ -953,10 +953,10 @@ class iOSBackend(BasePlatformBackend):
|
|
|
953
953
|
"max_tokens": max_tokens or 2048,
|
|
954
954
|
"stream": True,
|
|
955
955
|
}
|
|
956
|
-
if tools:
|
|
956
|
+
if tools and getattr(self._cfg, "enable_native_tools", False):
|
|
957
957
|
payload["tools"] = tools
|
|
958
958
|
payload["tool_choice"] = "auto"
|
|
959
|
-
client = httpx.Client(timeout=
|
|
959
|
+
client = httpx.Client(timeout=httpx.Timeout(300.0, connect=10.0, read=300.0, write=30.0))
|
|
960
960
|
req = client.build_request("POST", f"{bridge_url}/v1/chat/completions", json=payload)
|
|
961
961
|
resp = client.send(req, stream=True)
|
|
962
962
|
if resp.status_code != 200:
|
|
@@ -1056,11 +1056,14 @@ class AndroidBackend(BasePlatformBackend):
|
|
|
1056
1056
|
if not model_path:
|
|
1057
1057
|
self._last_server_error = "No GGUF model found in ~/.alpiecode/models or ~/models."
|
|
1058
1058
|
return False
|
|
1059
|
-
|
|
1060
|
-
|
|
1059
|
+
n_ctx = getattr(self._cfg, "n_ctx", 2048)
|
|
1060
|
+
if n_ctx > 2048:
|
|
1061
|
+
n_ctx = 2048
|
|
1062
|
+
n_ctx_val = str(n_ctx)
|
|
1063
|
+
base_cmd = [
|
|
1061
1064
|
str(server_bin),
|
|
1062
1065
|
"-m", str(model_path),
|
|
1063
|
-
"-c",
|
|
1066
|
+
"-c", n_ctx_val,
|
|
1064
1067
|
"-t", "4",
|
|
1065
1068
|
"-np", "1",
|
|
1066
1069
|
"-b", "512",
|
|
@@ -1069,10 +1072,21 @@ class AndroidBackend(BasePlatformBackend):
|
|
|
1069
1072
|
"--cache-type-v", "q8_0",
|
|
1070
1073
|
"--port", "8080",
|
|
1071
1074
|
]
|
|
1072
|
-
if
|
|
1073
|
-
|
|
1075
|
+
# Only load vision projector if specifically needed to keep RAM footprint under 5GB
|
|
1076
|
+
if getattr(self._cfg, "enable_vision", False):
|
|
1077
|
+
mmprojs = list(model_path.parent.glob("*mmproj*.gguf"))
|
|
1078
|
+
if mmprojs:
|
|
1079
|
+
base_cmd += ["--mmproj", str(mmprojs[0])]
|
|
1080
|
+
|
|
1081
|
+
taskset_bin = shutil.which("taskset")
|
|
1082
|
+
if taskset_bin and os.cpu_count() == 8:
|
|
1083
|
+
cmd = [taskset_bin, "-c", "4-7"] + base_cmd
|
|
1084
|
+
else:
|
|
1085
|
+
cmd = base_cmd
|
|
1086
|
+
|
|
1074
1087
|
try:
|
|
1075
|
-
print(f"⏳ Starting
|
|
1088
|
+
print(f"⏳ Starting Alpie ({model_path.name}) in background...")
|
|
1089
|
+
print(f" Context window: {n_ctx_val} tokens | Performance cores: 4 | Lean KV cache active")
|
|
1076
1090
|
subprocess.Popen(
|
|
1077
1091
|
cmd,
|
|
1078
1092
|
stdout=subprocess.DEVNULL,
|
|
@@ -1080,7 +1094,7 @@ class AndroidBackend(BasePlatformBackend):
|
|
|
1080
1094
|
start_new_session=True,
|
|
1081
1095
|
)
|
|
1082
1096
|
import time
|
|
1083
|
-
for _ in range(
|
|
1097
|
+
for _ in range(40):
|
|
1084
1098
|
if self._is_bridge_online():
|
|
1085
1099
|
return True
|
|
1086
1100
|
time.sleep(0.5)
|
|
@@ -180,7 +180,7 @@ class _HTTPXClient:
|
|
|
180
180
|
elif "127.0.0.1" in base_url or "localhost" in base_url:
|
|
181
181
|
self.http_client = httpx.Client(
|
|
182
182
|
http2=False,
|
|
183
|
-
timeout=httpx.Timeout(
|
|
183
|
+
timeout=httpx.Timeout(300.0, connect=10.0, read=300.0, write=30.0),
|
|
184
184
|
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
|
185
185
|
)
|
|
186
186
|
else:
|
|
@@ -263,9 +263,10 @@ def main():
|
|
|
263
263
|
|
|
264
264
|
if getattr(args, "offline", False):
|
|
265
265
|
cfg.base_url = "offline"
|
|
266
|
-
# 8192 default fits 4.4GB model + KV cache 100% in 6GB VRAM (4.75GB / 6GB) with zero host-paging
|
|
267
266
|
import os
|
|
268
|
-
|
|
267
|
+
from .paths import is_android
|
|
268
|
+
default_ctx = 2048 if is_android() else 8192
|
|
269
|
+
cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", default_ctx))
|
|
269
270
|
os.environ["ALPIECODE_BASE_URL"] = "offline"
|
|
270
271
|
|
|
271
272
|
if getattr(args, "ctx_size", None):
|
|
@@ -233,9 +233,13 @@ def load_config() -> Config:
|
|
|
233
233
|
except ValueError:
|
|
234
234
|
pass
|
|
235
235
|
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
# On mobile (Android / Termux), keep context at 2048 to prevent ZRAM thrashing with 9B models
|
|
237
|
+
if is_android() and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
|
|
238
|
+
if data.get("n_ctx", 32768) > 2048:
|
|
239
|
+
data["n_ctx"] = 2048
|
|
240
|
+
if data.get("max_tokens", 8192) > 1024:
|
|
241
|
+
data["max_tokens"] = 1024
|
|
242
|
+
elif data.get("base_url") == "offline" and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
|
|
239
243
|
if data.get("n_ctx") in (32768, 4096, 16384):
|
|
240
244
|
data["n_ctx"] = 8192
|
|
241
245
|
if data.get("max_tokens", 8192) > 4096:
|
|
@@ -379,8 +379,8 @@ class ContextManager:
|
|
|
379
379
|
context_blocks.extend(recent_messages)
|
|
380
380
|
|
|
381
381
|
# ── Budget Verification ──
|
|
382
|
-
reserved_headroom = 2048 if effective_limit <= 8192 else 4096
|
|
383
|
-
prompt_budget = max(
|
|
382
|
+
reserved_headroom = 512 if effective_limit <= 2048 else (2048 if effective_limit <= 8192 else 4096)
|
|
383
|
+
prompt_budget = max(512, effective_limit - reserved_headroom)
|
|
384
384
|
|
|
385
385
|
if estimate_tokens(context_blocks) > prompt_budget:
|
|
386
386
|
context_blocks = compact_messages(context_blocks, keep_recent=4 if effective_limit <= 8192 else 8)
|
|
@@ -490,6 +490,15 @@ COMPLEXITY_CONFIG = {
|
|
|
490
490
|
|
|
491
491
|
# -- Pre-Read Context Injection ---------------------------------------------
|
|
492
492
|
|
|
493
|
+
def _is_home_or_root(path: Path) -> bool:
|
|
494
|
+
"""Check if path is the user home directory or system root."""
|
|
495
|
+
try:
|
|
496
|
+
resolved = path.resolve()
|
|
497
|
+
return resolved in (Path.home().resolve(), Path("/").resolve(), Path("C:\\").resolve())
|
|
498
|
+
except Exception:
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
|
|
493
502
|
def gather_relevant_context(
|
|
494
503
|
task: str,
|
|
495
504
|
workdir: Path,
|
|
@@ -512,8 +521,12 @@ def gather_relevant_context(
|
|
|
512
521
|
|
|
513
522
|
task_lower = task.lower().strip()
|
|
514
523
|
|
|
515
|
-
# Skip pre-reading repo files for standalone algorithms or scripts (e.g. palindrome, calculator)
|
|
516
|
-
standalone_kws = [
|
|
524
|
+
# Skip pre-reading repo files for standalone algorithms or scripts (e.g. palindrome, calculator, primes)
|
|
525
|
+
standalone_kws = [
|
|
526
|
+
"palindrome", "fibonacci", "quicksort", "bubblesort", "factorial",
|
|
527
|
+
"hello world", "calculator", "reverse string", "reverse_string",
|
|
528
|
+
"prime", "primes", "prime number", "prime numbers",
|
|
529
|
+
]
|
|
517
530
|
if any(kw in task_lower for kw in standalone_kws):
|
|
518
531
|
return None
|
|
519
532
|
sections: List[str] = []
|
|
@@ -577,7 +590,9 @@ def gather_relevant_context(
|
|
|
577
590
|
task_lower = task.lower()
|
|
578
591
|
try:
|
|
579
592
|
all_files = []
|
|
580
|
-
|
|
593
|
+
is_broad = _is_home_or_root(workdir)
|
|
594
|
+
file_iter = workdir.glob("*") if is_broad else workdir.rglob("*")
|
|
595
|
+
for p in file_iter:
|
|
581
596
|
if p.is_file() and not any(
|
|
582
597
|
skip in str(p) for skip in [
|
|
583
598
|
".git/", "__pycache__", "node_modules", ".venv",
|
|
@@ -585,6 +600,8 @@ def gather_relevant_context(
|
|
|
585
600
|
]
|
|
586
601
|
):
|
|
587
602
|
all_files.append(str(p.relative_to(workdir)))
|
|
603
|
+
if len(all_files) >= 500:
|
|
604
|
+
break
|
|
588
605
|
# Match filenames or stems mentioned in the task
|
|
589
606
|
for rel in all_files:
|
|
590
607
|
fname = Path(rel).stem.lower()
|
|
@@ -609,6 +626,9 @@ def build_symbol_index(workdir: Path, max_files: int = 40) -> Dict[str, List[str
|
|
|
609
626
|
|
|
610
627
|
Extracts classes, methods, and functions across Python files.
|
|
611
628
|
"""
|
|
629
|
+
if _is_home_or_root(workdir):
|
|
630
|
+
return {}
|
|
631
|
+
|
|
612
632
|
symbols: Dict[str, List[str]] = {}
|
|
613
633
|
count = 0
|
|
614
634
|
import ast
|
|
@@ -453,39 +453,31 @@ class AgentOrchestrator:
|
|
|
453
453
|
yield AgentEvent("turn_start", {"turn": turn, "thinking": turn_enable_thinking})
|
|
454
454
|
|
|
455
455
|
# In offline mode, guide the model with current workspace progress
|
|
456
|
-
if is_offline and
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
rel = ep.relative_to(session.workdir).as_posix()
|
|
463
|
-
existing_files.add(rel)
|
|
464
|
-
except Exception:
|
|
465
|
-
pass
|
|
466
|
-
if existing_files:
|
|
467
|
-
file_list = ", ".join(sorted(list(existing_files)))
|
|
468
|
-
state_hint = f"[Workspace State: Files created so far: {file_list}. Do NOT re-create these files. Proceed with remaining files or tests.]"
|
|
469
|
-
last_msgs = session.context.messages
|
|
470
|
-
if last_msgs and not any(state_hint in str(m.get("content", "")) for m in last_msgs[-2:]):
|
|
471
|
-
session.context.add_user_message(state_hint)
|
|
456
|
+
if is_offline and session_touched_files:
|
|
457
|
+
file_list = ", ".join(sorted(list(session_touched_files)))
|
|
458
|
+
state_hint = f"[Workspace State: Files created so far: {file_list}. Do NOT re-create these files. Proceed with remaining files or tests.]"
|
|
459
|
+
last_msgs = session.context.messages
|
|
460
|
+
if last_msgs and not any(state_hint in str(m.get("content", "")) for m in last_msgs[-2:]):
|
|
461
|
+
session.context.add_user_message(state_hint)
|
|
472
462
|
|
|
473
463
|
try:
|
|
474
464
|
if is_offline:
|
|
475
465
|
# In offline mode, dynamically allocate output tokens based on context headroom
|
|
476
|
-
n_ctx = getattr(cfg, "n_ctx", 8192)
|
|
477
|
-
tools_overhead = 800 if active_tools else 0
|
|
466
|
+
n_ctx = getattr(cfg, "n_ctx", 2048 if getattr(session.workdir, "_is_mobile", False) else 8192)
|
|
478
467
|
try:
|
|
479
468
|
used_tokens = session.context.estimate_tokens()
|
|
480
469
|
except Exception:
|
|
481
|
-
used_tokens =
|
|
470
|
+
used_tokens = 600
|
|
482
471
|
|
|
483
|
-
#
|
|
484
|
-
if n_ctx
|
|
485
|
-
|
|
472
|
+
# Scale headroom threshold with context window (e.g. 512 for 2048 mobile ctx, 1500 for 8192 desktop ctx)
|
|
473
|
+
tight_headroom_threshold = 512 if n_ctx <= 2048 else 1500
|
|
474
|
+
if n_ctx - used_tokens < tight_headroom_threshold and hasattr(session.context, "compact_for_headroom"):
|
|
475
|
+
target_hr = 512 if n_ctx <= 2048 else 2048
|
|
476
|
+
used_tokens = session.context.compact_for_headroom(target_headroom=target_hr, n_ctx=n_ctx, tools_overhead=0)
|
|
486
477
|
|
|
487
|
-
available_headroom = max(
|
|
488
|
-
|
|
478
|
+
available_headroom = max(256, n_ctx - used_tokens - 32)
|
|
479
|
+
token_ceiling = 1024 if n_ctx <= 2048 else 4096
|
|
480
|
+
max_tokens = min(token_ceiling, available_headroom)
|
|
489
481
|
elif turn_enable_thinking:
|
|
490
482
|
max_tokens = max(effective_max_tokens, 16384)
|
|
491
483
|
else:
|
|
@@ -134,3 +134,123 @@ def test_prompt_builder_lean_offline_prompt():
|
|
|
134
134
|
assert "You are running in OFFLINE mode" in prompt
|
|
135
135
|
assert "Execution & Goal Convergence Rules" not in prompt
|
|
136
136
|
assert len(prompt) < 3500 # Lean offline prompt under 3500 chars (~600 tokens vs 4500 cloud tokens)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_android_backend_skips_mmproj_for_code_tasks(tmp_path):
|
|
140
|
+
from pathlib import Path
|
|
141
|
+
from codeagent.backends.local_backend import AndroidBackend
|
|
142
|
+
cfg = Config(base_url="offline")
|
|
143
|
+
backend = AndroidBackend(cfg)
|
|
144
|
+
|
|
145
|
+
fake_model = tmp_path / "alpie-vl-text-q3_k_m.gguf"
|
|
146
|
+
fake_model.touch()
|
|
147
|
+
fake_mmproj = tmp_path / "alpie-vl-mmproj-f16.gguf"
|
|
148
|
+
fake_mmproj.touch()
|
|
149
|
+
|
|
150
|
+
with patch.object(backend, "_find_llama_server_bin", return_value=Path("/bin/llama-server")):
|
|
151
|
+
with patch.object(backend, "_find_gguf_model", return_value=fake_model):
|
|
152
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
153
|
+
with patch.object(backend, "_is_bridge_online", side_effect=[False, True]):
|
|
154
|
+
backend._ensure_local_server()
|
|
155
|
+
assert mock_popen.called
|
|
156
|
+
args = mock_popen.call_args[0][0]
|
|
157
|
+
# By default (for code tasks), --mmproj must NOT be loaded
|
|
158
|
+
assert "--mmproj" not in args
|
|
159
|
+
assert "-c" in args
|
|
160
|
+
assert "2048" in args
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_offline_orchestrator_turn1_no_workspace_file_explosion(tmp_path):
|
|
164
|
+
"""Verify that offline orchestrator on Turn 1 does not inject thousands of workspace files."""
|
|
165
|
+
from codeagent.orchestrator import AgentOrchestrator
|
|
166
|
+
from codeagent.session import SessionManager
|
|
167
|
+
from codeagent.backends.base import ChatResponse
|
|
168
|
+
|
|
169
|
+
# Create dummy files in workspace
|
|
170
|
+
for i in range(50):
|
|
171
|
+
(tmp_path / f"file_{i}.txt").write_text("hello")
|
|
172
|
+
|
|
173
|
+
cfg = Config(base_url="offline", n_ctx=2048)
|
|
174
|
+
mock_backend = MagicMock()
|
|
175
|
+
mock_backend.name = "MockOffline"
|
|
176
|
+
mock_backend.is_available = True
|
|
177
|
+
mock_backend.is_offline = True
|
|
178
|
+
mock_backend.context_window = 2048
|
|
179
|
+
|
|
180
|
+
captured_messages = []
|
|
181
|
+
def mock_chat_stream(messages, **kwargs):
|
|
182
|
+
captured_messages.append(list(messages))
|
|
183
|
+
yield ("token", {"delta": "DONE: completed"})
|
|
184
|
+
yield ("done", ChatResponse(content="DONE: completed"))
|
|
185
|
+
|
|
186
|
+
mock_backend.chat_completion_stream = mock_chat_stream
|
|
187
|
+
|
|
188
|
+
orchestrator = AgentOrchestrator(mock_backend)
|
|
189
|
+
session_mgr = SessionManager()
|
|
190
|
+
session = session_mgr.create_session(tmp_path, max_tokens=2048)
|
|
191
|
+
|
|
192
|
+
events = list(orchestrator.run_task(
|
|
193
|
+
session=session,
|
|
194
|
+
task="create a python script that calculates prime numbers",
|
|
195
|
+
cfg=cfg,
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
# Verify that captured messages did NOT inject workspace file list
|
|
199
|
+
assert len(captured_messages) > 0
|
|
200
|
+
first_turn_msgs = captured_messages[0]
|
|
201
|
+
for msg in first_turn_msgs:
|
|
202
|
+
content_str = str(msg.get("content", ""))
|
|
203
|
+
assert "file_49.txt" not in content_str
|
|
204
|
+
assert "Workspace State: Files created so far" not in content_str
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_local_backend_bridge_omits_tools_payload():
|
|
208
|
+
"""Verify that LocalBackend bridge requests do not include tools payload by default."""
|
|
209
|
+
from codeagent.backends.local_backend import LocalBackend
|
|
210
|
+
cfg = Config(base_url="offline")
|
|
211
|
+
backend = LocalBackend(cfg)
|
|
212
|
+
|
|
213
|
+
with patch("httpx.Client") as mock_client_cls:
|
|
214
|
+
mock_client = MagicMock()
|
|
215
|
+
mock_resp = MagicMock()
|
|
216
|
+
mock_resp.status_code = 200
|
|
217
|
+
mock_resp.text = '{"choices":[{"message":{"content":"hello"}}]}'
|
|
218
|
+
mock_resp.iter_lines.return_value = ['data: {"choices":[{"delta":{"content":"hi"}}]}', 'data: [DONE]']
|
|
219
|
+
mock_client.build_request.return_value = MagicMock()
|
|
220
|
+
mock_client.send.return_value = mock_resp
|
|
221
|
+
mock_client.post.return_value = mock_resp
|
|
222
|
+
mock_client_cls.return_value = mock_client
|
|
223
|
+
|
|
224
|
+
with patch.object(backend, "_get_bridge_url", return_value="http://127.0.0.1:8080"):
|
|
225
|
+
# Stream call
|
|
226
|
+
list(backend._bridge_chat_completion_stream(
|
|
227
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
228
|
+
tools=[{"type": "function", "function": {"name": "test"}}],
|
|
229
|
+
))
|
|
230
|
+
_, kwargs = mock_client.build_request.call_args
|
|
231
|
+
assert "tools" not in kwargs["json"]
|
|
232
|
+
|
|
233
|
+
# Non-stream call
|
|
234
|
+
backend._bridge_chat_completion(
|
|
235
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
236
|
+
tools=[{"type": "function", "function": {"name": "test"}}],
|
|
237
|
+
)
|
|
238
|
+
_, kwargs = mock_client.post.call_args
|
|
239
|
+
assert "tools" not in kwargs["json"]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_discovery_guard_against_home_dir():
|
|
243
|
+
"""Verify that discovery functions safely handle home directory."""
|
|
244
|
+
from pathlib import Path
|
|
245
|
+
from codeagent.discovery import _is_home_or_root, build_symbol_index, gather_relevant_context, TaskContext
|
|
246
|
+
assert _is_home_or_root(Path.home()) is True
|
|
247
|
+
|
|
248
|
+
# build_symbol_index should return empty dict without scanning
|
|
249
|
+
assert build_symbol_index(Path.home()) == {}
|
|
250
|
+
|
|
251
|
+
# gather_relevant_context should return None for prime task
|
|
252
|
+
ctx = TaskContext(project_type="python", file_count=100)
|
|
253
|
+
res = gather_relevant_context("create a python script that calculates prime numbers", Path.home(), ctx)
|
|
254
|
+
assert res is None
|
|
255
|
+
|
|
256
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|