alpiecode 10.5.0__tar.gz → 10.5.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-10.5.0 → alpiecode-10.5.1}/PKG-INFO +1 -1
- {alpiecode-10.5.0 → alpiecode-10.5.1}/pyproject.toml +1 -1
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/SOURCES.txt +3 -1
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/local_backend.py +31 -19
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/openai_backend.py +4 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/cli.py +26 -14
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/compaction.py +122 -10
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/config.py +2 -2
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/context.py +43 -14
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/dream_rsi.py +3 -2
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/executor.py +10 -6
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/local_model.py +38 -10
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/orchestrator.py +85 -22
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/prompt.py +5 -1
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/system1.py +80 -16
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/tools.py +27 -0
- alpiecode-10.5.1/test/test_context_and_trajectory.py +337 -0
- alpiecode-10.5.1/test/test_dream_rsi.py +478 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/README.md +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/setup.cfg +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/agent.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/cache.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/client.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/github.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/media.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/memory.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/metrics.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/progress.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/server.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/session.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/updater.py +0 -0
- {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/vscode_installer.py +0 -0
|
@@ -42,4 +42,6 @@ src/codeagent/backends/__init__.py
|
|
|
42
42
|
src/codeagent/backends/base.py
|
|
43
43
|
src/codeagent/backends/local_backend.py
|
|
44
44
|
src/codeagent/backends/openai_backend.py
|
|
45
|
-
src/codeagent/extension/alpiecode.vsix
|
|
45
|
+
src/codeagent/extension/alpiecode.vsix
|
|
46
|
+
test/test_context_and_trajectory.py
|
|
47
|
+
test/test_dream_rsi.py
|
|
@@ -17,7 +17,7 @@ Supports:
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any, Dict
|
|
19
19
|
|
|
20
|
-
__version__ = "10.5.
|
|
20
|
+
__version__ = "10.5.1"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -91,26 +91,38 @@ class LocalBackend:
|
|
|
91
91
|
elif not isinstance(content, str):
|
|
92
92
|
content = str(content)
|
|
93
93
|
|
|
94
|
-
# For assistant messages
|
|
94
|
+
# For assistant messages, condense bulky write_file / edit_file payloads from content
|
|
95
|
+
if role == "assistant":
|
|
96
|
+
from ..compaction import compact_text_tool_calls
|
|
97
|
+
content = compact_text_tool_calls(content)
|
|
98
|
+
|
|
99
|
+
# For assistant messages with tool_calls, embed tool call info in content text ONLY if not already marked
|
|
95
100
|
if role == "assistant" and msg.get("tool_calls"):
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
already_marked = (
|
|
102
|
+
"[Completed " in content
|
|
103
|
+
or "[Incomplete" in content
|
|
104
|
+
or "[Called tool:" in content
|
|
105
|
+
or "<tool_call>" in content
|
|
106
|
+
)
|
|
107
|
+
if not already_marked:
|
|
108
|
+
tc_descriptions = []
|
|
109
|
+
for tc in msg["tool_calls"]:
|
|
110
|
+
if isinstance(tc, dict):
|
|
111
|
+
fn = tc.get("function", {})
|
|
112
|
+
name = fn.get("name", "unknown")
|
|
113
|
+
args = fn.get("arguments", "{}")
|
|
114
|
+
# Do not duplicate full file content in context history
|
|
115
|
+
if name in ("write_file", "edit_file", "apply_patch"):
|
|
116
|
+
try:
|
|
117
|
+
import json as _json
|
|
118
|
+
a_dict = _json.loads(args) if isinstance(args, str) else args
|
|
119
|
+
p = a_dict.get("path") or a_dict.get("filename") or a_dict.get("file") or ""
|
|
120
|
+
args = f'{{"path": "{p}"}}'
|
|
121
|
+
except Exception:
|
|
122
|
+
args = "{...}"
|
|
123
|
+
tc_descriptions.append(f"[Called tool: {name}({args})]")
|
|
124
|
+
if tc_descriptions:
|
|
125
|
+
content = (content + "\n" + "\n".join(tc_descriptions)).strip()
|
|
114
126
|
# Do NOT include tool_calls key — GGUF template will crash on it
|
|
115
127
|
|
|
116
128
|
clean["content"] = content
|
|
@@ -54,9 +54,11 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
|
|
|
54
54
|
# If trailing comma at end: `{"a": 1,` -> `{"a": 1`
|
|
55
55
|
repaired = re.sub(r",\s*$", "", repaired)
|
|
56
56
|
|
|
57
|
+
was_truncated = False
|
|
57
58
|
# Check unclosed quotes: count non-escaped double quotes
|
|
58
59
|
quote_count = len(re.findall(r'(?<!\\)"', repaired))
|
|
59
60
|
if quote_count % 2 != 0:
|
|
61
|
+
was_truncated = True
|
|
60
62
|
dangling_bs = len(repaired) - len(repaired.rstrip('\\'))
|
|
61
63
|
if dangling_bs % 2 != 0:
|
|
62
64
|
repaired = repaired[:-1]
|
|
@@ -75,6 +77,8 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
|
|
|
75
77
|
try:
|
|
76
78
|
parsed = json.loads(repaired)
|
|
77
79
|
if isinstance(parsed, dict):
|
|
80
|
+
if was_truncated:
|
|
81
|
+
parsed["_was_truncated"] = True
|
|
78
82
|
return parsed
|
|
79
83
|
except Exception:
|
|
80
84
|
pass
|
|
@@ -31,16 +31,28 @@ def _show_banner():
|
|
|
31
31
|
def _normalize_args():
|
|
32
32
|
known_commands = {"init", "serve", "run", "chat", "plan", "diff", "undo", "doctor", "explain", "dream", "version", "-h", "--help", "-v", "--version"}
|
|
33
33
|
subcommand = None
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
# Normalize common flag typos / aliases before parsing
|
|
36
|
+
for idx, arg in enumerate(sys.argv):
|
|
37
|
+
if arg in ("--offlie", "-offline"):
|
|
38
|
+
sys.argv[idx] = "--offline"
|
|
39
|
+
|
|
35
40
|
# Check if a subcommand is present
|
|
36
41
|
for arg in sys.argv[1:]:
|
|
37
42
|
if arg in known_commands:
|
|
38
43
|
subcommand = arg
|
|
39
44
|
break
|
|
40
|
-
|
|
45
|
+
|
|
41
46
|
if not subcommand and len(sys.argv) > 1:
|
|
42
|
-
|
|
43
|
-
sys.argv.
|
|
47
|
+
# Determine if any positional non-flag argument was provided
|
|
48
|
+
positional_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
49
|
+
if not positional_args:
|
|
50
|
+
# Flags like --offline or --thinking given without a subcommand or task -> launch chat mode
|
|
51
|
+
subcommand = "chat"
|
|
52
|
+
sys.argv.insert(1, "chat")
|
|
53
|
+
else:
|
|
54
|
+
subcommand = "run"
|
|
55
|
+
sys.argv.insert(1, "run")
|
|
44
56
|
elif subcommand and subcommand not in ("-h", "--help", "-v", "--version") and sys.argv[1] != subcommand:
|
|
45
57
|
# Move subcommand to index 1 so subparsers parse arguments correctly without default override
|
|
46
58
|
sys.argv.remove(subcommand)
|
|
@@ -49,11 +61,11 @@ def _normalize_args():
|
|
|
49
61
|
if subcommand in ("run", "plan", "explain") and len(sys.argv) > 2:
|
|
50
62
|
cmd_idx = sys.argv.index(subcommand)
|
|
51
63
|
sub_args = sys.argv[cmd_idx + 1:]
|
|
52
|
-
|
|
64
|
+
|
|
53
65
|
flags = []
|
|
54
66
|
positionals = []
|
|
55
|
-
value_flags = {"--workdir", "--image", "--video", "--url", "--github", "--max-turns", "--host", "--port"}
|
|
56
|
-
|
|
67
|
+
value_flags = {"--workdir", "--image", "--video", "--url", "--github", "--max-turns", "--host", "--port", "--ctx-size"}
|
|
68
|
+
|
|
57
69
|
i = 0
|
|
58
70
|
while i < len(sub_args):
|
|
59
71
|
arg = sub_args[i]
|
|
@@ -67,7 +79,7 @@ def _normalize_args():
|
|
|
67
79
|
else:
|
|
68
80
|
positionals.append(arg)
|
|
69
81
|
i += 1
|
|
70
|
-
|
|
82
|
+
|
|
71
83
|
sys.argv = sys.argv[:cmd_idx + 1] + flags + positionals
|
|
72
84
|
|
|
73
85
|
|
|
@@ -87,7 +99,7 @@ def main():
|
|
|
87
99
|
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
88
100
|
common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
|
|
89
101
|
common.add_argument("--offline", "--local", dest="offline", action="store_true", help="Run using local offline GGUF model on GPU without internet")
|
|
90
|
-
common.add_argument("--ctx-size", type=int, default=None, help="Context size in tokens (default:
|
|
102
|
+
common.add_argument("--ctx-size", type=int, default=None, help="Context size in tokens (default: 8192 offline, 262144 online)")
|
|
91
103
|
|
|
92
104
|
from . import __version__
|
|
93
105
|
parser = argparse.ArgumentParser(
|
|
@@ -103,7 +115,7 @@ def main():
|
|
|
103
115
|
sub.add_parser("init", help="Configure your VLM/OpenAI-compatible endpoint")
|
|
104
116
|
|
|
105
117
|
# ── serve ──
|
|
106
|
-
serve_p = sub.add_parser("serve", help="Start the AlpieCode API server")
|
|
118
|
+
serve_p = sub.add_parser("serve", help="Start the AlpieCode API server", parents=[common])
|
|
107
119
|
serve_p.add_argument("--host", default="127.0.0.1", help="Host address to bind to (default: 127.0.0.1)")
|
|
108
120
|
serve_p.add_argument("--port", type=int, default=7169, help="Port to listen on (default: 7169)")
|
|
109
121
|
|
|
@@ -125,14 +137,14 @@ def main():
|
|
|
125
137
|
undo_p = sub.add_parser("undo", help="Revert file changes made by the last AlpieCode session", parents=[common])
|
|
126
138
|
|
|
127
139
|
# ── doctor ──
|
|
128
|
-
sub.add_parser("doctor", help="Run system health checks (CUDA, Python, network, dev tools)")
|
|
140
|
+
sub.add_parser("doctor", help="Run system health checks (CUDA, Python, network, dev tools)", parents=[common])
|
|
129
141
|
|
|
130
142
|
# ── explain ──
|
|
131
143
|
explain_p = sub.add_parser("explain", help="Explain a file, function, or codebase concept", parents=[common])
|
|
132
144
|
explain_p.add_argument("target", help="File path or concept to explain")
|
|
133
145
|
|
|
134
146
|
# ?? dream ??
|
|
135
|
-
dream_p = sub.add_parser('dream', help='Dream-RSI: offline recursive self-improvement simulation')
|
|
147
|
+
dream_p = sub.add_parser('dream', help='Dream-RSI: offline recursive self-improvement simulation', parents=[common])
|
|
136
148
|
dream_p.add_argument('--optimize', action='store_true', help='Run offline dreaming simulation to synthesize optimal search policy')
|
|
137
149
|
dream_p.add_argument('--stats', action='store_true', help='Show trajectory replay metrics and active policy status')
|
|
138
150
|
dream_p.add_argument('--clear', action='store_true', help='Clear historical trajectory replay pool')
|
|
@@ -227,9 +239,9 @@ def main():
|
|
|
227
239
|
|
|
228
240
|
if getattr(args, "offline", False):
|
|
229
241
|
cfg.base_url = "offline"
|
|
230
|
-
#
|
|
242
|
+
# 8192 default fits 4.4GB model + KV cache 100% in 6GB VRAM (4.75GB / 6GB) with zero host-paging
|
|
231
243
|
import os
|
|
232
|
-
cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE",
|
|
244
|
+
cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 8192))
|
|
233
245
|
os.environ["ALPIECODE_BASE_URL"] = "offline"
|
|
234
246
|
|
|
235
247
|
if getattr(args, "ctx_size", None):
|
|
@@ -15,6 +15,7 @@ Strategy:
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
import re
|
|
18
19
|
from typing import Any, Dict, List, Optional
|
|
19
20
|
|
|
20
21
|
# Our model's context window
|
|
@@ -219,27 +220,120 @@ def select_relevant_history(
|
|
|
219
220
|
return "### Relevant Historical Anchors:\n" + "\n".join(reversed(high_value_notes))
|
|
220
221
|
|
|
221
222
|
|
|
222
|
-
def
|
|
223
|
+
def compact_text_tool_calls(text: str) -> str:
|
|
224
|
+
"""Condense bulky file content in text-based tool calls."""
|
|
225
|
+
if not text or ("write_file" not in text and "edit_file" not in text and "apply_patch" not in text):
|
|
226
|
+
return text
|
|
227
|
+
|
|
228
|
+
def _replace_block(m):
|
|
229
|
+
raw = m.group(1).strip()
|
|
230
|
+
try:
|
|
231
|
+
from .backends.openai_backend import _repair_and_parse_json_arguments
|
|
232
|
+
data = _repair_and_parse_json_arguments(raw)
|
|
233
|
+
if isinstance(data, dict) and data.get("name") in ("write_file", "edit_file", "apply_patch"):
|
|
234
|
+
args = data.get("arguments", {})
|
|
235
|
+
if isinstance(args, dict):
|
|
236
|
+
p = args.get("path") or args.get("filename") or args.get("file") or "file"
|
|
237
|
+
return f"[Completed {data['name']} for '{p}']"
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
return m.group(0)
|
|
241
|
+
|
|
242
|
+
# 1. First condense complete/closed <tool_call>...</tool_call> blocks
|
|
243
|
+
text = re.sub(r"<tool_call>\s*(.*?)\s*</tool_call>", _replace_block, text, flags=re.DOTALL)
|
|
244
|
+
|
|
245
|
+
# 2. Also condense unclosed/truncated <tool_call> blocks (e.g. from token cutoff before </tool_call>)
|
|
246
|
+
if "<tool_call>" in text:
|
|
247
|
+
def _replace_unclosed(m):
|
|
248
|
+
raw = m.group(0)
|
|
249
|
+
t_name = "write_file"
|
|
250
|
+
p = "file"
|
|
251
|
+
name_m = re.search(r'"name":\s*"([^"]+)"', raw)
|
|
252
|
+
if name_m:
|
|
253
|
+
t_name = name_m.group(1)
|
|
254
|
+
path_m = re.search(r'"(?:path|filename|file)":\s*"([^"]+)"', raw)
|
|
255
|
+
if path_m:
|
|
256
|
+
p = path_m.group(1)
|
|
257
|
+
return f"[Incomplete/truncated {t_name} for '{p}']"
|
|
258
|
+
|
|
259
|
+
text = re.sub(r"<tool_call>(?:(?!</tool_call>).)*$", _replace_unclosed, text, flags=re.DOTALL)
|
|
260
|
+
|
|
261
|
+
return text
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def compact_tool_calls(tool_calls: List[Any]) -> List[Any]:
|
|
265
|
+
"""Condense bulky file content in historical tool calls to conserve context tokens."""
|
|
266
|
+
if not tool_calls:
|
|
267
|
+
return []
|
|
268
|
+
compacted = []
|
|
269
|
+
for tc in tool_calls:
|
|
270
|
+
if not isinstance(tc, dict):
|
|
271
|
+
compacted.append(tc)
|
|
272
|
+
continue
|
|
273
|
+
fn = tc.get("function", {})
|
|
274
|
+
if not isinstance(fn, dict):
|
|
275
|
+
compacted.append(tc)
|
|
276
|
+
continue
|
|
277
|
+
name = fn.get("name", "")
|
|
278
|
+
raw_args = fn.get("arguments", "")
|
|
279
|
+
if name in ("write_file", "edit_file", "apply_patch"):
|
|
280
|
+
try:
|
|
281
|
+
args = json.loads(raw_args) if isinstance(raw_args, str) else dict(raw_args)
|
|
282
|
+
content = args.get("content") or args.get("patch") or ""
|
|
283
|
+
path = args.get("path") or args.get("filename") or "file"
|
|
284
|
+
if isinstance(content, str) and len(content) > 150:
|
|
285
|
+
lines = content.splitlines()
|
|
286
|
+
args["content"] = f"[Content saved to {path} ({len(lines)} lines, {len(content)} bytes)]"
|
|
287
|
+
compact_tc = dict(tc)
|
|
288
|
+
compact_tc["function"] = dict(fn)
|
|
289
|
+
compact_tc["function"]["arguments"] = json.dumps(args) if isinstance(raw_args, str) else args
|
|
290
|
+
compacted.append(compact_tc)
|
|
291
|
+
continue
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
compacted.append(tc)
|
|
295
|
+
return compacted
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def compact_messages(messages: List[dict], keep_recent: int = KEEP_RECENT_TURNS) -> List[dict]:
|
|
223
299
|
"""
|
|
224
300
|
Compact a message list by summarizing older turns.
|
|
225
301
|
|
|
226
302
|
Preserves:
|
|
227
303
|
- System prompt (index 0)
|
|
228
304
|
- All user messages (verbatim)
|
|
229
|
-
- Last
|
|
305
|
+
- Last keep_recent messages (verbatim, with older tool arguments trimmed if bulky)
|
|
230
306
|
|
|
231
307
|
Summarizes:
|
|
232
308
|
- Older tool results (truncated)
|
|
233
309
|
- Older assistant reasoning (removed)
|
|
310
|
+
- Older write_file/edit_file payload content (condensed)
|
|
234
311
|
"""
|
|
235
|
-
if len(messages) <=
|
|
236
|
-
|
|
312
|
+
if len(messages) <= keep_recent + 2:
|
|
313
|
+
# Even if short, condense older bulky assistant tool calls while keeping latest intact
|
|
314
|
+
result = []
|
|
315
|
+
last_asst_idx = -1
|
|
316
|
+
for i in range(len(messages) - 1, -1, -1):
|
|
317
|
+
if messages[i].get("role") == "assistant":
|
|
318
|
+
last_asst_idx = i
|
|
319
|
+
break
|
|
320
|
+
for i, m in enumerate(messages):
|
|
321
|
+
if i != last_asst_idx and m.get("role") == "assistant":
|
|
322
|
+
cm = dict(m)
|
|
323
|
+
if cm.get("tool_calls"):
|
|
324
|
+
cm["tool_calls"] = compact_tool_calls(m["tool_calls"])
|
|
325
|
+
if cm.get("content"):
|
|
326
|
+
cm["content"] = compact_text_tool_calls(cm["content"])
|
|
327
|
+
result.append(cm)
|
|
328
|
+
else:
|
|
329
|
+
result.append(m)
|
|
330
|
+
return result
|
|
237
331
|
|
|
238
332
|
# Always keep system prompt
|
|
239
333
|
system = messages[0] if messages and messages[0].get("role") == "system" else None
|
|
240
334
|
|
|
241
335
|
# Split into old and recent
|
|
242
|
-
cutoff = len(messages) -
|
|
336
|
+
cutoff = len(messages) - keep_recent
|
|
243
337
|
|
|
244
338
|
# ── Tool-Call Pairing Guard ──
|
|
245
339
|
# If cutoff lands on a 'tool' message, move it backwards to include
|
|
@@ -263,11 +357,11 @@ def compact_messages(messages: List[dict]) -> List[dict]:
|
|
|
263
357
|
compacted_old.append(msg)
|
|
264
358
|
|
|
265
359
|
elif role == "assistant":
|
|
266
|
-
# Compact assistant messages: keep tool calls but remove reasoning
|
|
360
|
+
# Compact assistant messages: keep tool calls (condensed) but remove reasoning
|
|
267
361
|
compact_msg = {"role": "assistant"}
|
|
268
362
|
if msg.get("content"):
|
|
269
363
|
# Truncate long assistant content
|
|
270
|
-
content = msg["content"]
|
|
364
|
+
content = compact_text_tool_calls(msg["content"])
|
|
271
365
|
if len(content) > 200:
|
|
272
366
|
compact_msg["content"] = content[:200] + "..."
|
|
273
367
|
else:
|
|
@@ -276,7 +370,7 @@ def compact_messages(messages: List[dict]) -> List[dict]:
|
|
|
276
370
|
compact_msg["content"] = None
|
|
277
371
|
|
|
278
372
|
if msg.get("tool_calls"):
|
|
279
|
-
compact_msg["tool_calls"] = msg["tool_calls"]
|
|
373
|
+
compact_msg["tool_calls"] = compact_tool_calls(msg["tool_calls"])
|
|
280
374
|
compacted_old.append(compact_msg)
|
|
281
375
|
|
|
282
376
|
elif role == "tool":
|
|
@@ -301,11 +395,29 @@ def compact_messages(messages: List[dict]) -> List[dict]:
|
|
|
301
395
|
"content": _summarize_tool_result(tool_name, content),
|
|
302
396
|
})
|
|
303
397
|
|
|
398
|
+
# In recent messages, also condense older assistant tool calls (except the immediate last one)
|
|
399
|
+
last_asst_idx = -1
|
|
400
|
+
for i in range(len(recent_messages) - 1, -1, -1):
|
|
401
|
+
if recent_messages[i].get("role") == "assistant":
|
|
402
|
+
last_asst_idx = i
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
cleaned_recent = []
|
|
406
|
+
for i, m in enumerate(recent_messages):
|
|
407
|
+
if i != last_asst_idx and m.get("role") == "assistant":
|
|
408
|
+
cm = dict(m)
|
|
409
|
+
if cm.get("tool_calls"):
|
|
410
|
+
cm["tool_calls"] = compact_tool_calls(m["tool_calls"])
|
|
411
|
+
if cm.get("content"):
|
|
412
|
+
cm["content"] = compact_text_tool_calls(cm["content"])
|
|
413
|
+
cleaned_recent.append(cm)
|
|
414
|
+
else:
|
|
415
|
+
cleaned_recent.append(m)
|
|
416
|
+
|
|
304
417
|
# Rebuild message list
|
|
305
418
|
result = []
|
|
306
419
|
if system:
|
|
307
420
|
result.append(system)
|
|
308
421
|
result.extend(compacted_old)
|
|
309
|
-
result.extend(
|
|
310
|
-
|
|
422
|
+
result.extend(cleaned_recent)
|
|
311
423
|
return result
|
|
@@ -158,8 +158,8 @@ def load_config() -> Config:
|
|
|
158
158
|
# For offline mode, set high-performance 16k context (16384 tokens) for big multi-file projects
|
|
159
159
|
# Fits completely in 6GB VRAM on RTX 3050 with zero paging
|
|
160
160
|
if data.get("base_url") == "offline" and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
|
|
161
|
-
if data.get("n_ctx") in (32768, 4096):
|
|
162
|
-
data["n_ctx"] =
|
|
161
|
+
if data.get("n_ctx") in (32768, 4096, 16384):
|
|
162
|
+
data["n_ctx"] = 8192
|
|
163
163
|
if data.get("max_tokens", 8192) > 4096:
|
|
164
164
|
data["max_tokens"] = 4096
|
|
165
165
|
|
|
@@ -14,6 +14,8 @@ from typing import Any, Dict, List, Optional
|
|
|
14
14
|
|
|
15
15
|
from .backends.base import ChatResponse, ToolCall
|
|
16
16
|
from .compaction import (
|
|
17
|
+
compact_tool_calls,
|
|
18
|
+
compact_text_tool_calls,
|
|
17
19
|
CHARS_PER_TOKEN,
|
|
18
20
|
compact_messages,
|
|
19
21
|
estimate_tokens,
|
|
@@ -350,25 +352,42 @@ class ContextManager:
|
|
|
350
352
|
context_blocks.append(dict(m))
|
|
351
353
|
break
|
|
352
354
|
|
|
355
|
+
# ── Budget Verification & Headroom Limits ──
|
|
356
|
+
effective_limit = max_tokens or self.max_tokens
|
|
357
|
+
|
|
353
358
|
# ── Layer 4 & 5: Recent Messages and Current Request ──
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
359
|
+
# Condense bulky file write payloads in older assistant messages
|
|
360
|
+
last_asst_idx = -1
|
|
361
|
+
for i in range(len(recent_messages) - 1, -1, -1):
|
|
362
|
+
if recent_messages[i].get("role") == "assistant":
|
|
363
|
+
last_asst_idx = i
|
|
364
|
+
break
|
|
365
|
+
|
|
366
|
+
for i, m in enumerate(recent_messages):
|
|
367
|
+
if i != last_asst_idx and m.get("role") == "assistant":
|
|
368
|
+
if m.get("tool_calls"):
|
|
369
|
+
m["tool_calls"] = compact_tool_calls(m["tool_calls"])
|
|
370
|
+
if m.get("content"):
|
|
371
|
+
m["content"] = compact_text_tool_calls(m["content"])
|
|
372
|
+
elif m.get("role") == "tool":
|
|
373
|
+
c = str(m.get("content") or "")
|
|
374
|
+
if len(c) > 300 and "[Plan updated]" not in c and "Plan Status:" not in c:
|
|
375
|
+
lines = c.splitlines()
|
|
376
|
+
if len(lines) > 8:
|
|
377
|
+
top = "\n".join(lines[:3])
|
|
378
|
+
bot = "\n".join(lines[-2:])
|
|
379
|
+
m["content"] = f"[Output: {len(lines)} lines condensed]\n{top}\n...\n{bot}"
|
|
380
|
+
else:
|
|
381
|
+
m["content"] = c[:180] + "... [condensed]"
|
|
365
382
|
|
|
366
383
|
context_blocks.extend(recent_messages)
|
|
367
384
|
|
|
368
385
|
# ── Budget Verification ──
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
386
|
+
reserved_headroom = 2048 if effective_limit <= 8192 else 4096
|
|
387
|
+
prompt_budget = max(1024, effective_limit - reserved_headroom)
|
|
388
|
+
|
|
389
|
+
if estimate_tokens(context_blocks) > prompt_budget:
|
|
390
|
+
context_blocks = compact_messages(context_blocks, keep_recent=4 if effective_limit <= 8192 else 8)
|
|
372
391
|
|
|
373
392
|
if is_default_params:
|
|
374
393
|
self._cached_context = [dict(m) for m in context_blocks]
|
|
@@ -403,6 +422,16 @@ class ContextManager:
|
|
|
403
422
|
"""Returns parallel metadata list for all messages."""
|
|
404
423
|
return list(self._metadata)
|
|
405
424
|
|
|
425
|
+
def compact_for_headroom(self, target_headroom: int = 1536, n_ctx: int = 8192, tools_overhead: int = 800) -> int:
|
|
426
|
+
"""
|
|
427
|
+
Actively compact context to ensure at least target_headroom tokens are available for generation.
|
|
428
|
+
Returns the new estimated token count.
|
|
429
|
+
"""
|
|
430
|
+
self.invalidate_cache(reset_summary=True)
|
|
431
|
+
self._cached_summary = extract_conversation_summary(self._messages, self._metadata)
|
|
432
|
+
self._messages = compact_messages(self._messages, keep_recent=4)
|
|
433
|
+
return self.estimate_tokens()
|
|
434
|
+
|
|
406
435
|
def estimate_tokens(self) -> int:
|
|
407
436
|
"""Estimate tokens of the active built context."""
|
|
408
437
|
return estimate_tokens(self.build_context())
|
|
@@ -328,8 +328,9 @@ class DreamOptimizer:
|
|
|
328
328
|
best_stats = {}
|
|
329
329
|
|
|
330
330
|
# Exploration Policy Search Grid (Dream Simulation Space)
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
# Minimum threshold = 2 to prevent false positive loop detection on first call
|
|
332
|
+
syntax_thresholds = [2, 3]
|
|
333
|
+
general_thresholds = [2, 3, 4]
|
|
333
334
|
stall_thresholds = [2, 3, 4]
|
|
334
335
|
single_file_options = [True, False]
|
|
335
336
|
nudge_options = [True, False]
|
|
@@ -51,11 +51,13 @@ def parse_text_tool_calls(text: str) -> List[dict]:
|
|
|
51
51
|
if isinstance(data, dict) and "name" in data:
|
|
52
52
|
args = data.get("arguments")
|
|
53
53
|
if not isinstance(args, dict) or not args:
|
|
54
|
-
fallback_args = {k: v for k, v in data.items() if k not in ("name", "id", "type", "_raw_arguments", "_parse_error")}
|
|
54
|
+
fallback_args = {k: v for k, v in data.items() if k not in ("name", "id", "type", "_raw_arguments", "_parse_error", "_was_truncated")}
|
|
55
55
|
if fallback_args:
|
|
56
56
|
args = fallback_args
|
|
57
57
|
elif not isinstance(args, dict):
|
|
58
58
|
args = {}
|
|
59
|
+
if data.get("_was_truncated"):
|
|
60
|
+
args["_was_truncated"] = True
|
|
59
61
|
tool_calls.append({
|
|
60
62
|
"name": data["name"],
|
|
61
63
|
"arguments": args
|
|
@@ -148,11 +150,13 @@ def _parse_error_hint(stderr: str, stdout: str) -> str:
|
|
|
148
150
|
)
|
|
149
151
|
elif "command not found" in combined:
|
|
150
152
|
cmd_match = combined.split("command not found")[0].strip().split()
|
|
151
|
-
cmd = cmd_match[-1] if cmd_match else "unknown"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"
|
|
155
|
-
|
|
153
|
+
cmd = cmd_match[-1].rstrip(":") if cmd_match else "unknown"
|
|
154
|
+
hint = f"Hint: Command '{cmd}' not found. It may not be installed or not in PATH."
|
|
155
|
+
if cmd in ("node", "npm", "npx"):
|
|
156
|
+
hint += " For client-side web projects (HTML/CSS/JS), files do not require node to run. Output DONE: <summary> when complete."
|
|
157
|
+
else:
|
|
158
|
+
hint += " Check available commands or install the required package."
|
|
159
|
+
hints.append(hint)
|
|
156
160
|
|
|
157
161
|
return "\n".join(hints)
|
|
158
162
|
|
|
@@ -463,17 +463,34 @@ class LocalModel:
|
|
|
463
463
|
msgs = messages
|
|
464
464
|
|
|
465
465
|
# Context headroom safety guard: dynamically clamp max_tokens so prompt + max_tokens <= n_ctx
|
|
466
|
-
n_ctx_limit = getattr(self, "n_ctx",
|
|
466
|
+
n_ctx_limit = getattr(self, "n_ctx", 8192)
|
|
467
467
|
try:
|
|
468
|
-
total_chars =
|
|
468
|
+
total_chars = 0
|
|
469
|
+
for m in msgs:
|
|
470
|
+
total_chars += len(str(m.get("content") or ""))
|
|
471
|
+
for tc in m.get("tool_calls") or []:
|
|
472
|
+
if isinstance(tc, dict):
|
|
473
|
+
fn = tc.get("function", {})
|
|
474
|
+
total_chars += len(str(fn.get("arguments", ""))) + len(str(fn.get("name", "")))
|
|
469
475
|
if tools:
|
|
470
476
|
import json
|
|
471
477
|
total_chars += len(json.dumps(tools))
|
|
472
|
-
#
|
|
473
|
-
est_prompt_tokens = int(total_chars / 3.
|
|
474
|
-
available_tokens = n_ctx_limit - est_prompt_tokens -
|
|
475
|
-
if
|
|
476
|
-
|
|
478
|
+
# Accurate token estimate: 1 token ≈ 3.2 chars + template overhead
|
|
479
|
+
est_prompt_tokens = int(total_chars / 3.2) + 120
|
|
480
|
+
available_tokens = n_ctx_limit - est_prompt_tokens - 32
|
|
481
|
+
if available_tokens < 1024 and len(msgs) > 2:
|
|
482
|
+
from .compaction import compact_messages
|
|
483
|
+
msgs = compact_messages(msgs, keep_recent=2)
|
|
484
|
+
total_chars = sum(len(str(m.get("content") or "")) for m in msgs)
|
|
485
|
+
if tools:
|
|
486
|
+
import json
|
|
487
|
+
total_chars += len(json.dumps(tools))
|
|
488
|
+
est_prompt_tokens = int(total_chars / 3.2) + 120
|
|
489
|
+
available_tokens = n_ctx_limit - est_prompt_tokens - 32
|
|
490
|
+
|
|
491
|
+
safe_cap = max(256, available_tokens)
|
|
492
|
+
if max_tokens > safe_cap:
|
|
493
|
+
max_tokens = safe_cap
|
|
477
494
|
except Exception:
|
|
478
495
|
pass
|
|
479
496
|
|
|
@@ -506,7 +523,13 @@ class LocalModel:
|
|
|
506
523
|
requested = int(m.group(1))
|
|
507
524
|
win = int(m.group(2))
|
|
508
525
|
excess = requested - win
|
|
509
|
-
if
|
|
526
|
+
if excess > 0 and len(params.get("messages", [])) > 2:
|
|
527
|
+
from .compaction import compact_messages
|
|
528
|
+
params["messages"] = compact_messages(params["messages"], keep_recent=2)
|
|
529
|
+
cur_max = min(2048, max(256, win - int(requested - excess) // 2))
|
|
530
|
+
params["max_tokens"] = cur_max
|
|
531
|
+
continue
|
|
532
|
+
elif cur_max - excess - 32 >= 64:
|
|
510
533
|
cur_max = max(64, cur_max - excess - 32)
|
|
511
534
|
continue
|
|
512
535
|
raise
|
|
@@ -526,8 +549,13 @@ class LocalModel:
|
|
|
526
549
|
requested = int(m.group(1))
|
|
527
550
|
win = int(m.group(2))
|
|
528
551
|
excess = requested - win
|
|
529
|
-
|
|
530
|
-
|
|
552
|
+
if excess > 0 and len(params.get("messages", [])) > 2:
|
|
553
|
+
from .compaction import compact_messages
|
|
554
|
+
params["messages"] = compact_messages(params["messages"], keep_recent=2)
|
|
555
|
+
params["max_tokens"] = min(2048, max(512, win - int(requested - excess) // 2))
|
|
556
|
+
else:
|
|
557
|
+
new_max = max(128, params["max_tokens"] - excess - 32)
|
|
558
|
+
params["max_tokens"] = new_max
|
|
531
559
|
result_dict = llm.create_chat_completion(**params)
|
|
532
560
|
else:
|
|
533
561
|
raise
|