alpiecode 9.0.4__tar.gz → 10.0.0__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 (52) hide show
  1. {alpiecode-9.0.4 → alpiecode-10.0.0}/PKG-INFO +1 -1
  2. {alpiecode-9.0.4 → alpiecode-10.0.0}/pyproject.toml +1 -1
  3. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/SOURCES.txt +3 -0
  5. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/__init__.py +2 -1
  6. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/agent.py +65 -10
  7. alpiecode-10.0.0/src/codeagent/backends/local_backend.py +314 -0
  8. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/cli.py +16 -2
  9. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/client.py +1 -1
  10. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/config.py +10 -4
  11. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/discovery.py +7 -1
  12. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/doctor.py +72 -32
  13. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/executor.py +18 -0
  14. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/extension/alpiecode.vsix +0 -0
  15. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/local_model.py +45 -1
  16. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/orchestrator.py +91 -13
  17. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/server.py +11 -6
  18. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/tools.py +6 -2
  19. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/updater.py +8 -0
  20. alpiecode-10.0.0/test/test_audit_fixes.py +71 -0
  21. alpiecode-10.0.0/test/test_chat_mode.py +85 -0
  22. alpiecode-10.0.0/test/test_offline_mode.py +314 -0
  23. {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_phase1.py +42 -0
  24. alpiecode-9.0.4/src/codeagent/backends/local_backend.py +0 -150
  25. {alpiecode-9.0.4 → alpiecode-10.0.0}/README.md +0 -0
  26. {alpiecode-9.0.4 → alpiecode-10.0.0}/setup.cfg +0 -0
  27. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode/__init__.py +0 -0
  28. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  29. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
  30. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/requires.txt +0 -0
  31. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/top_level.txt +0 -0
  32. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/backends/__init__.py +0 -0
  33. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/backends/base.py +0 -0
  34. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/backends/openai_backend.py +0 -0
  35. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/cache.py +0 -0
  36. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/compaction.py +0 -0
  37. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/context.py +0 -0
  38. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/git_ops.py +0 -0
  39. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/github.py +0 -0
  40. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/guardian.py +0 -0
  41. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/guardrails.py +0 -0
  42. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/ipython_ext.py +0 -0
  43. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/media.py +0 -0
  44. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/memory.py +0 -0
  45. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/progress.py +0 -0
  46. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/prompt.py +0 -0
  47. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/rephraser.py +0 -0
  48. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/session.py +0 -0
  49. {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/vscode_installer.py +0 -0
  50. {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_claude_codex_capabilities.py +0 -0
  51. {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_phase2.py +0 -0
  52. {alpiecode-9.0.4 → alpiecode-10.0.0}/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: 10.0.0
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 = "10.0.0"
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: 10.0.0
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
@@ -1,6 +1,7 @@
1
1
  import warnings
2
2
  warnings.filterwarnings("ignore", category=DeprecationWarning)
3
3
  warnings.filterwarnings("ignore", category=FutureWarning)
4
+ warnings.filterwarnings("ignore", category=SyntaxWarning, message=r".*invalid escape sequence.*")
4
5
  warnings.filterwarnings("ignore", module="jupyter_client.*")
5
6
 
6
7
  """
@@ -16,7 +17,7 @@ Supports:
16
17
  from pathlib import Path
17
18
  from typing import Optional, Any, Dict
18
19
 
19
- __version__ = "9.0.4"
20
+ __version__ = "10.0.0"
20
21
 
21
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
22
23
  """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
@@ -0,0 +1,314 @@
1
+ """
2
+ Local GGUF model inference backend.
3
+ """
4
+
5
+ from typing import Any, List, Optional
6
+
7
+ from ..config import Config
8
+ from ..local_model import LocalModel
9
+ from .base import ChatResponse, ToolCall
10
+
11
+
12
+ class LocalBackend:
13
+ """Backend for local GGUF model inference via llama-cpp-python."""
14
+
15
+ def __init__(self, cfg: Config):
16
+ self._cfg = cfg
17
+ self._model: Optional[LocalModel] = None
18
+
19
+ @property
20
+ def name(self) -> str:
21
+ return f"Local GGUF ({self._cfg.model_repo})"
22
+
23
+ @property
24
+ def is_available(self) -> bool:
25
+ return True # Local model is always available (downloads if missing)
26
+
27
+ @property
28
+ def context_window(self) -> int:
29
+ return self._cfg.n_ctx
30
+
31
+ def _ensure_model(self) -> LocalModel:
32
+ if self._model is None:
33
+ self._model = LocalModel(
34
+ repo_id=self._cfg.model_repo,
35
+ n_ctx=self._cfg.n_ctx,
36
+ n_gpu_layers=self._cfg.n_gpu_layers,
37
+ token=self._cfg.hf_token,
38
+ )
39
+ return self._model
40
+
41
+ def load_model(self) -> None:
42
+ """Explicitly load model into VRAM/RAM (warmup)."""
43
+ model = self._ensure_model()
44
+ model.load()
45
+
46
+ @staticmethod
47
+ def _sanitize_messages_for_gguf(messages: List[dict]) -> List[dict]:
48
+ """
49
+ Sanitize OpenAI-format messages for GGUF Jinja2 chat templates.
50
+ Fixes 'Can only get item pairs from a mapping' errors by:
51
+ - Ensuring content is always a string (never None or list)
52
+ - Stripping tool_calls and tool messages (GGUF templates don't support them)
53
+ - Removing tool-related role messages
54
+ """
55
+ sanitized = []
56
+ for msg in messages:
57
+ role = msg.get("role", "")
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 failure-preserving truncation for local offline context
65
+ from ..tools import _smart_truncate
66
+ raw_content = _smart_truncate(raw_content, max_chars=1800)
67
+
68
+ tool_text = f"[Tool Result for {name}]:\n{raw_content}"
69
+ if sanitized and sanitized[-1].get("role") == "user":
70
+ sanitized[-1]["content"] += "\n\n" + tool_text
71
+ else:
72
+ sanitized.append({"role": "user", "content": tool_text})
73
+ continue
74
+
75
+ clean = {"role": role}
76
+
77
+ # Ensure content is always a string
78
+ content = msg.get("content")
79
+ if content is None:
80
+ content = ""
81
+ elif isinstance(content, list):
82
+ # Multimodal content (image_url + text blocks) — extract text parts
83
+ text_parts = []
84
+ for part in content:
85
+ if isinstance(part, dict):
86
+ if part.get("type") == "text":
87
+ text_parts.append(part.get("text", ""))
88
+ elif isinstance(part, str):
89
+ text_parts.append(part)
90
+ content = "\n".join(text_parts) if text_parts else ""
91
+ elif not isinstance(content, str):
92
+ content = str(content)
93
+
94
+ # For assistant messages with tool_calls, embed tool call info in content text
95
+ if role == "assistant" and msg.get("tool_calls"):
96
+ tc_descriptions = []
97
+ for tc in msg["tool_calls"]:
98
+ if isinstance(tc, dict):
99
+ fn = tc.get("function", {})
100
+ name = fn.get("name", "unknown")
101
+ args = fn.get("arguments", "{}")
102
+ tc_descriptions.append(f"[Called tool: {name}({args})]")
103
+ if tc_descriptions:
104
+ content = (content + "\n" + "\n".join(tc_descriptions)).strip()
105
+ # Do NOT include tool_calls key — GGUF template will crash on it
106
+
107
+ clean["content"] = content
108
+ sanitized.append(clean)
109
+
110
+ return sanitized
111
+
112
+ def chat_completion(
113
+ self,
114
+ messages: List[dict],
115
+ tools: Optional[List[dict]] = None,
116
+ temperature: float = 0.2,
117
+ max_tokens: int = 2048,
118
+ enable_thinking: bool = True,
119
+ ) -> ChatResponse:
120
+ model = self._ensure_model()
121
+ sanitized_msgs = self._sanitize_messages_for_gguf(messages)
122
+ resp = model.create_chat_completion(
123
+ messages=sanitized_msgs,
124
+ tools=tools,
125
+ tool_choice="auto",
126
+ temperature=temperature,
127
+ max_tokens=max_tokens,
128
+ enable_thinking=enable_thinking,
129
+ )
130
+ msg = resp.choices[0].message
131
+
132
+ tool_calls = None
133
+ if msg.tool_calls:
134
+ import json
135
+ tool_calls = []
136
+ for tc in msg.tool_calls:
137
+ args = tc.function.arguments
138
+ if isinstance(args, str):
139
+ try:
140
+ args = json.loads(args or "{}")
141
+ except Exception:
142
+ args = {}
143
+ tool_calls.append(
144
+ ToolCall(
145
+ id=tc.id,
146
+ name=tc.function.name,
147
+ arguments=args if isinstance(args, dict) else {},
148
+ )
149
+ )
150
+
151
+ reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
152
+
153
+ return ChatResponse(
154
+ content=msg.content,
155
+ reasoning=reasoning,
156
+ tool_calls=tool_calls,
157
+ raw=resp,
158
+ )
159
+
160
+ def chat_completion_stream(
161
+ self,
162
+ messages: List[dict],
163
+ tools: Optional[List[dict]] = None,
164
+ temperature: Optional[float] = None,
165
+ max_tokens: Optional[int] = None,
166
+ enable_thinking: bool = True,
167
+ ):
168
+ """
169
+ Stream local GGUF completion yielding progressive thinking deltas,
170
+ content tokens, and the final ChatResponse.
171
+ """
172
+ import time
173
+ start_time = time.time()
174
+ temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.2)
175
+ m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 2048)
176
+
177
+ model = self._ensure_model()
178
+ sanitized_msgs = self._sanitize_messages_for_gguf(messages)
179
+
180
+ chunks = model.create_chat_completion(
181
+ messages=sanitized_msgs,
182
+ tools=tools,
183
+ tool_choice="auto" if tools else None,
184
+ temperature=temp,
185
+ max_tokens=m_tokens,
186
+ enable_thinking=enable_thinking,
187
+ stream=True,
188
+ )
189
+
190
+ try:
191
+ from .openai_backend import _repair_and_parse_json_arguments
192
+ except Exception:
193
+ import json
194
+ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
195
+ try:
196
+ return json.loads(raw_args or "{}")
197
+ except Exception:
198
+ return {}
199
+
200
+ in_think = enable_thinking
201
+ think_started = False
202
+ full_content = []
203
+ full_reasoning = []
204
+ tool_calls_dict = {}
205
+
206
+ for chunk in chunks:
207
+ if not chunk or not chunk.get("choices"):
208
+ continue
209
+ choice = chunk["choices"][0]
210
+ delta = choice.get("delta") or {}
211
+
212
+ explicit_reasoning = delta.get("reasoning") or delta.get("reasoning_content")
213
+ if explicit_reasoning:
214
+ if not think_started:
215
+ think_started = True
216
+ yield ("thinking_start", {"time": start_time})
217
+ full_reasoning.append(explicit_reasoning)
218
+ yield ("thinking_delta", {"delta": explicit_reasoning})
219
+
220
+ if delta.get("tool_calls"):
221
+ for tc in delta["tool_calls"]:
222
+ idx = tc.get("index", 0)
223
+ if idx not in tool_calls_dict:
224
+ fn = tc.get("function") or {}
225
+ tool_calls_dict[idx] = {
226
+ "id": tc.get("id") or f"call_{idx}",
227
+ "name": fn.get("name", ""),
228
+ "arguments": fn.get("arguments", ""),
229
+ }
230
+ else:
231
+ if tc.get("id"):
232
+ tool_calls_dict[idx]["id"] = tc["id"]
233
+ fn = tc.get("function") or {}
234
+ if fn.get("name"):
235
+ tool_calls_dict[idx]["name"] += fn["name"]
236
+ if fn.get("arguments"):
237
+ tool_calls_dict[idx]["arguments"] += fn["arguments"]
238
+
239
+ text = delta.get("content")
240
+ if text:
241
+ if in_think:
242
+ if not think_started:
243
+ think_started = True
244
+ yield ("thinking_start", {"time": start_time})
245
+
246
+ if "</think>" in text:
247
+ before, after = text.split("</think>", 1)
248
+ if before:
249
+ full_reasoning.append(before)
250
+ yield ("thinking_delta", {"delta": before})
251
+ in_think = False
252
+ elapsed = time.time() - start_time
253
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
254
+ if after.strip():
255
+ full_content.append(after)
256
+ yield ("token", {"delta": after})
257
+ elif "DONE:" in text:
258
+ before, after = text.split("DONE:", 1)
259
+ if before:
260
+ full_reasoning.append(before)
261
+ yield ("thinking_delta", {"delta": before})
262
+ in_think = False
263
+ elapsed = time.time() - start_time
264
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
265
+ done_chunk = "DONE:" + after
266
+ full_content.append(done_chunk)
267
+ yield ("token", {"delta": done_chunk})
268
+ else:
269
+ clean_text = text.replace("<think>", "")
270
+ full_reasoning.append(clean_text)
271
+ yield ("thinking_delta", {"delta": clean_text})
272
+ else:
273
+ full_content.append(text)
274
+ yield ("token", {"delta": text})
275
+
276
+ if in_think and think_started:
277
+ elapsed = time.time() - start_time
278
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
279
+
280
+ tool_calls = None
281
+ if tool_calls_dict:
282
+ tool_calls = []
283
+ for idx in sorted(tool_calls_dict.keys()):
284
+ item = tool_calls_dict[idx]
285
+ args = _repair_and_parse_json_arguments(item.get("arguments", ""))
286
+ tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
287
+
288
+ final_content_str = "".join(full_content).strip()
289
+ final_reasoning_str = "".join(full_reasoning).strip()
290
+
291
+ if not final_content_str and final_reasoning_str:
292
+ if "DONE:" in final_reasoning_str:
293
+ idx = final_reasoning_str.find("DONE:")
294
+ final_content_str = final_reasoning_str[idx:].strip()
295
+ final_reasoning_str = final_reasoning_str[:idx].strip()
296
+ yield ("token", {"delta": final_content_str})
297
+ elif "</think>" in final_reasoning_str:
298
+ parts = final_reasoning_str.split("</think>", 1)
299
+ final_reasoning_str = parts[0].strip()
300
+ final_content_str = parts[1].strip()
301
+ yield ("token", {"delta": final_content_str})
302
+
303
+ chat_resp = ChatResponse(
304
+ content=final_content_str,
305
+ reasoning=final_reasoning_str or None,
306
+ tool_calls=tool_calls,
307
+ raw=None,
308
+ )
309
+ yield ("done", chat_resp)
310
+
311
+ def shutdown(self) -> None:
312
+ if self._model:
313
+ self._model._llm = None
314
+ self._model = None
@@ -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
 
@@ -30,6 +30,8 @@ DEFAULTS = {
30
30
  "temperature": 0.1,
31
31
  "max_tokens": 8192,
32
32
  "enable_thinking": False, # Reasoning OFF by default
33
+ "adaptive_thinking": True, # Adaptive turn-level reasoning dynamics
34
+ "force_thinking": False, # Force reasoning on every single turn
33
35
  "n_ctx": 32768, # 32k context window
34
36
  "n_gpu_layers": None, # None = auto-detect GPU
35
37
  "config_version": CONFIG_VERSION,
@@ -92,6 +94,8 @@ class Config:
92
94
  temperature: float = 0.1
93
95
  max_tokens: int = 8192
94
96
  enable_thinking: bool = False
97
+ adaptive_thinking: bool = True
98
+ force_thinking: bool = False
95
99
  n_ctx: int = 32768
96
100
  n_gpu_layers: Optional[int] = None
97
101
 
@@ -151,11 +155,13 @@ def load_config() -> Config:
151
155
  except ValueError:
152
156
  pass
153
157
 
154
- # For offline mode, optimize default context size to 4096 to prevent VRAM/RAM bottleneck
155
- # unless explicitly configured by the user
158
+ # For offline mode, set high-performance 16k context (16384 tokens) for big multi-file projects
159
+ # Fits completely in 6GB VRAM on RTX 3050 with zero paging
156
160
  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
161
+ if data.get("n_ctx") in (32768, 4096):
162
+ data["n_ctx"] = 16384
163
+ if data.get("max_tokens", 8192) > 4096:
164
+ data["max_tokens"] = 4096
159
165
 
160
166
  # Filter data to only keys present in Config fields
161
167
  valid_keys = {f.name for f in fields(Config)}
@@ -220,6 +220,11 @@ def scan_repository(workdir: Path) -> dict:
220
220
  Designed to run in under 30ms by checking only key indicator files.
221
221
  """
222
222
  workdir = workdir.resolve()
223
+ if not workdir.exists():
224
+ try:
225
+ workdir.mkdir(parents=True, exist_ok=True)
226
+ except Exception:
227
+ pass
223
228
 
224
229
  has_git = (workdir / ".git").is_dir()
225
230
  has_venv = (workdir / ".venv").is_dir() or (workdir / "venv").is_dir()
@@ -407,7 +412,7 @@ def _has_test_files(workdir: Path) -> bool:
407
412
  for f in workdir.iterdir():
408
413
  if f.is_file() and (f.name.startswith("test_") or f.name.endswith("_test.py")):
409
414
  return True
410
- except PermissionError:
415
+ except (PermissionError, FileNotFoundError):
411
416
  pass
412
417
  return False
413
418
 
@@ -559,6 +564,7 @@ def gather_relevant_context(
559
564
  if getattr(task_context, "intent", "") in ("qa",):
560
565
  return None
561
566
 
567
+ task_lower = task.lower().strip()
562
568
  sections: List[str] = []
563
569
  chars_used = 0
564
570