alpiecode 10.7.1__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.1 → alpiecode-10.7.2}/PKG-INFO +1 -1
  2. {alpiecode-10.7.1 → alpiecode-10.7.2}/pyproject.toml +1 -1
  3. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/__init__.py +1 -1
  5. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/backends/local_backend.py +4 -4
  6. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/cli.py +3 -2
  7. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/context.py +2 -2
  8. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/discovery.py +23 -3
  9. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/orchestrator.py +16 -24
  10. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_non_thinking_and_local_stream.py +95 -0
  11. {alpiecode-10.7.1 → alpiecode-10.7.2}/README.md +0 -0
  12. {alpiecode-10.7.1 → alpiecode-10.7.2}/setup.cfg +0 -0
  13. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode/__init__.py +0 -0
  14. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  15. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  16. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
  17. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode.egg-info/requires.txt +0 -0
  18. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/alpiecode.egg-info/top_level.txt +0 -0
  19. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/agent.py +0 -0
  20. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/assessment.py +0 -0
  21. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/backends/__init__.py +0 -0
  22. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/backends/base.py +0 -0
  23. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/backends/openai_backend.py +0 -0
  24. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/cache.py +0 -0
  25. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/client.py +0 -0
  26. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/compaction.py +0 -0
  27. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/config.py +0 -0
  28. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/doctor.py +0 -0
  29. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/dream_rsi.py +0 -0
  30. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/executor.py +0 -0
  31. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/extension/alpiecode.vsix +0 -0
  32. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/git_ops.py +0 -0
  33. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/github.py +0 -0
  34. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/guardian.py +0 -0
  35. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/guardrails.py +0 -0
  36. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/ipython_ext.py +0 -0
  37. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/local_model.py +0 -0
  38. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/media.py +0 -0
  39. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/memory.py +0 -0
  40. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/metrics.py +0 -0
  41. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/paths.py +0 -0
  42. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/progress.py +0 -0
  43. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/prompt.py +0 -0
  44. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/rephraser.py +0 -0
  45. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/server.py +0 -0
  46. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/session.py +0 -0
  47. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/system1.py +0 -0
  48. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/tools.py +0 -0
  49. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/updater.py +0 -0
  50. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/verifier.py +0 -0
  51. {alpiecode-10.7.1 → alpiecode-10.7.2}/src/codeagent/vscode_installer.py +0 -0
  52. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_adaptive_resilience.py +0 -0
  53. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_cli_ui.py +0 -0
  54. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_context_and_trajectory.py +0 -0
  55. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_dream_rsi.py +0 -0
  56. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_local_model_download.py +0 -0
  57. {alpiecode-10.7.1 → alpiecode-10.7.2}/test/test_task_assessment.py +0 -0
  58. {alpiecode-10.7.1 → 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.1
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.1"
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.1
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.7.1"
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,7 +874,7 @@ 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
880
  resp = httpx.post(f"{bridge_url}/v1/chat/completions", json=payload, timeout=300.0)
@@ -953,7 +953,7 @@ 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
959
  client = httpx.Client(timeout=httpx.Timeout(300.0, connect=10.0, read=300.0, write=30.0))
@@ -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):
@@ -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:
@@ -159,3 +159,98 @@ def test_android_backend_skips_mmproj_for_code_tasks(tmp_path):
159
159
  assert "-c" in args
160
160
  assert "2048" in args
161
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