alpiecode 9.0.4__tar.gz → 9.0.6__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 (51) hide show
  1. {alpiecode-9.0.4 → alpiecode-9.0.6}/PKG-INFO +1 -1
  2. {alpiecode-9.0.4 → alpiecode-9.0.6}/pyproject.toml +1 -1
  3. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/SOURCES.txt +3 -0
  5. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/agent.py +65 -10
  7. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/local_backend.py +18 -1
  8. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/cli.py +16 -2
  9. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/client.py +1 -1
  10. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/config.py +6 -4
  11. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/discovery.py +1 -0
  12. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/executor.py +18 -0
  13. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/extension/alpiecode.vsix +0 -0
  14. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/local_model.py +29 -1
  15. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/orchestrator.py +1 -0
  16. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/server.py +7 -1
  17. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/updater.py +8 -0
  18. alpiecode-9.0.6/test/test_audit_fixes.py +71 -0
  19. alpiecode-9.0.6/test/test_chat_mode.py +85 -0
  20. alpiecode-9.0.6/test/test_offline_mode.py +78 -0
  21. {alpiecode-9.0.4 → alpiecode-9.0.6}/README.md +0 -0
  22. {alpiecode-9.0.4 → alpiecode-9.0.6}/setup.cfg +0 -0
  23. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode/__init__.py +0 -0
  24. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  25. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/entry_points.txt +0 -0
  26. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/requires.txt +0 -0
  27. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/top_level.txt +0 -0
  28. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/__init__.py +0 -0
  29. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/base.py +0 -0
  30. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/openai_backend.py +0 -0
  31. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/cache.py +0 -0
  32. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/compaction.py +0 -0
  33. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/context.py +0 -0
  34. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/doctor.py +0 -0
  35. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/git_ops.py +0 -0
  36. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/github.py +0 -0
  37. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/guardian.py +0 -0
  38. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/guardrails.py +0 -0
  39. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/ipython_ext.py +0 -0
  40. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/media.py +0 -0
  41. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/memory.py +0 -0
  42. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/progress.py +0 -0
  43. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/prompt.py +0 -0
  44. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/rephraser.py +0 -0
  45. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/session.py +0 -0
  46. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/tools.py +0 -0
  47. {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/vscode_installer.py +0 -0
  48. {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_claude_codex_capabilities.py +0 -0
  49. {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_phase1.py +0 -0
  50. {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_phase2.py +0 -0
  51. {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_phase3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 9.0.4
3
+ Version: 9.0.6
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
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 = "9.0.4"
7
+ version = "9.0.6"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
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: 9.0.4
3
+ Version: 9.0.6
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -40,7 +40,10 @@ src/codeagent/backends/base.py
40
40
  src/codeagent/backends/local_backend.py
41
41
  src/codeagent/backends/openai_backend.py
42
42
  src/codeagent/extension/alpiecode.vsix
43
+ test/test_audit_fixes.py
44
+ test/test_chat_mode.py
43
45
  test/test_claude_codex_capabilities.py
46
+ test/test_offline_mode.py
44
47
  test/test_phase1.py
45
48
  test/test_phase2.py
46
49
  test/test_phase3.py
@@ -16,7 +16,7 @@ Supports:
16
16
  from pathlib import Path
17
17
  from typing import Optional, Any, Dict
18
18
 
19
- __version__ = "9.0.4"
19
+ __version__ = "9.0.6"
20
20
 
21
21
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
22
22
  """Run an autonomous coding task programmatically."""
@@ -213,11 +213,15 @@ def run_agent(
213
213
  # CLI-as-Client check: auto-detect if local/remote server is running
214
214
  from .client import AlpieCodeClient
215
215
  target_server = server_url or getattr(cfg, "server_url", None)
216
- if not target_server:
217
- # Check default local server
218
- local_client = AlpieCodeClient("http://127.0.0.1:7169")
219
- if local_client.health().get("status") == "online":
220
- target_server = "http://127.0.0.1:7169"
216
+ if not target_server and cfg.base_url != "offline" and not getattr(cfg, "offline", False):
217
+ # Check default local server (online mode only)
218
+ try:
219
+ local_client = AlpieCodeClient("http://127.0.0.1:7169")
220
+ h = local_client.health()
221
+ if h.get("status") == "online" and h.get("mode") != "offline":
222
+ target_server = "http://127.0.0.1:7169"
223
+ except Exception:
224
+ pass
221
225
 
222
226
  if target_server:
223
227
  client = AlpieCodeClient(target_server)
@@ -234,7 +238,7 @@ def run_agent(
234
238
  backend = resolve_backend(cfg)
235
239
  orchestrator = AgentOrchestrator(backend)
236
240
  session_mgr = SessionManager()
237
- session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
241
+ session = session_mgr.create_session(workdir, max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144))
238
242
  event_stream = orchestrator.run_task(
239
243
  session=session,
240
244
  task=task,
@@ -467,7 +471,7 @@ def run_agent(
467
471
  return session.context.messages if "session" in locals() else []
468
472
 
469
473
 
470
- def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
474
+ def run_chat(workdir: Path, cfg: Config, verbose: bool = True, debug: bool = False) -> None:
471
475
  """Run interactive chat session with Rich presentation."""
472
476
  workdir = workdir.resolve()
473
477
  _ensure_git(workdir)
@@ -475,7 +479,7 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
475
479
  backend = resolve_backend(cfg)
476
480
  orchestrator = AgentOrchestrator(backend)
477
481
  session_mgr = SessionManager()
478
- session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
482
+ session = session_mgr.create_session(workdir, max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144))
479
483
 
480
484
  is_offline = isinstance(backend, LocalBackend) or not backend.is_available
481
485
  active_tools = PromptBuilder().get_tools(is_offline=is_offline)
@@ -513,7 +517,50 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
513
517
  console.print("Goodbye! 👋")
514
518
  break
515
519
 
520
+ # In-chat slash commands
521
+ if user_input.lower() in ("/clear", "/reset"):
522
+ session = session_mgr.create_session(
523
+ workdir,
524
+ max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144)
525
+ )
526
+ if HAS_RICH:
527
+ console.print("[bold green]✨ Conversation context reset.[/bold green]")
528
+ else:
529
+ print("✨ Conversation context reset.")
530
+ continue
531
+
532
+ if user_input.lower() in ("/info", "/status"):
533
+ mode_str = "OFFLINE (Local GGUF)" if is_offline else "ONLINE (Cloud API)"
534
+ ctx_tokens = backend.context_window if hasattr(backend, 'context_window') and backend.context_window else cfg.n_ctx
535
+ user_turns = len([m for m in session.context.messages if m.get("role") == "user"])
536
+ info_msg = (
537
+ f"[bold]AlpieCode Session Info[/bold]\n"
538
+ f" • Mode: {mode_str}\n"
539
+ f" • Working Dir: {workdir}\n"
540
+ f" • Context Window: {ctx_tokens} tokens\n"
541
+ f" • Session Turns: {user_turns}"
542
+ )
543
+ if HAS_RICH:
544
+ console.print(Panel(info_msg, border_style="cyan", title="ℹ️ Status"))
545
+ else:
546
+ print(info_msg)
547
+ continue
548
+
549
+ if user_input.lower() in ("/help", "/?"):
550
+ help_msg = (
551
+ "Available commands:\n"
552
+ " • /clear, /reset - Reset conversation memory\n"
553
+ " • /info, /status - View current model and session diagnostics\n"
554
+ " • exit, quit, q - Exit interactive chat"
555
+ )
556
+ if HAS_RICH:
557
+ console.print(Panel(help_msg, border_style="dim", title="Help"))
558
+ else:
559
+ print(help_msg)
560
+ continue
561
+
516
562
  has_streamed_chat = False
563
+ has_printed_chat = False
517
564
  for event in orchestrator.run_task(session, user_input, cfg):
518
565
  if event.type == "token":
519
566
  delta = event.data.get("delta", "")
@@ -521,6 +568,7 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
521
568
  sys.stdout.write(delta)
522
569
  sys.stdout.flush()
523
570
  has_streamed_chat = True
571
+ has_printed_chat = True
524
572
 
525
573
  elif event.type == "turn_start" and debug:
526
574
  if HAS_RICH:
@@ -532,7 +580,12 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
532
580
  _print_reasoning(event.data["content"])
533
581
 
534
582
  elif event.type == "tool_call" and verbose:
583
+ if has_streamed_chat:
584
+ sys.stdout.write("\n")
585
+ sys.stdout.flush()
586
+ has_streamed_chat = False
535
587
  _print_tool_call(event.data["turn"], event.data["name"], event.data["arguments"])
588
+ has_printed_chat = True
536
589
 
537
590
  elif event.type == "tool_result":
538
591
  if verbose:
@@ -543,8 +596,9 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
543
596
  if has_streamed_chat:
544
597
  sys.stdout.write("\n")
545
598
  has_streamed_chat = False
546
- else:
599
+ elif not has_printed_chat:
547
600
  _print_assistant_message(event.data["content"])
601
+ has_printed_chat = True
548
602
  _checkpoint(workdir, "checkpoint: done")
549
603
 
550
604
  elif event.type == "error":
@@ -557,6 +611,7 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
557
611
  if has_streamed_chat:
558
612
  sys.stdout.write("\n")
559
613
  has_streamed_chat = False
560
- elif event.data.get("summary"):
614
+ elif not has_printed_chat and event.data.get("summary"):
561
615
  _print_assistant_message(event.data["summary"])
616
+ has_printed_chat = True
562
617
  break
@@ -55,8 +55,25 @@ class LocalBackend:
55
55
  sanitized = []
56
56
  for msg in messages:
57
57
  role = msg.get("role", "")
58
- # Skip tool result messages entirely — GGUF models don't understand them
59
58
  if role == "tool":
59
+ # Bridge tool execution results into ChatML user turns so local model sees outputs
60
+ name = msg.get("name") or "tool"
61
+ raw_content = msg.get("content") or ""
62
+ if not isinstance(raw_content, str):
63
+ raw_content = str(raw_content)
64
+ # Smart truncate oversized terminal logs to conserve context
65
+ if len(raw_content) > 1500:
66
+ lines = raw_content.splitlines()
67
+ if len(lines) > 12:
68
+ raw_content = f"[Output: {len(lines)} lines]\n" + "\n".join(lines[:6]) + "\n...\n" + "\n".join(lines[-4:])
69
+ else:
70
+ raw_content = raw_content[:1500] + "\n... [truncated]"
71
+
72
+ tool_text = f"[Tool Result for {name}]:\n{raw_content}"
73
+ if sanitized and sanitized[-1].get("role") == "user":
74
+ sanitized[-1]["content"] += "\n\n" + tool_text
75
+ else:
76
+ sanitized.append({"role": "user", "content": tool_text})
60
77
  continue
61
78
 
62
79
  clean = {"role": role}
@@ -41,6 +41,10 @@ def _normalize_args():
41
41
  if not subcommand and len(sys.argv) > 1:
42
42
  subcommand = "run"
43
43
  sys.argv.insert(1, "run")
44
+ elif subcommand and subcommand not in ("-h", "--help", "-v", "--version") and sys.argv[1] != subcommand:
45
+ # Move subcommand to index 1 so subparsers parse arguments correctly without default override
46
+ sys.argv.remove(subcommand)
47
+ sys.argv.insert(1, subcommand)
44
48
 
45
49
  if subcommand in ("run", "plan", "explain") and len(sys.argv) > 2:
46
50
  cmd_idx = sys.argv.index(subcommand)
@@ -82,6 +86,7 @@ def main():
82
86
  common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
83
87
  common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
84
88
  common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
89
+ common.add_argument("--offline", "--local", dest="offline", action="store_true", help="Run using local offline GGUF model on GPU without internet")
85
90
 
86
91
  from . import __version__
87
92
  parser = argparse.ArgumentParser(
@@ -145,13 +150,16 @@ def main():
145
150
 
146
151
  if args.command == "serve":
147
152
  _show_banner()
153
+ if getattr(args, "offline", False):
154
+ import os
155
+ os.environ["ALPIECODE_BASE_URL"] = "offline"
148
156
  try:
149
157
  from .vscode_installer import ensure_vscode_extension
150
158
  ensure_vscode_extension()
151
159
  except Exception as e:
152
160
  pass
153
161
  from .server import run_server
154
- run_server(host=args.host, port=args.port)
162
+ run_server(host=args.host, port=args.port, offline=getattr(args, "offline", False))
155
163
  return
156
164
 
157
165
  # Check for auto-updates from GitHub in background
@@ -164,6 +172,12 @@ def main():
164
172
 
165
173
  cfg = load_config()
166
174
 
175
+ if getattr(args, "offline", False):
176
+ cfg.base_url = "offline"
177
+ cfg.n_ctx = 16384
178
+ import os
179
+ os.environ["ALPIECODE_BASE_URL"] = "offline"
180
+
167
181
  if getattr(args, "thinking", False):
168
182
  cfg.enable_thinking = True
169
183
  elif getattr(args, "no_thinking", False):
@@ -188,7 +202,7 @@ def main():
188
202
  cfg.max_turns = args.max_turns
189
203
  _show_banner()
190
204
  from .agent import run_chat
191
- run_chat(Path(args.workdir), cfg, verbose=not args.quiet)
205
+ run_chat(Path(args.workdir), cfg, verbose=not args.quiet, debug=getattr(args, 'debug', False))
192
206
 
193
207
  elif args.command == "plan":
194
208
  plan_task = (
@@ -11,7 +11,7 @@ import urllib.parse
11
11
  import urllib.request
12
12
  from dataclasses import dataclass
13
13
  from pathlib import Path
14
- from typing import Any, Dict, Iterator, Optional
14
+ from typing import Any, Dict, Iterator, List, Optional
15
15
 
16
16
  from .orchestrator import AgentEvent
17
17
 
@@ -151,11 +151,13 @@ def load_config() -> Config:
151
151
  except ValueError:
152
152
  pass
153
153
 
154
- # For offline mode, optimize default context size to 4096 to prevent VRAM/RAM bottleneck
155
- # unless explicitly configured by the user
154
+ # For offline mode, set high-performance 16k context (16384 tokens) for big multi-file projects
155
+ # Fits completely in 6GB VRAM on RTX 3050 with zero paging
156
156
  if data.get("base_url") == "offline" and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
157
- if data.get("n_ctx") == 32768:
158
- data["n_ctx"] = 4096
157
+ if data.get("n_ctx") in (32768, 4096):
158
+ data["n_ctx"] = 16384
159
+ if data.get("max_tokens", 8192) > 4096:
160
+ data["max_tokens"] = 4096
159
161
 
160
162
  # Filter data to only keys present in Config fields
161
163
  valid_keys = {f.name for f in fields(Config)}
@@ -559,6 +559,7 @@ def gather_relevant_context(
559
559
  if getattr(task_context, "intent", "") in ("qa",):
560
560
  return None
561
561
 
562
+ task_lower = task.lower().strip()
562
563
  sections: List[str] = []
563
564
  chars_used = 0
564
565
 
@@ -194,6 +194,7 @@ class ToolExecutor:
194
194
  self.dispatch = make_dispatch(workdir)
195
195
  self.tool_call_history: List[Tuple[str, str]] = []
196
196
  self.compile_fail_counts: Dict[str, int] = {}
197
+ self.successful_bash_commands: set = set()
197
198
 
198
199
  def extract_tool_calls(self, response: ChatResponse) -> List[ToolCall]:
199
200
  """Extract tool calls from response (structured or text format)."""
@@ -342,6 +343,10 @@ class ToolExecutor:
342
343
  res_str = f"error: Unknown tool '{tc.name}'"
343
344
  elapsed = (time.monotonic() - t0) * 1000
344
345
 
346
+ # Clear passed verification set whenever code files are written or edited
347
+ if tc.name in ("write_file", "edit_file", "apply_patch"):
348
+ self.successful_bash_commands.clear()
349
+
345
350
  # Tool loop detection guard
346
351
  call_sig = (tc.name, json.dumps(tc.arguments, sort_keys=True))
347
352
  self.tool_call_history.append(call_sig)
@@ -354,6 +359,19 @@ class ToolExecutor:
354
359
  "All checks have passed. Do NOT run this tool again. Output your final summary starting with: DONE: <summary>."
355
360
  )
356
361
 
362
+ # Repetitive verification sequence guard
363
+ if tc.name == "bash":
364
+ cmd_str = safe_args.get("command", "").strip()
365
+ if cmd_str and cmd_str in self.successful_bash_commands:
366
+ res_str += (
367
+ "\n\n⚠️ REPETITIVE VERIFICATION NOTICE: This exact command has already run and passed with exit code 0, "
368
+ "and no files have been modified since. Re-running identical tests is unnecessary. "
369
+ "All checks have already passed! Finalize the task now with: DONE: <summary>."
370
+ )
371
+ elif '"exit_code": 0' in res_str:
372
+ if cmd_str:
373
+ self.successful_bash_commands.add(cmd_str)
374
+
357
375
  # Smart error hint injection for failed bash commands
358
376
  if tc.name == "bash" and '"exit_code"' in res_str:
359
377
  try:
@@ -414,6 +414,17 @@ class LocalModel:
414
414
  else:
415
415
  msgs = messages
416
416
 
417
+ # Context headroom safety guard: dynamically clamp max_tokens if prompt is large
418
+ n_ctx_limit = getattr(self, "n_ctx", 16384)
419
+ try:
420
+ total_chars = sum(len(str(m.get("content", ""))) for m in msgs)
421
+ est_prompt_tokens = int(total_chars / 3.0) + (len(tools or []) * 80) + 100
422
+ if est_prompt_tokens + max_tokens > n_ctx_limit:
423
+ available_tokens = max(128, n_ctx_limit - est_prompt_tokens - 32)
424
+ max_tokens = min(max_tokens, available_tokens)
425
+ except Exception:
426
+ pass
427
+
417
428
  params = {
418
429
  "messages": msgs,
419
430
  "temperature": 0.1 if not enable_thinking else temperature,
@@ -423,7 +434,24 @@ class LocalModel:
423
434
  params["tools"] = tools
424
435
  params["tool_choice"] = kwargs.get("tool_choice", "auto")
425
436
 
426
- result_dict = llm.create_chat_completion(**params)
437
+ try:
438
+ result_dict = llm.create_chat_completion(**params)
439
+ except ValueError as ve:
440
+ err_msg = str(ve)
441
+ if "exceed context window" in err_msg.lower():
442
+ # Parse: "Requested tokens (X) exceed context window of Y"
443
+ m = re.search(r"Requested tokens \((\d+)\) exceed context window of (\d+)", err_msg)
444
+ if m:
445
+ requested = int(m.group(1))
446
+ win = int(m.group(2))
447
+ excess = requested - win
448
+ new_max = max(128, params["max_tokens"] - excess - 32)
449
+ params["max_tokens"] = new_max
450
+ result_dict = llm.create_chat_completion(**params)
451
+ else:
452
+ raise
453
+ else:
454
+ raise
427
455
 
428
456
  # Fix prefill response content so chat history remains valid across multi-turn sessions
429
457
  if not enable_thinking and result_dict.get("choices"):
@@ -39,6 +39,7 @@ Owns the turn loop, backend resolution, caching, and event stream.
39
39
  """
40
40
 
41
41
  import copy
42
+ import re
42
43
  from dataclasses import dataclass, field
43
44
  from typing import Any, Dict, Iterator, List, Optional
44
45
 
@@ -77,9 +77,12 @@ if HAS_FASTAPI:
77
77
  except Exception:
78
78
  cache_stats = {}
79
79
 
80
+ is_offline = "LocalBackend" in backend_name or getattr(getattr(app.state, "cfg", None), "base_url", "") == "offline"
80
81
  return {
81
82
  "status": "online",
83
+ "mode": "offline" if is_offline else "online",
82
84
  "backend": backend_name,
85
+ "model": getattr(getattr(app.state, "cfg", None), "model", "alpie_9b"),
83
86
  "available": is_avail,
84
87
  "uptime_seconds": round(uptime, 2),
85
88
  "version": "0.9.2",
@@ -283,7 +286,10 @@ else:
283
286
  app = None
284
287
 
285
288
 
286
- def run_server(host: str = "127.0.0.1", port: int = 7169) -> None:
289
+ def run_server(host: str = "127.0.0.1", port: int = 7169, offline: bool = False) -> None:
290
+ if offline:
291
+ import os
292
+ os.environ["ALPIECODE_BASE_URL"] = "offline"
287
293
  """Start the uvicorn ASGI server."""
288
294
  if not HAS_FASTAPI:
289
295
  print("❌ FastAPI/Uvicorn is not installed. Install with: pip install 'alpiecode[server]' or pip install fastapi uvicorn")
@@ -110,6 +110,14 @@ def auto_update(quiet: bool = False) -> bool:
110
110
  if os.environ.get("ALPIECODE_NO_UPDATE") == "1":
111
111
  return False
112
112
 
113
+ # Do not auto-update if running from a local development checkout
114
+ try:
115
+ dev_root = Path(__file__).resolve().parents[2]
116
+ if (dev_root / "pyproject.toml").exists() and (dev_root / ".git").exists():
117
+ return False
118
+ except Exception:
119
+ pass
120
+
113
121
  # Show notice if last run installed an update
114
122
  if not quiet:
115
123
  _check_pending_update_notice()
@@ -0,0 +1,71 @@
1
+ """
2
+ Tests for audit fixes:
3
+ 1. orchestrator regex splitting on DONE:
4
+ 2. discovery task_lower scoping
5
+ 3. client.py List import
6
+ 4. CLI --offline flag argument parsing
7
+ """
8
+
9
+ import argparse
10
+ from pathlib import Path
11
+
12
+ from codeagent.client import AlpieCodeClient
13
+ from codeagent.discovery import TaskContext, gather_relevant_context
14
+ from codeagent import orchestrator
15
+
16
+
17
+ def test_orchestrator_re_import():
18
+ """Verify that re is imported in orchestrator module and DONE: regex splitting works."""
19
+ assert hasattr(orchestrator, "re"), "orchestrator module must have 're' imported"
20
+ content = "The work is completed.\n\nDONE: All tests passed successfully."
21
+ parts = orchestrator.re.split(r"(?i)\bDONE:\s*", content)
22
+ assert len(parts) == 2
23
+ assert "All tests passed" in parts[1]
24
+
25
+
26
+ def test_discovery_task_lower_scoping(tmp_path):
27
+ """Verify gather_relevant_context works with symbol_index without UnboundLocalError."""
28
+ # Create sample file
29
+ code_file = tmp_path / "math_utils.py"
30
+ code_file.write_text("def is_palindrome(s): return s == s[::-1]\n", encoding="utf-8")
31
+
32
+ task_context = TaskContext(
33
+ intent="modify",
34
+ project_type="python",
35
+ file_count=1,
36
+ entry_points=["math_utils.py"],
37
+ symbol_index={"math_utils.py": ["is_palindrome"]},
38
+ )
39
+
40
+ # Calling gather_relevant_context with a task matching symbols
41
+ res = gather_relevant_context(
42
+ task="check the palindrome logic in math_utils.py",
43
+ workdir=tmp_path,
44
+ task_context=task_context,
45
+ max_chars=4000,
46
+ )
47
+ assert res is not None
48
+ assert "math_utils.py" in res
49
+
50
+
51
+ def test_client_typing_imports():
52
+ """Verify client module imports List and AlpieCodeClient instantiates."""
53
+ from codeagent import client
54
+ assert hasattr(client, "List"), "client module should import List from typing"
55
+ c = AlpieCodeClient("http://127.0.0.1:7169")
56
+ assert c.base_url == "http://127.0.0.1:7169"
57
+
58
+
59
+ def test_cli_offline_flag_parsing():
60
+ """Verify --offline flag is recognized in CLI parser."""
61
+ common = argparse.ArgumentParser(add_help=False)
62
+ common.add_argument("--offline", "--local", dest="offline", action="store_true")
63
+
64
+ args = common.parse_args(["--offline"])
65
+ assert args.offline is True
66
+
67
+ args_local = common.parse_args(["--local"])
68
+ assert args_local.offline is True
69
+
70
+ args_default = common.parse_args([])
71
+ assert args_default.offline is False
@@ -0,0 +1,85 @@
1
+ """
2
+ Tests for interactive chat mode:
3
+ 1. run_chat signature accepts debug flag
4
+ 2. event loop handles turn_start when debug is True or False
5
+ 3. in-chat slash commands (/info, /clear, /help) execute cleanly
6
+ """
7
+
8
+ from unittest.mock import MagicMock, patch
9
+ from pathlib import Path
10
+
11
+ from codeagent.agent import run_chat
12
+ from codeagent.config import Config
13
+ from codeagent.orchestrator import AgentEvent
14
+
15
+
16
+ def test_run_chat_debug_true(tmp_path):
17
+ """Verify run_chat signature accepts debug=True and exits cleanly."""
18
+ cfg = Config(base_url="offline")
19
+ with patch("builtins.input", side_effect=["exit"]):
20
+ run_chat(tmp_path, cfg, verbose=False, debug=True)
21
+
22
+
23
+ def test_run_chat_debug_false(tmp_path):
24
+ """Verify run_chat signature accepts debug=False and exits cleanly."""
25
+ cfg = Config(base_url="offline")
26
+ with patch("builtins.input", side_effect=["exit"]):
27
+ run_chat(tmp_path, cfg, verbose=False, debug=False)
28
+
29
+
30
+ def test_run_chat_in_chat_commands(tmp_path):
31
+ """Verify run_chat handles /info, /help, /clear commands before exiting."""
32
+ cfg = Config(base_url="offline")
33
+ commands = ["/info", "/help", "/clear", "exit"]
34
+ with patch("builtins.input", side_effect=commands):
35
+ run_chat(tmp_path, cfg, verbose=False, debug=False)
36
+
37
+
38
+ def test_chat_event_loop_turn_start_with_debug(tmp_path):
39
+ """Verify that turn_start event does not raise NameError when debug is enabled."""
40
+ cfg = Config(base_url="offline")
41
+
42
+ mock_events = [
43
+ AgentEvent("turn_start", {"turn": 1}),
44
+ AgentEvent("token", {"delta": "Mock output"}),
45
+ AgentEvent("done", {"summary": "Done"}),
46
+ ]
47
+
48
+ with patch("builtins.input", side_effect=["what is gravity?", "exit"]):
49
+ with patch("codeagent.agent.AgentOrchestrator.run_task", return_value=iter(mock_events)):
50
+ run_chat(tmp_path, cfg, verbose=False, debug=True)
51
+
52
+
53
+ def test_chat_event_loop_turn_start_without_debug(tmp_path):
54
+ """Verify that turn_start event does not raise NameError when debug is disabled."""
55
+ cfg = Config(base_url="offline")
56
+
57
+ mock_events = [
58
+ AgentEvent("turn_start", {"turn": 1}),
59
+ AgentEvent("token", {"delta": "Mock output"}),
60
+ AgentEvent("done", {"summary": "Done"}),
61
+ ]
62
+
63
+ with patch("builtins.input", side_effect=["what is gravity?", "exit"]):
64
+ with patch("codeagent.agent.AgentOrchestrator.run_task", return_value=iter(mock_events)):
65
+ run_chat(tmp_path, cfg, verbose=False, debug=False)
66
+
67
+
68
+ def test_chat_no_duplicate_response(tmp_path, capsys):
69
+ """Verify that streamed tokens + done summary are not printed twice."""
70
+ cfg = Config(base_url="offline")
71
+
72
+ unique_phrase = "Einstein spacetime curvature 1915"
73
+ mock_events = [
74
+ AgentEvent("turn_start", {"turn": 1}),
75
+ AgentEvent("token", {"delta": unique_phrase}),
76
+ AgentEvent("message", {"content": unique_phrase}),
77
+ AgentEvent("done", {"summary": unique_phrase}),
78
+ ]
79
+
80
+ with patch("builtins.input", side_effect=["what is GR?", "exit"]):
81
+ with patch("codeagent.agent.AgentOrchestrator.run_task", return_value=iter(mock_events)):
82
+ run_chat(tmp_path, cfg, verbose=True, debug=False)
83
+
84
+ captured = capsys.readouterr().out
85
+ assert captured.count(unique_phrase) == 1, f"Expected 1 occurrence of response, got {captured.count(unique_phrase)}"
@@ -0,0 +1,78 @@
1
+ import os
2
+ from pathlib import Path
3
+ from unittest.mock import MagicMock
4
+
5
+ from codeagent.config import load_config
6
+ from codeagent.backends.local_backend import LocalBackend
7
+ from codeagent.session import SessionManager
8
+ from codeagent.context import ContextManager
9
+
10
+
11
+ def test_offline_config_defaults():
12
+ os.environ["ALPIECODE_BASE_URL"] = "offline"
13
+ try:
14
+ cfg = load_config()
15
+ assert cfg.base_url == "offline"
16
+ assert cfg.n_ctx == 16384
17
+ assert cfg.max_tokens <= 4096
18
+ finally:
19
+ os.environ.pop("ALPIECODE_BASE_URL", None)
20
+
21
+
22
+ def test_local_backend_tool_result_sanitization():
23
+ messages = [
24
+ {"role": "system", "content": "You are a helpful coding assistant."},
25
+ {"role": "user", "content": "Write a test file."},
26
+ {
27
+ "role": "assistant",
28
+ "content": "Writing file now.",
29
+ "tool_calls": [
30
+ {
31
+ "id": "call_1",
32
+ "function": {"name": "write_file", "arguments": '{"path": "test.py"}'},
33
+ }
34
+ ],
35
+ },
36
+ {
37
+ "role": "tool",
38
+ "name": "write_file",
39
+ "tool_call_id": "call_1",
40
+ "content": "wrote 150 bytes to test.py",
41
+ },
42
+ {
43
+ "role": "tool",
44
+ "name": "bash",
45
+ "tool_call_id": "call_2",
46
+ "content": "pytest passed (1 passed in 0.05s)",
47
+ },
48
+ ]
49
+
50
+ sanitized = LocalBackend._sanitize_messages_for_gguf(messages)
51
+ # Ensure role 'tool' was NOT dropped
52
+ assert not any(m.get("role") == "tool" for m in sanitized)
53
+
54
+ # Tool results should be converted into user turns
55
+ user_turns = [m for m in sanitized if m.get("role") == "user"]
56
+ # Consecutive tool results are coalesced into a single user turn
57
+ assert any("[Tool Result for write_file]" in m["content"] for m in user_turns)
58
+ assert any("[Tool Result for bash]" in m["content"] for m in user_turns)
59
+
60
+
61
+ def test_agent_session_max_tokens_uses_backend_context_window():
62
+ cfg_mock = MagicMock()
63
+ cfg_mock.n_ctx = 16384
64
+ cfg_mock.base_url = "offline"
65
+
66
+ backend = LocalBackend(cfg_mock)
67
+ assert backend.context_window == 16384
68
+
69
+ effective_ctx = (
70
+ backend.context_window
71
+ if hasattr(backend, "context_window") and backend.context_window
72
+ else (cfg_mock.n_ctx if cfg_mock.base_url == "offline" else 262_144)
73
+ )
74
+ assert effective_ctx == 16384
75
+
76
+ session_mgr = SessionManager()
77
+ session = session_mgr.create_session(Path.cwd(), max_tokens=effective_ctx)
78
+ assert session.context.max_tokens == 16384
File without changes
File without changes
File without changes
File without changes
File without changes