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.
- {alpiecode-9.0.4 → alpiecode-9.0.6}/PKG-INFO +1 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/pyproject.toml +1 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/SOURCES.txt +3 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/__init__.py +1 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/agent.py +65 -10
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/local_backend.py +18 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/cli.py +16 -2
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/client.py +1 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/config.py +6 -4
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/discovery.py +1 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/executor.py +18 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/local_model.py +29 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/orchestrator.py +1 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/server.py +7 -1
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/updater.py +8 -0
- alpiecode-9.0.6/test/test_audit_fixes.py +71 -0
- alpiecode-9.0.6/test/test_chat_mode.py +85 -0
- alpiecode-9.0.6/test/test_offline_mode.py +78 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/README.md +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/setup.cfg +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode/__init__.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/base.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/cache.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/compaction.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/context.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/doctor.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/git_ops.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/github.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/guardian.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/guardrails.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/media.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/memory.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/progress.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/prompt.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/rephraser.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/session.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/tools.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_claude_codex_capabilities.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_phase1.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_phase2.py +0 -0
- {alpiecode-9.0.4 → alpiecode-9.0.6}/test/test_phase3.py +0 -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.
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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,
|
|
155
|
-
#
|
|
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")
|
|
158
|
-
data["n_ctx"] =
|
|
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)}
|
|
@@ -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:
|
|
Binary file
|
|
@@ -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
|
-
|
|
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"):
|
|
@@ -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
|
|
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
|