alpiecode 9.0.0__tar.gz → 9.0.1__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 (47) hide show
  1. {alpiecode-9.0.0 → alpiecode-9.0.1}/PKG-INFO +1 -1
  2. {alpiecode-9.0.0 → alpiecode-9.0.1}/pyproject.toml +1 -1
  3. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/__init__.py +1 -1
  5. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/local_backend.py +55 -1
  6. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/orchestrator.py +48 -17
  7. {alpiecode-9.0.0 → alpiecode-9.0.1}/test/test_phase1.py +2 -1
  8. {alpiecode-9.0.0 → alpiecode-9.0.1}/README.md +0 -0
  9. {alpiecode-9.0.0 → alpiecode-9.0.1}/setup.cfg +0 -0
  10. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode/__init__.py +0 -0
  11. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  12. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  13. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  14. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/requires.txt +0 -0
  15. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  16. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/agent.py +0 -0
  17. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/__init__.py +0 -0
  18. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/base.py +0 -0
  19. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/openai_backend.py +0 -0
  20. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/cache.py +0 -0
  21. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/cli.py +0 -0
  22. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/client.py +0 -0
  23. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/compaction.py +0 -0
  24. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/config.py +0 -0
  25. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/context.py +0 -0
  26. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/discovery.py +0 -0
  27. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/doctor.py +0 -0
  28. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/executor.py +0 -0
  29. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/extension/alpiecode.vsix +0 -0
  30. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/git_ops.py +0 -0
  31. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/github.py +0 -0
  32. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/guardian.py +0 -0
  33. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/guardrails.py +0 -0
  34. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/ipython_ext.py +0 -0
  35. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/local_model.py +0 -0
  36. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/media.py +0 -0
  37. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/memory.py +0 -0
  38. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/progress.py +0 -0
  39. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/prompt.py +0 -0
  40. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/rephraser.py +0 -0
  41. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/server.py +0 -0
  42. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/session.py +0 -0
  43. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/tools.py +0 -0
  44. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/updater.py +0 -0
  45. {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/vscode_installer.py +0 -0
  46. {alpiecode-9.0.0 → alpiecode-9.0.1}/test/test_phase2.py +0 -0
  47. {alpiecode-9.0.0 → alpiecode-9.0.1}/test/test_phase3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 9.0.0
3
+ Version: 9.0.1
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.0"
7
+ version = "9.0.1"
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.0
3
+ Version: 9.0.1
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
@@ -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.0"
19
+ __version__ = "9.0.1"
20
20
 
21
21
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
22
22
  """Run an autonomous coding task programmatically."""
@@ -43,6 +43,59 @@ class LocalBackend:
43
43
  model = self._ensure_model()
44
44
  model.load()
45
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
+ # Skip tool result messages entirely — GGUF models don't understand them
59
+ if role == "tool":
60
+ continue
61
+
62
+ clean = {"role": role}
63
+
64
+ # Ensure content is always a string
65
+ content = msg.get("content")
66
+ if content is None:
67
+ content = ""
68
+ elif isinstance(content, list):
69
+ # Multimodal content (image_url + text blocks) — extract text parts
70
+ text_parts = []
71
+ for part in content:
72
+ if isinstance(part, dict):
73
+ if part.get("type") == "text":
74
+ text_parts.append(part.get("text", ""))
75
+ elif isinstance(part, str):
76
+ text_parts.append(part)
77
+ content = "\n".join(text_parts) if text_parts else ""
78
+ elif not isinstance(content, str):
79
+ content = str(content)
80
+
81
+ # For assistant messages with tool_calls, embed tool call info in content text
82
+ if role == "assistant" and msg.get("tool_calls"):
83
+ tc_descriptions = []
84
+ for tc in msg["tool_calls"]:
85
+ if isinstance(tc, dict):
86
+ fn = tc.get("function", {})
87
+ name = fn.get("name", "unknown")
88
+ args = fn.get("arguments", "{}")
89
+ tc_descriptions.append(f"[Called tool: {name}({args})]")
90
+ if tc_descriptions:
91
+ content = (content + "\n" + "\n".join(tc_descriptions)).strip()
92
+ # Do NOT include tool_calls key — GGUF template will crash on it
93
+
94
+ clean["content"] = content
95
+ sanitized.append(clean)
96
+
97
+ return sanitized
98
+
46
99
  def chat_completion(
47
100
  self,
48
101
  messages: List[dict],
@@ -52,8 +105,9 @@ class LocalBackend:
52
105
  enable_thinking: bool = True,
53
106
  ) -> ChatResponse:
54
107
  model = self._ensure_model()
108
+ sanitized_msgs = self._sanitize_messages_for_gguf(messages)
55
109
  resp = model.create_chat_completion(
56
- messages=messages,
110
+ messages=sanitized_msgs,
57
111
  tools=tools,
58
112
  tool_choice="auto",
59
113
  temperature=temperature,
@@ -46,7 +46,7 @@ from .backends.base import InferenceBackend, ChatResponse
46
46
  from .backends.local_backend import LocalBackend
47
47
  from .backends.openai_backend import OpenAIBackend
48
48
  from .cache import get_cache
49
- from .config import Config, is_server_reachable
49
+ from .config import Config, is_server_reachable, is_internet_available
50
50
  from .memory import extract_and_save_memories
51
51
  from .discovery import build_task_context, gather_relevant_context, COMPLEXITY_CONFIG
52
52
  from .progress import ProgressMonitor
@@ -285,23 +285,54 @@ class AgentOrchestrator:
285
285
  enable_thinking=enable_thinking,
286
286
  )
287
287
  except Exception as e:
288
- # Online error -> fallback to local
288
+ # Online error -> only fallback to local GGUF if internet is genuinely unavailable.
289
+ # Server timeouts (read timeout, slow inference) should NOT trigger GGUF fallback
290
+ # because the server IS reachable — it's just slow.
289
291
  if not is_offline and isinstance(task_backend, OpenAIBackend):
290
- yield AgentEvent("fallback", {"error": str(e), "message": "Falling back to local engine"})
291
- task_backend = LocalBackend(cfg)
292
- session.is_offline = True
293
- is_offline = True
294
- active_tools = self.prompt_builder.get_tools(is_offline=True, complexity=complexity)
295
- try:
296
- resp = task_backend.chat_completion(
297
- messages=session.context.messages,
298
- tools=active_tools if active_tools else None,
299
- temperature=cfg.temperature,
300
- max_tokens=2048,
301
- enable_thinking=enable_thinking,
302
- )
303
- except Exception as fallback_err:
304
- yield AgentEvent("error", {"error": str(fallback_err)})
292
+ err_str = str(e).lower()
293
+ is_timeout = "timeout" in err_str or "timed out" in err_str or "read operation timed out" in err_str
294
+ is_network_down = not is_internet_available(timeout=1.5)
295
+
296
+ if is_timeout and not is_network_down:
297
+ # Server is alive but slow — retry, don't fallback to GGUF
298
+ yield AgentEvent("warning", {
299
+ "message": f"Server inference timed out: {e}. Retrying...",
300
+ })
301
+ try:
302
+ resp = task_backend.chat_completion(
303
+ messages=session.context.messages,
304
+ tools=active_tools if active_tools else None,
305
+ temperature=cfg.temperature,
306
+ max_tokens=max_tokens,
307
+ enable_thinking=enable_thinking,
308
+ )
309
+ except Exception as retry_err:
310
+ yield AgentEvent("error", {
311
+ "error": f"Server inference failed after retry: {retry_err}. "
312
+ "The server may be under heavy load. Please try again."
313
+ })
314
+ return
315
+ elif is_network_down:
316
+ # Internet genuinely unavailable — fallback to local GGUF
317
+ yield AgentEvent("fallback", {"error": str(e), "message": "Internet unavailable. Falling back to local engine"})
318
+ task_backend = LocalBackend(cfg)
319
+ session.is_offline = True
320
+ is_offline = True
321
+ active_tools = self.prompt_builder.get_tools(is_offline=True, complexity=complexity)
322
+ try:
323
+ resp = task_backend.chat_completion(
324
+ messages=session.context.messages,
325
+ tools=active_tools if active_tools else None,
326
+ temperature=cfg.temperature,
327
+ max_tokens=2048,
328
+ enable_thinking=enable_thinking,
329
+ )
330
+ except Exception as fallback_err:
331
+ yield AgentEvent("error", {"error": str(fallback_err)})
332
+ return
333
+ else:
334
+ # Other API error (400, 500, etc.) — report directly, don't fallback
335
+ yield AgentEvent("error", {"error": f"Server error: {e}"})
305
336
  return
306
337
  else:
307
338
  yield AgentEvent("error", {"error": str(e)})
@@ -119,7 +119,8 @@ def test_bug06_fallback_preserves_orchestrator_backend(tmp_path):
119
119
  )
120
120
 
121
121
  with patch.object(LocalBackend, "__init__", return_value=None), \
122
- patch.object(LocalBackend, "chat_completion", return_value=fallback_resp):
122
+ patch.object(LocalBackend, "chat_completion", return_value=fallback_resp), \
123
+ patch("codeagent.orchestrator.is_internet_available", return_value=False):
123
124
  events = list(orchestrator.run_task(session=session, task="Fallback test", cfg=cfg))
124
125
 
125
126
  event_types = [e.type for e in events]
File without changes
File without changes
File without changes
File without changes