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.
Files changed (58) hide show
  1. {alpiecode-10.7.0 → alpiecode-10.7.2}/PKG-INFO +1 -1
  2. {alpiecode-10.7.0 → alpiecode-10.7.2}/pyproject.toml +1 -1
  3. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/__init__.py +1 -1
  5. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/local_backend.py +27 -13
  6. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/openai_backend.py +1 -1
  7. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/cli.py +3 -2
  8. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/config.py +7 -3
  9. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/context.py +2 -2
  10. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/discovery.py +23 -3
  11. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/orchestrator.py +16 -24
  12. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_non_thinking_and_local_stream.py +120 -0
  13. {alpiecode-10.7.0 → alpiecode-10.7.2}/README.md +0 -0
  14. {alpiecode-10.7.0 → alpiecode-10.7.2}/setup.cfg +0 -0
  15. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode/__init__.py +0 -0
  16. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  17. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  18. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
  19. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/requires.txt +0 -0
  20. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/alpiecode.egg-info/top_level.txt +0 -0
  21. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/agent.py +0 -0
  22. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/assessment.py +0 -0
  23. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/__init__.py +0 -0
  24. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/backends/base.py +0 -0
  25. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/cache.py +0 -0
  26. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/client.py +0 -0
  27. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/compaction.py +0 -0
  28. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/doctor.py +0 -0
  29. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/dream_rsi.py +0 -0
  30. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/executor.py +0 -0
  31. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/extension/alpiecode.vsix +0 -0
  32. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/git_ops.py +0 -0
  33. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/github.py +0 -0
  34. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/guardian.py +0 -0
  35. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/guardrails.py +0 -0
  36. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/ipython_ext.py +0 -0
  37. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/local_model.py +0 -0
  38. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/media.py +0 -0
  39. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/memory.py +0 -0
  40. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/metrics.py +0 -0
  41. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/paths.py +0 -0
  42. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/progress.py +0 -0
  43. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/prompt.py +0 -0
  44. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/rephraser.py +0 -0
  45. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/server.py +0 -0
  46. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/session.py +0 -0
  47. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/system1.py +0 -0
  48. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/tools.py +0 -0
  49. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/updater.py +0 -0
  50. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/verifier.py +0 -0
  51. {alpiecode-10.7.0 → alpiecode-10.7.2}/src/codeagent/vscode_installer.py +0 -0
  52. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_adaptive_resilience.py +0 -0
  53. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_cli_ui.py +0 -0
  54. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_context_and_trajectory.py +0 -0
  55. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_dream_rsi.py +0 -0
  56. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_local_model_download.py +0 -0
  57. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_task_assessment.py +0 -0
  58. {alpiecode-10.7.0 → alpiecode-10.7.2}/test/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.7.0
3
+ Version: 10.7.2
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "10.7.0"
7
+ version = "10.7.2"
8
8
  description = "AlpieCode"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.7.0
3
+ Version: 10.7.2
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.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.6.9"
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=90.0)
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=90.0)
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
- mmprojs = list(model_path.parent.glob("*mmproj*.gguf"))
1060
- cmd = [
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", "2048",
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 mmprojs:
1073
- cmd += ["--mmproj", str(mmprojs[0])]
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 local llama-server in background ({model_path.name})...")
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(35):
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(180.0, connect=5.0, read=180.0, write=30.0),
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
- cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 8192))
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
- # For offline mode, set high-performance 16k context (16384 tokens) for big multi-file projects
237
- # Fits completely in 6GB VRAM on RTX 3050 with zero paging
238
- if data.get("base_url") == "offline" and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
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(1024, effective_limit - reserved_headroom)
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 = ["palindrome", "fibonacci", "quicksort", "bubblesort", "factorial", "hello world", "calculator", "reverse string", "reverse_string"]
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
- for p in workdir.rglob("*"):
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 turn >= 1:
457
- existing_files = set(session_touched_files)
458
- try:
459
- for ep in session.workdir.rglob("*"):
460
- if ep.is_file() and not any(p in ep.parts for p in (".git", "node_modules", ".venv", "__pycache__", ".alpiecode")):
461
- if ep.name != "walkthrough.md":
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 = 1000
470
+ used_tokens = 600
482
471
 
483
- # If headroom is tight (< 1500 tokens), proactively compact context
484
- if n_ctx - used_tokens - tools_overhead < 1500 and hasattr(session.context, "compact_for_headroom"):
485
- used_tokens = session.context.compact_for_headroom(target_headroom=2048, n_ctx=n_ctx, tools_overhead=tools_overhead)
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(512, n_ctx - used_tokens - tools_overhead - 64)
488
- max_tokens = min(4096, available_headroom)
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