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.
- {alpiecode-9.0.0 → alpiecode-9.0.1}/PKG-INFO +1 -1
- {alpiecode-9.0.0 → alpiecode-9.0.1}/pyproject.toml +1 -1
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/__init__.py +1 -1
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/local_backend.py +55 -1
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/orchestrator.py +48 -17
- {alpiecode-9.0.0 → alpiecode-9.0.1}/test/test_phase1.py +2 -1
- {alpiecode-9.0.0 → alpiecode-9.0.1}/README.md +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/setup.cfg +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode/__init__.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/agent.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/base.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/cache.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/cli.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/client.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/compaction.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/config.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/context.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/discovery.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/doctor.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/executor.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/git_ops.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/github.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/guardian.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/guardrails.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/local_model.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/media.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/memory.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/progress.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/prompt.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/rephraser.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/server.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/session.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/tools.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/updater.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/test/test_phase2.py +0 -0
- {alpiecode-9.0.0 → alpiecode-9.0.1}/test/test_phase3.py +0 -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.
|
|
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=
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|