alpiecode 4.0.0__tar.gz → 5.1.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-4.0.0 → alpiecode-5.1.1}/PKG-INFO +1 -1
- {alpiecode-4.0.0 → alpiecode-5.1.1}/pyproject.toml +1 -1
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/SOURCES.txt +1 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/agent.py +25 -1
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/cli.py +8 -3
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/config.py +1 -1
- alpiecode-5.1.1/src/codeagent/discovery.py +572 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/orchestrator.py +20 -8
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/prompt.py +138 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/tools.py +47 -9
- {alpiecode-4.0.0 → alpiecode-5.1.1}/README.md +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/setup.cfg +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode/__init__.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/__init__.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/backends/base.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/cache.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/client.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/compaction.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/context.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/doctor.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/executor.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/github.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/guardian.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/local_model.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/media.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/memory.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/server.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/session.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/updater.py +0 -0
- {alpiecode-4.0.0 → alpiecode-5.1.1}/src/codeagent/vscode_installer.py +0 -0
|
@@ -156,6 +156,7 @@ def run_agent(
|
|
|
156
156
|
github_repo: str = None,
|
|
157
157
|
server_url: str = None,
|
|
158
158
|
complexity: str = None,
|
|
159
|
+
debug: bool = False,
|
|
159
160
|
) -> list:
|
|
160
161
|
"""Run non-interactive agent task with Rich presentation."""
|
|
161
162
|
workdir = workdir.resolve()
|
|
@@ -200,8 +201,27 @@ def run_agent(
|
|
|
200
201
|
_checkpoint(workdir, "checkpoint: start")
|
|
201
202
|
|
|
202
203
|
current_turn = 0
|
|
204
|
+
last_discovery = {}
|
|
205
|
+
|
|
203
206
|
for event in event_stream:
|
|
204
|
-
if event.type == "
|
|
207
|
+
if event.type == "discovery" and verbose:
|
|
208
|
+
last_discovery = event.data
|
|
209
|
+
if debug and HAS_RICH:
|
|
210
|
+
console.print(Panel(
|
|
211
|
+
f"[bold cyan]🔍 Autonomous Discovery Engine[/bold cyan]\n"
|
|
212
|
+
f"• Intent: [bold]{event.data.get('intent')}[/bold]\n"
|
|
213
|
+
f"• Complexity: [bold]{event.data.get('complexity')}[/bold]\n"
|
|
214
|
+
f"• OS: {event.data.get('os')} | Shell: {event.data.get('shell')}\n"
|
|
215
|
+
f"• Project: {event.data.get('project_type')} ({event.data.get('file_count', 0)} files)\n"
|
|
216
|
+
f"• Frameworks: {', '.join(event.data.get('frameworks', [])) or 'None'}",
|
|
217
|
+
title="Pre-Execution Intelligence",
|
|
218
|
+
border_style="dim cyan",
|
|
219
|
+
padding=(0, 1)
|
|
220
|
+
))
|
|
221
|
+
elif debug:
|
|
222
|
+
print(f"[Discovery] intent={event.data.get('intent')}, complexity={event.data.get('complexity')}, shell={event.data.get('shell')}, project={event.data.get('project_type')}")
|
|
223
|
+
|
|
224
|
+
elif event.type == "start" and verbose:
|
|
205
225
|
data = event.data
|
|
206
226
|
if HAS_RICH:
|
|
207
227
|
console.rule("[bold blue]Agent Started[/bold blue]")
|
|
@@ -223,6 +243,10 @@ def run_agent(
|
|
|
223
243
|
comp_label = {"qa": "Q&A (instant)", "low": "Low (fast)", "medium": "Medium (balanced)", "high": "High (thorough)"}.get(comp, comp)
|
|
224
244
|
comp_color = {"qa": "cyan", "low": "green", "medium": "yellow", "high": "red"}.get(comp, "white")
|
|
225
245
|
console.print(f"⚡ Complexity: [bold {comp_color}]{comp_label}[/bold {comp_color}]", style="dim")
|
|
246
|
+
if last_discovery:
|
|
247
|
+
intent_str = last_discovery.get('intent', 'create').title()
|
|
248
|
+
proj_str = last_discovery.get('project_type', 'empty')
|
|
249
|
+
console.print(f"🔍 Discovery: {intent_str} on {proj_str} project ({last_discovery.get('shell', 'bash')})", style="dim")
|
|
226
250
|
if cfg.enable_thinking:
|
|
227
251
|
console.print(f"🧠 Reasoning: [bold green]ON[/bold green]", style="dim")
|
|
228
252
|
else:
|
|
@@ -48,13 +48,16 @@ def _normalize_args():
|
|
|
48
48
|
|
|
49
49
|
flags = []
|
|
50
50
|
positionals = []
|
|
51
|
+
value_flags = {"--workdir", "--image", "--video", "--url", "--github", "--max-turns", "--host", "--port"}
|
|
51
52
|
|
|
52
53
|
i = 0
|
|
53
54
|
while i < len(sub_args):
|
|
54
55
|
arg = sub_args[i]
|
|
55
56
|
if arg.startswith("-"):
|
|
56
57
|
flags.append(arg)
|
|
57
|
-
if
|
|
58
|
+
if "=" in arg:
|
|
59
|
+
pass # e.g. --complexity=low already contains its value
|
|
60
|
+
elif arg in value_flags and i + 1 < len(sub_args) and not sub_args[i + 1].startswith("-"):
|
|
58
61
|
flags.append(sub_args[i + 1])
|
|
59
62
|
i += 1
|
|
60
63
|
else:
|
|
@@ -76,9 +79,9 @@ def main():
|
|
|
76
79
|
common.add_argument("--max-turns", type=int, default=None, help="Override max turns")
|
|
77
80
|
common.add_argument("--thinking", "--reasoning", dest="thinking", action="store_true", help="Enable VLM reasoning mode")
|
|
78
81
|
common.add_argument("--no-thinking", "--non-thinking", dest="no_thinking", action="store_true", help="Disable VLM reasoning mode")
|
|
79
|
-
common.add_argument("--complexity", choices=["low", "medium", "high"], default=None, help="Task complexity: low (fast, 10 turns), medium (balanced, 20 turns), high (thorough, 40 turns)")
|
|
80
82
|
common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
|
|
81
83
|
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
84
|
+
common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
|
|
82
85
|
|
|
83
86
|
parser = argparse.ArgumentParser(
|
|
84
87
|
prog="alpiecode",
|
|
@@ -167,8 +170,8 @@ def main():
|
|
|
167
170
|
image_path=args.image,
|
|
168
171
|
video_path=getattr(args, "video", None),
|
|
169
172
|
url=getattr(args, "url", None),
|
|
170
|
-
complexity=getattr(args, "complexity", None),
|
|
171
173
|
github_repo=getattr(args, "github", None),
|
|
174
|
+
debug=getattr(args, "debug", False),
|
|
172
175
|
)
|
|
173
176
|
|
|
174
177
|
elif args.command == "chat":
|
|
@@ -195,6 +198,7 @@ def main():
|
|
|
195
198
|
video_path=getattr(args, "video", None),
|
|
196
199
|
url=getattr(args, "url", None),
|
|
197
200
|
github_repo=getattr(args, "github", None),
|
|
201
|
+
debug=getattr(args, "debug", False),
|
|
198
202
|
)
|
|
199
203
|
|
|
200
204
|
elif args.command == "doctor":
|
|
@@ -230,6 +234,7 @@ def main():
|
|
|
230
234
|
video_path=getattr(args, "video", None),
|
|
231
235
|
url=getattr(args, "url", None),
|
|
232
236
|
github_repo=getattr(args, "github", None),
|
|
237
|
+
debug=getattr(args, "debug", False),
|
|
233
238
|
)
|
|
234
239
|
|
|
235
240
|
elif args.command == "diff":
|
|
@@ -18,7 +18,7 @@ CONFIG_DIR = Path.home() / ".alpiecode"
|
|
|
18
18
|
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
19
19
|
|
|
20
20
|
# Config version — bump this when defaults change to trigger auto-migration
|
|
21
|
-
CONFIG_VERSION =
|
|
21
|
+
CONFIG_VERSION = 7 # v7: autonomous discovery engine (zero-flag flow)
|
|
22
22
|
|
|
23
23
|
DEFAULTS = {
|
|
24
24
|
"base_url": "http://20.245.200.125:8000/v1", # Primary endpoint
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Discovery Engine for AlpieCode -- Zero-LLM Pre-Execution Intelligence.
|
|
3
|
+
|
|
4
|
+
Runs in under 50ms before the agent turn loop starts. Detects:
|
|
5
|
+
- Task intent (qa, create, modify, debug, explain)
|
|
6
|
+
- Environment (OS, shell, Python version, venv, git)
|
|
7
|
+
- Repository state (project type, frameworks, file count, entry points)
|
|
8
|
+
- Smart complexity (from intent + repo state combined)
|
|
9
|
+
|
|
10
|
+
All detection is heuristic/filesystem-based -- no LLM calls, no network.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# -- Task Context ----------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class TaskContext:
|
|
29
|
+
"""Pre-computed intelligence about the task and environment."""
|
|
30
|
+
|
|
31
|
+
# Intent
|
|
32
|
+
intent: str = "create" # qa, create, modify, debug, explain
|
|
33
|
+
complexity: str = "low" # qa, low, medium, high
|
|
34
|
+
|
|
35
|
+
# Environment
|
|
36
|
+
os_name: str = "linux" # linux, darwin, windows
|
|
37
|
+
shell: str = "bash" # bash, powershell, cmd, sh, wsl
|
|
38
|
+
python_cmd: str = "python3" # python3 or python (platform-dependent)
|
|
39
|
+
python_version: str = "" # e.g. "3.11.2"
|
|
40
|
+
has_venv: bool = False
|
|
41
|
+
has_git: bool = False
|
|
42
|
+
|
|
43
|
+
# Repository
|
|
44
|
+
project_type: str = "empty" # python, node, rust, go, java, empty, unknown
|
|
45
|
+
frameworks: List[str] = field(default_factory=list)
|
|
46
|
+
file_count: int = 0
|
|
47
|
+
has_tests: bool = False
|
|
48
|
+
entry_points: List[str] = field(default_factory=list)
|
|
49
|
+
dependencies: List[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
# Budget (computed from complexity)
|
|
52
|
+
max_turns: int = 10
|
|
53
|
+
max_tokens: int = 8192
|
|
54
|
+
tool_set: str = "core" # none, core, full
|
|
55
|
+
enable_thinking: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# -- Intent Detection ------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Question patterns -- user wants an answer, not code
|
|
61
|
+
_QA_STARTERS = [
|
|
62
|
+
"what is", "what are", "what\'s", "what does", "what do",
|
|
63
|
+
"explain", "how does", "how do", "why does", "why do", "why is",
|
|
64
|
+
"describe", "define", "compare", "difference between",
|
|
65
|
+
"tell me about", "who invented", "who created", "when was",
|
|
66
|
+
"can you explain", "could you explain", "please explain",
|
|
67
|
+
"what\'s the difference", "is it possible", "is there a way",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Action verbs that indicate code generation
|
|
71
|
+
_ACTION_VERBS = [
|
|
72
|
+
"build", "create", "write", "make", "implement", "develop",
|
|
73
|
+
"generate", "code", "script", "program", "design", "set up",
|
|
74
|
+
"setup", "scaffold", "bootstrap", "initialize", "init",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Modification verbs
|
|
78
|
+
_MODIFY_VERBS = [
|
|
79
|
+
"fix", "add", "modify", "change", "update", "remove", "delete",
|
|
80
|
+
"refactor", "migrate", "upgrade", "downgrade", "rename",
|
|
81
|
+
"move", "restructure", "reorganize", "optimize", "improve",
|
|
82
|
+
"convert", "transform", "replace", "swap",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# Debug verbs
|
|
86
|
+
_DEBUG_VERBS = [
|
|
87
|
+
"debug", "investigate", "diagnose", "troubleshoot", "trace",
|
|
88
|
+
"find the bug", "find the error", "find the issue",
|
|
89
|
+
"why is this", "what\'s wrong", "what is wrong",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Explain verbs (when target is a file or code)
|
|
93
|
+
_EXPLAIN_VERBS = [
|
|
94
|
+
"explain this", "explain the", "walk me through",
|
|
95
|
+
"how does this work", "what does this do",
|
|
96
|
+
"analyze this", "review this", "read this",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detect_intent(task: str) -> str:
|
|
101
|
+
"""Classify task intent: qa, create, modify, debug, or explain.
|
|
102
|
+
|
|
103
|
+
Returns one of: 'qa', 'create', 'modify', 'debug', 'explain'
|
|
104
|
+
"""
|
|
105
|
+
t = task.lower().strip()
|
|
106
|
+
|
|
107
|
+
# Q&A: starts with a question pattern AND has no action verbs
|
|
108
|
+
all_action = _ACTION_VERBS + _MODIFY_VERBS
|
|
109
|
+
if any(t.startswith(pat) for pat in _QA_STARTERS):
|
|
110
|
+
if not any(re.search(r"\b" + re.escape(av) + r"\b", t) for av in all_action):
|
|
111
|
+
return "qa"
|
|
112
|
+
|
|
113
|
+
if t.endswith("?") and not any(re.search(r"\b" + re.escape(av) + r"\b", t) for av in all_action):
|
|
114
|
+
return "qa"
|
|
115
|
+
|
|
116
|
+
# Check if task starts with a create verb ("build a...", "create a...", "write a...")
|
|
117
|
+
if any(t.startswith(cv) for cv in ["build", "create", "write", "make", "implement", "develop", "generate", "code", "design"]):
|
|
118
|
+
return "create"
|
|
119
|
+
|
|
120
|
+
# Debug: explicitly asking to debug/investigate
|
|
121
|
+
if any(re.search(r"\b" + re.escape(dv) + r"\b", t) for dv in _DEBUG_VERBS):
|
|
122
|
+
return "debug"
|
|
123
|
+
|
|
124
|
+
# Explain: asking to explain existing code
|
|
125
|
+
if any(re.search(r"\b" + re.escape(ev) + r"\b", t) for ev in _EXPLAIN_VERBS):
|
|
126
|
+
return "explain"
|
|
127
|
+
|
|
128
|
+
# Modify: acting on existing code (e.g. "refactor", "fix", "update")
|
|
129
|
+
if any(re.search(r"\b" + re.escape(mv) + r"\b", t) for mv in ["refactor", "migrate", "fix", "update", "modify", "change", "delete", "remove", "rename"]):
|
|
130
|
+
return "modify"
|
|
131
|
+
|
|
132
|
+
# Create: building something new
|
|
133
|
+
if any(re.search(r"\b" + re.escape(av) + r"\b", t) for av in _ACTION_VERBS):
|
|
134
|
+
return "create"
|
|
135
|
+
|
|
136
|
+
if any(re.search(r"\b" + re.escape(mv) + r"\b", t) for mv in _MODIFY_VERBS):
|
|
137
|
+
return "modify"
|
|
138
|
+
|
|
139
|
+
if t.endswith("?"):
|
|
140
|
+
return "qa"
|
|
141
|
+
|
|
142
|
+
return "create"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# -- Environment Detection -------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def detect_environment() -> dict:
|
|
148
|
+
"""Detect OS, shell, Python version, and runtime environment.
|
|
149
|
+
|
|
150
|
+
Returns dict with: os_name, shell, python_cmd, python_version
|
|
151
|
+
"""
|
|
152
|
+
os_name = platform.system().lower()
|
|
153
|
+
if os_name == "linux":
|
|
154
|
+
# Check if we are inside WSL
|
|
155
|
+
try:
|
|
156
|
+
with open("/proc/version", "r") as f:
|
|
157
|
+
version_info = f.read().lower()
|
|
158
|
+
if "microsoft" in version_info or "wsl" in version_info:
|
|
159
|
+
os_name = "wsl"
|
|
160
|
+
except (FileNotFoundError, PermissionError):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
# Shell detection
|
|
164
|
+
shell = _detect_shell(os_name)
|
|
165
|
+
|
|
166
|
+
# Python command
|
|
167
|
+
python_cmd = "python" if os_name == "windows" else "python3"
|
|
168
|
+
|
|
169
|
+
# Python version
|
|
170
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"os_name": os_name if os_name != "wsl" else "linux",
|
|
174
|
+
"shell": "wsl" if os_name == "wsl" else shell,
|
|
175
|
+
"python_cmd": python_cmd,
|
|
176
|
+
"python_version": python_version,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _detect_shell(os_name: str) -> str:
|
|
181
|
+
"""Detect available shell, preferring WSL on Windows like Claude/Codex agents."""
|
|
182
|
+
|
|
183
|
+
if os_name == "windows":
|
|
184
|
+
# On Windows: prefer WSL (like Claude, Codex, Gemini agents do)
|
|
185
|
+
if _is_wsl_available():
|
|
186
|
+
return "wsl"
|
|
187
|
+
# Fallback to PowerShell
|
|
188
|
+
if shutil.which("powershell") or shutil.which("pwsh"):
|
|
189
|
+
return "powershell"
|
|
190
|
+
return "cmd"
|
|
191
|
+
|
|
192
|
+
if os_name in ("linux", "wsl"):
|
|
193
|
+
return "bash" # Standard on Linux/WSL
|
|
194
|
+
|
|
195
|
+
if os_name == "darwin":
|
|
196
|
+
return "bash" # Our commands work on both bash and zsh
|
|
197
|
+
|
|
198
|
+
return "sh"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _is_wsl_available() -> bool:
|
|
202
|
+
"""Check if WSL is installed and has at least one working distro."""
|
|
203
|
+
try:
|
|
204
|
+
result = subprocess.run(
|
|
205
|
+
["wsl", "--list", "--quiet"],
|
|
206
|
+
capture_output=True, text=True, timeout=3,
|
|
207
|
+
)
|
|
208
|
+
return result.returncode == 0 and bool(result.stdout.strip())
|
|
209
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# -- Repository Scanner ----------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def scan_repository(workdir: Path) -> dict:
|
|
216
|
+
"""Quick scan of repository to detect project type, frameworks, etc.
|
|
217
|
+
|
|
218
|
+
Designed to run in under 30ms by checking only key indicator files.
|
|
219
|
+
"""
|
|
220
|
+
workdir = workdir.resolve()
|
|
221
|
+
|
|
222
|
+
has_git = (workdir / ".git").is_dir()
|
|
223
|
+
has_venv = (workdir / ".venv").is_dir() or (workdir / "venv").is_dir()
|
|
224
|
+
|
|
225
|
+
# Quick file count (capped scan)
|
|
226
|
+
file_count = _quick_file_count(workdir, max_files=500)
|
|
227
|
+
|
|
228
|
+
# Detect project type and frameworks by checking indicator files
|
|
229
|
+
project_type, frameworks = _detect_project(workdir)
|
|
230
|
+
|
|
231
|
+
# Find entry points
|
|
232
|
+
entry_points = _find_entry_points(workdir, project_type)
|
|
233
|
+
|
|
234
|
+
# Check for tests
|
|
235
|
+
has_tests = _has_test_files(workdir)
|
|
236
|
+
|
|
237
|
+
# Read key dependencies (fast -- just reads manifest files)
|
|
238
|
+
dependencies = _read_dependencies(workdir, project_type)
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"has_git": has_git,
|
|
242
|
+
"has_venv": has_venv,
|
|
243
|
+
"file_count": file_count,
|
|
244
|
+
"project_type": project_type,
|
|
245
|
+
"frameworks": frameworks,
|
|
246
|
+
"entry_points": entry_points,
|
|
247
|
+
"has_tests": has_tests,
|
|
248
|
+
"dependencies": dependencies,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _quick_file_count(workdir: Path, max_files: int = 500) -> int:
|
|
253
|
+
"""Count tracked files quickly, respecting .gitignore if possible."""
|
|
254
|
+
try:
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
["git", "ls-files", "--cached", "--others", "--exclude-standard"],
|
|
257
|
+
cwd=workdir, capture_output=True, text=True, timeout=3,
|
|
258
|
+
)
|
|
259
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
260
|
+
return len(result.stdout.strip().splitlines())
|
|
261
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# Fallback: walk directory
|
|
265
|
+
count = 0
|
|
266
|
+
skip_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv",
|
|
267
|
+
".tox", "dist", "build", ".eggs", ".mypy_cache"}
|
|
268
|
+
for root, dirs, files in os.walk(workdir):
|
|
269
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith(".")]
|
|
270
|
+
count += len(files)
|
|
271
|
+
if count >= max_files:
|
|
272
|
+
return count
|
|
273
|
+
return count
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _detect_project(workdir: Path) -> Tuple[str, List[str]]:
|
|
277
|
+
"""Detect project type and frameworks from indicator files."""
|
|
278
|
+
frameworks = []
|
|
279
|
+
|
|
280
|
+
# Python indicators
|
|
281
|
+
has_pyproject = (workdir / "pyproject.toml").exists()
|
|
282
|
+
has_setup_py = (workdir / "setup.py").exists()
|
|
283
|
+
has_requirements = (workdir / "requirements.txt").exists()
|
|
284
|
+
has_pipfile = (workdir / "Pipfile").exists()
|
|
285
|
+
|
|
286
|
+
if has_pyproject or has_setup_py or has_requirements or has_pipfile:
|
|
287
|
+
frameworks = _detect_python_frameworks(workdir)
|
|
288
|
+
return "python", frameworks
|
|
289
|
+
|
|
290
|
+
# Node.js indicators
|
|
291
|
+
if (workdir / "package.json").exists():
|
|
292
|
+
frameworks = _detect_node_frameworks(workdir)
|
|
293
|
+
return "node", frameworks
|
|
294
|
+
|
|
295
|
+
# Rust
|
|
296
|
+
if (workdir / "Cargo.toml").exists():
|
|
297
|
+
return "rust", []
|
|
298
|
+
|
|
299
|
+
# Go
|
|
300
|
+
if (workdir / "go.mod").exists():
|
|
301
|
+
return "go", []
|
|
302
|
+
|
|
303
|
+
# Java
|
|
304
|
+
if (workdir / "pom.xml").exists() or (workdir / "build.gradle").exists():
|
|
305
|
+
return "java", []
|
|
306
|
+
|
|
307
|
+
# Check if there are any source files at all
|
|
308
|
+
for ext in (".py", ".js", ".ts", ".rs", ".go", ".java", ".c", ".cpp"):
|
|
309
|
+
for _ in workdir.glob(f"*{ext}"):
|
|
310
|
+
return "unknown", []
|
|
311
|
+
|
|
312
|
+
return "empty", []
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _detect_python_frameworks(workdir: Path) -> List[str]:
|
|
316
|
+
"""Detect Python frameworks from requirements/pyproject."""
|
|
317
|
+
frameworks = []
|
|
318
|
+
content = ""
|
|
319
|
+
|
|
320
|
+
for manifest in ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"]:
|
|
321
|
+
path = workdir / manifest
|
|
322
|
+
if path.exists():
|
|
323
|
+
try:
|
|
324
|
+
content += path.read_text(errors="replace").lower()
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
framework_map = {
|
|
329
|
+
"fastapi": "fastapi",
|
|
330
|
+
"flask": "flask",
|
|
331
|
+
"django": "django",
|
|
332
|
+
"pytest": "pytest",
|
|
333
|
+
"sqlalchemy": "sqlalchemy",
|
|
334
|
+
"pydantic": "pydantic",
|
|
335
|
+
"celery": "celery",
|
|
336
|
+
"redis": "redis",
|
|
337
|
+
"pymongo": "mongodb",
|
|
338
|
+
"torch": "pytorch",
|
|
339
|
+
"tensorflow": "tensorflow",
|
|
340
|
+
"numpy": "numpy",
|
|
341
|
+
"pandas": "pandas",
|
|
342
|
+
"streamlit": "streamlit",
|
|
343
|
+
"gradio": "gradio",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for pkg, name in framework_map.items():
|
|
347
|
+
if pkg in content:
|
|
348
|
+
frameworks.append(name)
|
|
349
|
+
|
|
350
|
+
return frameworks
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _detect_node_frameworks(workdir: Path) -> List[str]:
|
|
354
|
+
"""Detect Node.js frameworks from package.json."""
|
|
355
|
+
frameworks = []
|
|
356
|
+
pkg_path = workdir / "package.json"
|
|
357
|
+
try:
|
|
358
|
+
data = json.loads(pkg_path.read_text(errors="replace"))
|
|
359
|
+
all_deps = {}
|
|
360
|
+
all_deps.update(data.get("dependencies", {}))
|
|
361
|
+
all_deps.update(data.get("devDependencies", {}))
|
|
362
|
+
|
|
363
|
+
framework_map = {
|
|
364
|
+
"react": "react", "next": "nextjs", "vue": "vue",
|
|
365
|
+
"angular": "angular", "express": "express",
|
|
366
|
+
"fastify": "fastify", "nest": "nestjs",
|
|
367
|
+
"jest": "jest", "mocha": "mocha",
|
|
368
|
+
"typescript": "typescript",
|
|
369
|
+
}
|
|
370
|
+
for pkg, name in framework_map.items():
|
|
371
|
+
if any(pkg in dep for dep in all_deps):
|
|
372
|
+
frameworks.append(name)
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
return frameworks
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _find_entry_points(workdir: Path, project_type: str) -> List[str]:
|
|
379
|
+
"""Find likely entry point files."""
|
|
380
|
+
entry_names = [
|
|
381
|
+
"main.py", "app.py", "server.py", "manage.py", "run.py",
|
|
382
|
+
"index.py", "cli.py", "__main__.py",
|
|
383
|
+
"index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts",
|
|
384
|
+
"main.go", "main.rs", "Main.java",
|
|
385
|
+
]
|
|
386
|
+
found = []
|
|
387
|
+
for name in entry_names:
|
|
388
|
+
if (workdir / name).exists():
|
|
389
|
+
found.append(name)
|
|
390
|
+
# Also check src/ directory
|
|
391
|
+
if (workdir / "src" / name).exists():
|
|
392
|
+
found.append(f"src/{name}")
|
|
393
|
+
return found[:5]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _has_test_files(workdir: Path) -> bool:
|
|
397
|
+
"""Check if the project has test files."""
|
|
398
|
+
test_indicators = ["tests", "test", "__tests__", "spec"]
|
|
399
|
+
for d in test_indicators:
|
|
400
|
+
if (workdir / d).is_dir():
|
|
401
|
+
return True
|
|
402
|
+
|
|
403
|
+
# Check for test_*.py or *_test.py files in root
|
|
404
|
+
try:
|
|
405
|
+
for f in workdir.iterdir():
|
|
406
|
+
if f.is_file() and (f.name.startswith("test_") or f.name.endswith("_test.py")):
|
|
407
|
+
return True
|
|
408
|
+
except PermissionError:
|
|
409
|
+
pass
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _read_dependencies(workdir: Path, project_type: str) -> List[str]:
|
|
414
|
+
"""Read top-level dependency names (fast, no resolution)."""
|
|
415
|
+
deps = []
|
|
416
|
+
if project_type == "python":
|
|
417
|
+
req_path = workdir / "requirements.txt"
|
|
418
|
+
if req_path.exists():
|
|
419
|
+
try:
|
|
420
|
+
for line in req_path.read_text(errors="replace").splitlines():
|
|
421
|
+
line = line.strip()
|
|
422
|
+
if line and not line.startswith("#") and not line.startswith("-"):
|
|
423
|
+
name = re.split(r"[=<>!~;\[]", line)[0].strip()
|
|
424
|
+
if name:
|
|
425
|
+
deps.append(name)
|
|
426
|
+
except Exception:
|
|
427
|
+
pass
|
|
428
|
+
return deps[:20]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# -- Complexity Engine ------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
def compute_complexity(intent: str, repo_info: dict, task: str) -> str:
|
|
434
|
+
"""Compute task complexity from intent + repo state + task text.
|
|
435
|
+
|
|
436
|
+
This is smarter than pure keyword matching because it considers
|
|
437
|
+
the actual project context.
|
|
438
|
+
|
|
439
|
+
Returns: 'qa', 'low', 'medium', or 'high'
|
|
440
|
+
"""
|
|
441
|
+
if intent == "qa":
|
|
442
|
+
return "qa"
|
|
443
|
+
|
|
444
|
+
task_lower = task.lower()
|
|
445
|
+
file_count = repo_info.get("file_count", 0)
|
|
446
|
+
frameworks = repo_info.get("frameworks", [])
|
|
447
|
+
project_type = repo_info.get("project_type", "empty")
|
|
448
|
+
|
|
449
|
+
# -- HIGH: explicitly complex multi-component tasks --
|
|
450
|
+
high_keywords = [
|
|
451
|
+
"full stack", "fullstack", "full-stack",
|
|
452
|
+
"microservice", "e-commerce", "ecommerce",
|
|
453
|
+
"docker", "kubernetes", "ci/cd", "pipeline",
|
|
454
|
+
"machine learning", "deep learning", "neural network",
|
|
455
|
+
"train a model", "training pipeline",
|
|
456
|
+
"authentication", "oauth", "jwt auth",
|
|
457
|
+
"database migration", "schema migration",
|
|
458
|
+
]
|
|
459
|
+
if any(kw in task_lower for kw in high_keywords):
|
|
460
|
+
return "high"
|
|
461
|
+
|
|
462
|
+
# Large codebase + modify/debug -> high
|
|
463
|
+
if intent in ("modify", "debug") and file_count > 50:
|
|
464
|
+
if any(kw in task_lower for kw in ["refactor", "restructure", "redesign", "rewrite", "migrate"]):
|
|
465
|
+
return "high"
|
|
466
|
+
|
|
467
|
+
# -- MEDIUM: multi-step tasks --
|
|
468
|
+
medium_keywords = [
|
|
469
|
+
"api", "rest api", "graphql", "server",
|
|
470
|
+
"game", "snake", "tetris", "chess", "pong", "sudoku",
|
|
471
|
+
"website", "web page", "web app", "webapp",
|
|
472
|
+
"test suite", "unit tests", "integration test",
|
|
473
|
+
"dashboard", "portfolio",
|
|
474
|
+
"react", "vue", "angular", "next.js",
|
|
475
|
+
"django", "flask app", "fastapi app",
|
|
476
|
+
]
|
|
477
|
+
if any(kw in task_lower for kw in medium_keywords):
|
|
478
|
+
return "medium"
|
|
479
|
+
|
|
480
|
+
# Modify/debug on existing project with frameworks -> medium
|
|
481
|
+
if intent in ("modify", "debug") and frameworks:
|
|
482
|
+
return "medium"
|
|
483
|
+
|
|
484
|
+
# Modify/debug on project with 10+ files -> medium
|
|
485
|
+
if intent in ("modify", "debug") and file_count > 10:
|
|
486
|
+
return "medium"
|
|
487
|
+
|
|
488
|
+
# Create on existing project (adding to it) -> medium
|
|
489
|
+
if intent == "create" and project_type not in ("empty", "unknown") and file_count > 5:
|
|
490
|
+
return "medium"
|
|
491
|
+
|
|
492
|
+
# -- LOW: everything else --
|
|
493
|
+
return "low"
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# -- Complexity Configuration -----------------------------------------------
|
|
497
|
+
|
|
498
|
+
COMPLEXITY_CONFIG = {
|
|
499
|
+
"qa": {
|
|
500
|
+
"max_turns": 3,
|
|
501
|
+
"max_tokens": 4096,
|
|
502
|
+
"tool_set": "none",
|
|
503
|
+
"enable_thinking": False,
|
|
504
|
+
},
|
|
505
|
+
"low": {
|
|
506
|
+
"max_turns": 10,
|
|
507
|
+
"max_tokens": 8192,
|
|
508
|
+
"tool_set": "core",
|
|
509
|
+
"enable_thinking": False,
|
|
510
|
+
},
|
|
511
|
+
"medium": {
|
|
512
|
+
"max_turns": 20,
|
|
513
|
+
"max_tokens": 8192,
|
|
514
|
+
"tool_set": "full",
|
|
515
|
+
"enable_thinking": False,
|
|
516
|
+
},
|
|
517
|
+
"high": {
|
|
518
|
+
"max_turns": 40,
|
|
519
|
+
"max_tokens": 16384,
|
|
520
|
+
"tool_set": "full",
|
|
521
|
+
"enable_thinking": True,
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# -- Main Entry Point -------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
def build_task_context(task: str, workdir: Path) -> TaskContext:
|
|
529
|
+
"""Build complete TaskContext by running all discovery phases.
|
|
530
|
+
|
|
531
|
+
This is the single entry point -- call this from the orchestrator.
|
|
532
|
+
Runs in under 50ms total, no LLM calls, no network.
|
|
533
|
+
"""
|
|
534
|
+
# Phase 1: Intent
|
|
535
|
+
intent = detect_intent(task)
|
|
536
|
+
|
|
537
|
+
# Phase 2: Environment
|
|
538
|
+
env = detect_environment()
|
|
539
|
+
|
|
540
|
+
# Phase 3: Repository scan
|
|
541
|
+
repo = scan_repository(workdir)
|
|
542
|
+
|
|
543
|
+
# Phase 4: Complexity (uses intent + repo)
|
|
544
|
+
complexity = compute_complexity(intent, repo, task)
|
|
545
|
+
|
|
546
|
+
# Phase 5: Budget from complexity
|
|
547
|
+
budget = COMPLEXITY_CONFIG[complexity]
|
|
548
|
+
|
|
549
|
+
return TaskContext(
|
|
550
|
+
# Intent
|
|
551
|
+
intent=intent,
|
|
552
|
+
complexity=complexity,
|
|
553
|
+
# Environment
|
|
554
|
+
os_name=env["os_name"],
|
|
555
|
+
shell=env["shell"],
|
|
556
|
+
python_cmd=env["python_cmd"],
|
|
557
|
+
python_version=env["python_version"],
|
|
558
|
+
has_venv=repo["has_venv"],
|
|
559
|
+
has_git=repo["has_git"],
|
|
560
|
+
# Repository
|
|
561
|
+
project_type=repo["project_type"],
|
|
562
|
+
frameworks=repo["frameworks"],
|
|
563
|
+
file_count=repo["file_count"],
|
|
564
|
+
has_tests=repo["has_tests"],
|
|
565
|
+
entry_points=repo["entry_points"],
|
|
566
|
+
dependencies=repo["dependencies"],
|
|
567
|
+
# Budget
|
|
568
|
+
max_turns=budget["max_turns"],
|
|
569
|
+
max_tokens=budget["max_tokens"],
|
|
570
|
+
tool_set=budget["tool_set"],
|
|
571
|
+
enable_thinking=budget["enable_thinking"],
|
|
572
|
+
)
|
|
@@ -14,7 +14,8 @@ from .backends.openai_backend import OpenAIBackend
|
|
|
14
14
|
from .cache import get_cache
|
|
15
15
|
from .config import Config, is_server_reachable
|
|
16
16
|
from .memory import extract_and_save_memories
|
|
17
|
-
from .
|
|
17
|
+
from .discovery import build_task_context, COMPLEXITY_CONFIG
|
|
18
|
+
from .prompt import PromptBuilder, classify_task
|
|
18
19
|
from .session import Session, SessionManager
|
|
19
20
|
|
|
20
21
|
|
|
@@ -55,15 +56,25 @@ class AgentOrchestrator:
|
|
|
55
56
|
) -> Iterator[AgentEvent]:
|
|
56
57
|
"""Run full agent task loop. Yields AgentEvents."""
|
|
57
58
|
|
|
58
|
-
# ──
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
# ── Phase 0: Discovery — pre-compute task intelligence ──
|
|
60
|
+
task_context = build_task_context(task, session.workdir)
|
|
61
|
+
complexity = task_context.complexity
|
|
62
|
+
|
|
63
|
+
yield AgentEvent("discovery", {
|
|
64
|
+
"intent": task_context.intent,
|
|
65
|
+
"complexity": task_context.complexity,
|
|
66
|
+
"os": task_context.os_name,
|
|
67
|
+
"shell": task_context.shell,
|
|
68
|
+
"project_type": task_context.project_type,
|
|
69
|
+
"frameworks": task_context.frameworks,
|
|
70
|
+
"file_count": task_context.file_count,
|
|
71
|
+
})
|
|
61
72
|
|
|
62
73
|
comp_cfg = COMPLEXITY_CONFIG.get(complexity, COMPLEXITY_CONFIG["low"])
|
|
63
74
|
|
|
64
75
|
# ── Determine effective max_turns and max_tokens ──
|
|
65
|
-
effective_max_turns =
|
|
66
|
-
effective_max_tokens =
|
|
76
|
+
effective_max_turns = task_context.max_turns
|
|
77
|
+
effective_max_tokens = task_context.max_tokens
|
|
67
78
|
|
|
68
79
|
# User override: if they set --max-turns explicitly, respect it
|
|
69
80
|
if cfg.max_turns != 20: # 20 is new default, so non-default = explicit
|
|
@@ -102,7 +113,8 @@ class AgentOrchestrator:
|
|
|
102
113
|
# ── Configure tools & system prompt based on complexity ──
|
|
103
114
|
active_tools = self.prompt_builder.get_tools(is_offline=is_offline, complexity=complexity)
|
|
104
115
|
system_prompt = self.prompt_builder.build_system_prompt(
|
|
105
|
-
session.workdir, is_offline=is_offline, complexity=complexity
|
|
116
|
+
session.workdir, is_offline=is_offline, complexity=complexity,
|
|
117
|
+
task_context=task_context,
|
|
106
118
|
)
|
|
107
119
|
session.context.set_system_prompt(system_prompt)
|
|
108
120
|
|
|
@@ -126,7 +138,7 @@ class AgentOrchestrator:
|
|
|
126
138
|
})
|
|
127
139
|
|
|
128
140
|
# ── Adaptive thinking ──
|
|
129
|
-
enable_thinking = cfg.enable_thinking
|
|
141
|
+
enable_thinking = cfg.enable_thinking or task_context.enable_thinking
|
|
130
142
|
if enable_thinking and complexity in ("qa", "low"):
|
|
131
143
|
enable_thinking = False
|
|
132
144
|
yield AgentEvent("adaptive_mode", {"message": "Simple task detected, skipping deep reasoning."})
|
|
@@ -148,6 +148,12 @@ NEVER write partial code. Write it ALL in one go.
|
|
|
148
148
|
- bash for running code, compiling, testing
|
|
149
149
|
- Do NOT use bash just to list files or explore — start coding instead
|
|
150
150
|
|
|
151
|
+
## Testing Web Servers & REST APIs — CRITICAL
|
|
152
|
+
- For FastAPI or Flask APIs, ALWAYS test endpoints using in-process test clients:
|
|
153
|
+
- FastAPI: `from fastapi.testclient import TestClient; client = TestClient(app); res = client.get('/api/items')`
|
|
154
|
+
- Flask: `client = app.test_client(); res = client.get('/api/items')`
|
|
155
|
+
- NEVER start background servers with `&`, `sleep`, or `curl`. TestClient runs in 0.05s and tests all routes without daemon processes!
|
|
156
|
+
|
|
151
157
|
## Safety
|
|
152
158
|
- Never commit, push, or open pull requests unless asked
|
|
153
159
|
- Never write secrets, API keys, or tokens into files
|
|
@@ -215,6 +221,12 @@ For multi-file projects, build in dependency order:
|
|
|
215
221
|
- Use web_search only when you genuinely need external documentation
|
|
216
222
|
- bash for running, compiling, testing
|
|
217
223
|
|
|
224
|
+
## Testing Web Servers & REST APIs — CRITICAL
|
|
225
|
+
- For FastAPI or Flask APIs, ALWAYS test endpoints using in-process test clients:
|
|
226
|
+
- FastAPI: `from fastapi.testclient import TestClient; client = TestClient(app); res = client.get('/api/items')`
|
|
227
|
+
- Flask: `client = app.test_client(); res = client.get('/api/items')`
|
|
228
|
+
- NEVER start background servers with `&`, `sleep`, or `curl`. TestClient runs in 0.05s and tests all routes without daemon processes!
|
|
229
|
+
|
|
218
230
|
## Diagnosing Failures
|
|
219
231
|
1. Read the full error output carefully
|
|
220
232
|
2. Identify the root cause (not just the symptom)
|
|
@@ -347,6 +359,89 @@ COMPLEXITY_CONFIG = {
|
|
|
347
359
|
}
|
|
348
360
|
|
|
349
361
|
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# -- Shell-Aware Prompt Fragments ------------------------------------------
|
|
365
|
+
|
|
366
|
+
BASH_RULES = """\
|
|
367
|
+
## Shell Environment: Bash (Linux/macOS)
|
|
368
|
+
- You are ALREADY in the project root directory. NEVER run `cd /home/...` or `cd /testbed`
|
|
369
|
+
- Use `&&` to chain commands
|
|
370
|
+
- Use `export VAR=value` for environment variables
|
|
371
|
+
- Use `python3` (not `python` which may be Python 2)
|
|
372
|
+
- For testing python code without extra packages, use `python3 -m unittest`
|
|
373
|
+
- Background processes: `command &` (but AVOID for servers -- use TestClient)
|
|
374
|
+
- File paths use `/` forward slashes
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
WSL_RULES = """\
|
|
378
|
+
## Shell Environment: WSL Bash (Windows Subsystem for Linux)
|
|
379
|
+
- You are ALREADY in the project root directory. NEVER run `cd /home/...` or `cd /testbed`
|
|
380
|
+
- Use bash syntax: `&&` to chain, `export VAR=value` for env vars
|
|
381
|
+
- Use `python3` (not `python`)
|
|
382
|
+
- For testing python code, use `python3 -m unittest`
|
|
383
|
+
- The working directory is a Linux path (e.g. /home/user/project)
|
|
384
|
+
- Do NOT use PowerShell or Windows commands (no `dir`, `type`, `$env:`)
|
|
385
|
+
- File paths use `/` forward slashes
|
|
386
|
+
- Network: `localhost` in WSL may differ from Windows -- use `127.0.0.1`
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
POWERSHELL_RULES = """\
|
|
390
|
+
## Shell Environment: PowerShell (Windows)
|
|
391
|
+
- Use `;` to chain commands, NOT `&&` (PowerShell does not support `&&`)
|
|
392
|
+
- Use `$env:VAR = "value"` for environment variables, NOT `export VAR=value`
|
|
393
|
+
- Use `python` (not `python3`)
|
|
394
|
+
- Do NOT use `echo -e` or ANSI escapes -- use `Write-Output`
|
|
395
|
+
- Do NOT use `grep` -- use `Select-String` or `findstr`
|
|
396
|
+
- Do NOT use `&` for background processes -- they will fail
|
|
397
|
+
- File paths use `\\` but `/` also works in most cases
|
|
398
|
+
- Do NOT use `curl` -- use `Invoke-WebRequest` or Python's requests/httpx
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
REPO_CONTEXT_TEMPLATE = """\
|
|
402
|
+
## Current Project Context
|
|
403
|
+
- Project type: {project_type}
|
|
404
|
+
- Frameworks: {frameworks}
|
|
405
|
+
- Tracked files: {file_count}
|
|
406
|
+
- Entry points: {entry_points}
|
|
407
|
+
- Has tests: {has_tests}
|
|
408
|
+
{extra}"""
|
|
409
|
+
|
|
410
|
+
INTENT_CREATE = """\
|
|
411
|
+
## Task Intent: Create New Code
|
|
412
|
+
- Write the COMPLETE, WORKING code on Turn 1 using write_file
|
|
413
|
+
- Do NOT explore the filesystem first -- start coding immediately
|
|
414
|
+
- Include ALL imports, ALL functions, ALL logic -- no stubs, no TODOs
|
|
415
|
+
- After writing, run with bash to verify
|
|
416
|
+
- Fix any errors with edit_file, then re-run
|
|
417
|
+
- When everything works: DONE: <summary>
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
INTENT_MODIFY = """\
|
|
421
|
+
## Task Intent: Modify Existing Code
|
|
422
|
+
- Turn 1: Use read_file to read the relevant file(s)
|
|
423
|
+
- Turn 2: Use edit_file to make targeted changes
|
|
424
|
+
- Turn 3: Use bash to test the changes
|
|
425
|
+
- Fix any regressions, then: DONE: <summary>
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
INTENT_DEBUG = """\
|
|
429
|
+
## Task Intent: Debug / Investigate
|
|
430
|
+
- Turn 1: Reproduce the issue -- run the failing command/test
|
|
431
|
+
- Turn 2: Read error output carefully, use read_file to examine source
|
|
432
|
+
- Turn 3: Fix the root cause with edit_file (not the symptom)
|
|
433
|
+
- Turn 4: Re-run to verify the fix
|
|
434
|
+
- DONE: <summary of what was wrong and how it was fixed>
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
INTENT_EXPLAIN = """\
|
|
438
|
+
## Task Intent: Explain / Analyze
|
|
439
|
+
- Read the relevant file(s) with read_file
|
|
440
|
+
- Provide a clear, structured explanation
|
|
441
|
+
- Do NOT modify any files unless explicitly asked
|
|
442
|
+
- DONE: <explanation>
|
|
443
|
+
"""
|
|
444
|
+
|
|
350
445
|
class PromptBuilder:
|
|
351
446
|
"""Constructs system prompts and user content."""
|
|
352
447
|
|
|
@@ -355,7 +450,9 @@ class PromptBuilder:
|
|
|
355
450
|
workdir: Path,
|
|
356
451
|
is_offline: bool = False,
|
|
357
452
|
complexity: str = "low",
|
|
453
|
+
task_context=None,
|
|
358
454
|
) -> str:
|
|
455
|
+
"""Build system prompt, optionally enhanced with TaskContext discovery."""
|
|
359
456
|
if is_offline:
|
|
360
457
|
prompt = OFFLINE_SYSTEM_PROMPT
|
|
361
458
|
elif complexity == "high":
|
|
@@ -363,6 +460,47 @@ class PromptBuilder:
|
|
|
363
460
|
else:
|
|
364
461
|
prompt = SYSTEM_PROMPT
|
|
365
462
|
|
|
463
|
+
# -- Inject shell-aware rules from TaskContext --
|
|
464
|
+
if task_context is not None:
|
|
465
|
+
shell = getattr(task_context, "shell", "bash")
|
|
466
|
+
if shell == "powershell":
|
|
467
|
+
prompt += "\n\n" + POWERSHELL_RULES
|
|
468
|
+
elif shell == "wsl":
|
|
469
|
+
prompt += "\n\n" + WSL_RULES
|
|
470
|
+
elif shell == "bash":
|
|
471
|
+
prompt += "\n\n" + BASH_RULES
|
|
472
|
+
|
|
473
|
+
# Inject repo context (if not an empty dir)
|
|
474
|
+
if getattr(task_context, "project_type", "empty") != "empty":
|
|
475
|
+
frameworks_str = ", ".join(task_context.frameworks) if task_context.frameworks else "none detected"
|
|
476
|
+
entry_str = ", ".join(task_context.entry_points) if task_context.entry_points else "none found"
|
|
477
|
+
extra_lines = []
|
|
478
|
+
if task_context.has_venv:
|
|
479
|
+
extra_lines.append("- Virtual environment: detected (.venv)")
|
|
480
|
+
if task_context.dependencies:
|
|
481
|
+
extra_lines.append(f"- Key dependencies: {', '.join(task_context.dependencies[:8])}")
|
|
482
|
+
extra = "\n".join(extra_lines)
|
|
483
|
+
prompt += "\n\n" + REPO_CONTEXT_TEMPLATE.format(
|
|
484
|
+
project_type=task_context.project_type,
|
|
485
|
+
frameworks=frameworks_str,
|
|
486
|
+
file_count=task_context.file_count,
|
|
487
|
+
entry_points=entry_str,
|
|
488
|
+
has_tests="yes" if task_context.has_tests else "no",
|
|
489
|
+
extra=extra,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Inject intent-specific workflow
|
|
493
|
+
intent = getattr(task_context, "intent", "create")
|
|
494
|
+
if intent == "create":
|
|
495
|
+
prompt += "\n\n" + INTENT_CREATE
|
|
496
|
+
elif intent == "modify":
|
|
497
|
+
prompt += "\n\n" + INTENT_MODIFY
|
|
498
|
+
elif intent == "debug":
|
|
499
|
+
prompt += "\n\n" + INTENT_DEBUG
|
|
500
|
+
elif intent == "explain":
|
|
501
|
+
prompt += "\n\n" + INTENT_EXPLAIN
|
|
502
|
+
# qa intent: no extra prompt needed (model answers directly)
|
|
503
|
+
|
|
366
504
|
memories = format_memories_for_prompt(workdir)
|
|
367
505
|
if memories:
|
|
368
506
|
prompt += f"\n\n{memories}"
|
|
@@ -302,16 +302,45 @@ TOOLS = [
|
|
|
302
302
|
# ── Tool implementations ──────────────────────────────────────────────
|
|
303
303
|
|
|
304
304
|
def _build_venv_env(workdir: Path) -> dict:
|
|
305
|
-
"""Build an environment dict with
|
|
305
|
+
"""Build an environment dict with virtualenv bin prepended to PATH."""
|
|
306
|
+
import sys
|
|
306
307
|
env = os.environ.copy()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
308
|
+
path_sep = ";" if os.name == "nt" else ":"
|
|
309
|
+
|
|
310
|
+
# 1. Check if workdir has its own .venv
|
|
311
|
+
venv_dir = workdir / ".venv"
|
|
312
|
+
# 2. Otherwise fallback to active virtualenv from sys.prefix
|
|
313
|
+
if not venv_dir.is_dir() and sys.prefix != getattr(sys, "base_prefix", sys.prefix):
|
|
314
|
+
venv_dir = Path(sys.prefix)
|
|
315
|
+
|
|
316
|
+
all_paths = []
|
|
317
|
+
if venv_dir.is_dir():
|
|
318
|
+
bin_dir = venv_dir / ("Scripts" if os.name == "nt" else "bin")
|
|
319
|
+
if not bin_dir.is_dir():
|
|
320
|
+
bin_dir = venv_dir / ("bin" if os.name == "nt" else "Scripts")
|
|
321
|
+
if bin_dir.is_dir():
|
|
322
|
+
all_paths.append(str(bin_dir))
|
|
323
|
+
env["VIRTUAL_ENV"] = str(venv_dir)
|
|
324
|
+
env.pop("PYTHONHOME", None)
|
|
325
|
+
|
|
326
|
+
# Include user tools (uv, local bin, cargo)
|
|
327
|
+
user_home = Path.home()
|
|
328
|
+
extra_paths = [
|
|
329
|
+
str(user_home / ".local" / "bin"),
|
|
330
|
+
str(user_home / ".cargo" / "bin"),
|
|
331
|
+
"/usr/local/bin",
|
|
332
|
+
"/usr/bin",
|
|
333
|
+
"/bin",
|
|
334
|
+
]
|
|
335
|
+
for p in extra_paths:
|
|
336
|
+
if p not in all_paths and os.path.exists(p):
|
|
337
|
+
all_paths.append(p)
|
|
338
|
+
|
|
339
|
+
current_path = env.get("PATH", "")
|
|
340
|
+
if current_path:
|
|
341
|
+
all_paths.append(current_path)
|
|
342
|
+
|
|
343
|
+
env["PATH"] = path_sep.join(all_paths)
|
|
315
344
|
return env
|
|
316
345
|
|
|
317
346
|
|
|
@@ -384,6 +413,15 @@ def _bash(workdir: Path, command: str) -> str:
|
|
|
384
413
|
import shutil
|
|
385
414
|
env = _build_venv_env(workdir)
|
|
386
415
|
|
|
416
|
+
# Auto-translate pip install -> uv pip install if pip binary is absent but uv is available
|
|
417
|
+
if "pip install" in command or "pip3 install" in command:
|
|
418
|
+
has_pip = shutil.which("pip", path=env.get("PATH")) or shutil.which("pip3", path=env.get("PATH"))
|
|
419
|
+
if not has_pip:
|
|
420
|
+
uv_bin = shutil.which("uv", path=env.get("PATH")) or shutil.which("uv")
|
|
421
|
+
if uv_bin:
|
|
422
|
+
command = re.sub(r"(?:[^\s;&|]*/)?(?:python3?|py)(?:\.exe)?\s+-m\s+pip3?\s+install\b", f"{uv_bin} pip install", command)
|
|
423
|
+
command = re.sub(r"(?:[^\s;&|]*/)?pip3?\s+install\b", f"{uv_bin} pip install", command)
|
|
424
|
+
|
|
387
425
|
# Cross-platform shell resolution
|
|
388
426
|
if shutil.which("bash"):
|
|
389
427
|
shell_cmd = ["bash", "-c", command]
|
|
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
|