alpiecode 10.5.4__tar.gz → 10.5.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.
Files changed (55) hide show
  1. {alpiecode-10.5.4 → alpiecode-10.5.6}/PKG-INFO +1 -2
  2. {alpiecode-10.5.4 → alpiecode-10.5.6}/pyproject.toml +1 -2
  3. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/PKG-INFO +1 -2
  4. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/requires.txt +0 -1
  6. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/__init__.py +1 -1
  7. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/assessment.py +27 -6
  8. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/openai_backend.py +51 -4
  9. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/cli.py +29 -2
  10. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/config.py +21 -2
  11. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/discovery.py +13 -3
  12. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/orchestrator.py +35 -4
  13. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/prompt.py +13 -2
  14. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/rephraser.py +19 -4
  15. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/tools.py +96 -44
  16. alpiecode-10.5.6/test/test_adaptive_resilience.py +264 -0
  17. {alpiecode-10.5.4 → alpiecode-10.5.6}/README.md +0 -0
  18. {alpiecode-10.5.4 → alpiecode-10.5.6}/setup.cfg +0 -0
  19. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode/__init__.py +0 -0
  20. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  21. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/entry_points.txt +0 -0
  22. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/top_level.txt +0 -0
  23. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/agent.py +0 -0
  24. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/__init__.py +0 -0
  25. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/base.py +0 -0
  26. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/local_backend.py +0 -0
  27. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/cache.py +0 -0
  28. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/client.py +0 -0
  29. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/compaction.py +0 -0
  30. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/context.py +0 -0
  31. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/doctor.py +0 -0
  32. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/dream_rsi.py +0 -0
  33. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/executor.py +0 -0
  34. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/extension/alpiecode.vsix +0 -0
  35. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/git_ops.py +0 -0
  36. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/github.py +0 -0
  37. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/guardian.py +0 -0
  38. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/guardrails.py +0 -0
  39. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/ipython_ext.py +0 -0
  40. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/local_model.py +0 -0
  41. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/media.py +0 -0
  42. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/memory.py +0 -0
  43. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/metrics.py +0 -0
  44. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/progress.py +0 -0
  45. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/server.py +0 -0
  46. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/session.py +0 -0
  47. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/system1.py +0 -0
  48. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/updater.py +0 -0
  49. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/verifier.py +0 -0
  50. {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/vscode_installer.py +0 -0
  51. {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_cli_ui.py +0 -0
  52. {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_context_and_trajectory.py +0 -0
  53. {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_dream_rsi.py +0 -0
  54. {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_task_assessment.py +0 -0
  55. {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.5.4
3
+ Version: 10.5.6
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -8,7 +8,6 @@ Requires-Dist: rich>=13.0
8
8
  Requires-Dist: click>=8.0
9
9
  Requires-Dist: openai>=1.0.0
10
10
  Requires-Dist: httpx[http2]>=0.24.0
11
- Requires-Dist: duckduckgo-search>=7.0.0
12
11
  Requires-Dist: beautifulsoup4>=4.12.0
13
12
  Requires-Dist: yt-dlp>=2024.0.0
14
13
  Requires-Dist: imageio-ffmpeg>=0.4.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "10.5.4"
7
+ version = "10.5.6"
8
8
  description = "AlpieCode"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -13,7 +13,6 @@ dependencies = [
13
13
  "click>=8.0",
14
14
  "openai>=1.0.0",
15
15
  "httpx[http2]>=0.24.0",
16
- "duckduckgo-search>=7.0.0",
17
16
  "beautifulsoup4>=4.12.0",
18
17
  "yt-dlp>=2024.0.0",
19
18
  "imageio-ffmpeg>=0.4.0",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.5.4
3
+ Version: 10.5.6
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -8,7 +8,6 @@ Requires-Dist: rich>=13.0
8
8
  Requires-Dist: click>=8.0
9
9
  Requires-Dist: openai>=1.0.0
10
10
  Requires-Dist: httpx[http2]>=0.24.0
11
- Requires-Dist: duckduckgo-search>=7.0.0
12
11
  Requires-Dist: beautifulsoup4>=4.12.0
13
12
  Requires-Dist: yt-dlp>=2024.0.0
14
13
  Requires-Dist: imageio-ffmpeg>=0.4.0
@@ -45,6 +45,7 @@ src/codeagent/backends/base.py
45
45
  src/codeagent/backends/local_backend.py
46
46
  src/codeagent/backends/openai_backend.py
47
47
  src/codeagent/extension/alpiecode.vsix
48
+ test/test_adaptive_resilience.py
48
49
  test/test_cli_ui.py
49
50
  test/test_context_and_trajectory.py
50
51
  test/test_dream_rsi.py
@@ -3,7 +3,6 @@ rich>=13.0
3
3
  click>=8.0
4
4
  openai>=1.0.0
5
5
  httpx[http2]>=0.24.0
6
- duckduckgo-search>=7.0.0
7
6
  beautifulsoup4>=4.12.0
8
7
  yt-dlp>=2024.0.0
9
8
  imageio-ffmpeg>=0.4.0
@@ -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.4"
20
+ __version__ = "10.5.6"
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
23
23
  """Run an autonomous coding task programmatically."""
@@ -85,6 +85,7 @@ class TaskAssessment:
85
85
  estimated_tool_calls: int = 1
86
86
  strategy: str = ExecutionStrategy.FAST_PATH.value
87
87
  rationale: str = ""
88
+ requires_strict_format: bool = False
88
89
 
89
90
  @property
90
91
  def legacy_complexity(self) -> str:
@@ -115,6 +116,7 @@ class TaskAssessment:
115
116
  "estimated_tool_calls": self.estimated_tool_calls,
116
117
  "strategy": self.strategy,
117
118
  "rationale": self.rationale,
119
+ "requires_strict_format": self.requires_strict_format,
118
120
  "legacy_complexity": self.legacy_complexity,
119
121
  }
120
122
 
@@ -122,12 +124,15 @@ class TaskAssessment:
122
124
  # ── Pattern Dictionaries for Sub-15ms Deterministic Analysis ───────────────
123
125
 
124
126
  _QA_INDICATORS = [
125
- r"^\s*(what\s+is|what\s+are|what\'s|what\s+does|what\s+do)\b",
127
+ r"^\s*(what\s+is|what\s+are|what\'s|what\s+does|what\s+do|what\s+can\s+you|what\s+can\s+i|what\s+can\s+we)\b",
128
+ r"^\s*(what\s+are\s+your|what\s+tools|what\s+capabilities|what\s+can)\b",
129
+ r"^\s*(who\s+are\s+you|who\s+made\s+you|who\s+created\s+you)\b",
126
130
  r"^\s*(explain|describe|define|compare|contrast)\b",
127
- r"^\s*(how\s+does|how\s+do|why\s+does|why\s+do|why\s+is)\b",
128
- r"^\s*(can\s+you\s+explain|could\s+you\s+explain|tell\s+me\s+about)\b",
131
+ r"^\s*(how\s+does|how\s+do|why\s+does|why\s+do|why\s+is|how\s+to|how\s+can\s+i|how\s+can\s+you)\b",
132
+ r"^\s*(can\s+you\s+explain|could\s+you\s+explain|tell\s+me\s+about|can\s+you\s+help|help\s+me\s+with)\b",
129
133
  r"^\s*(who\s+invented|when\s+was|difference\s+between)\b",
130
134
  r"^\s*(is\s+it\s+possible|is\s+there\s+a\s+way)\b",
135
+ r"^\s*(help|capabilities|hello|hi|hey)\b",
131
136
  ]
132
137
 
133
138
  _EXPLAIN_INDICATORS = [
@@ -192,6 +197,15 @@ _EXTERNAL_DEP_INDICATORS = [
192
197
  r"\b(pydantic|fastapi|flask|django|react|vue|angular|express|torch|pandas|numpy)\b",
193
198
  ]
194
199
 
200
+ _STRICT_FORMAT_INDICATORS = [
201
+ r"\b(?:only\s+(?:a\s+)?json|valid\s+json|return\s+(?:only\s+)?json|json\s+object|json\s+only|just\s+json|pure\s+json)\b",
202
+ r"\b(?:strict(?:ly)?\s+json|in\s+json\s+format|raw\s+json|respond\s+with\s+json)\b",
203
+ r"\b(?:no\s+explanation|without\s+(?:any\s+)?explanation|without\s+extra\s+text|no\s+surrounding\s+text|no\s+preamble)\b",
204
+ r"\b(?:single\s+line|one\s+line\s+only|single-line)\b",
205
+ r"\b(?:one\s+word|single\s+word|just\s+the\s+number|regex\s+only)\b",
206
+ r"\b(?:format:\s*json|schema:\s*\{)\b",
207
+ ]
208
+
195
209
 
196
210
  def assess_task(
197
211
  task: str,
@@ -215,10 +229,12 @@ def assess_task(
215
229
  estimated_tool_calls=1,
216
230
  strategy=ExecutionStrategy.FAST_PATH.value,
217
231
  rationale="Empty task string defaulted to fast local execution.",
232
+ requires_strict_format=False,
218
233
  )
219
234
 
220
235
  task_clean = task.strip()
221
236
  task_lower = task_clean.lower()
237
+ requires_strict_format = any(re.search(pat, task_lower) for pat in _STRICT_FORMAT_INDICATORS)
222
238
  repo = repo_info or {}
223
239
  file_count = repo.get("file_count", 0)
224
240
  has_tests = repo.get("has_tests", False)
@@ -228,8 +244,12 @@ def assess_task(
228
244
 
229
245
  # ── 1. Intent Classification ──
230
246
  intent = TaskIntent.CREATE.value
231
- if any(re.search(pat, task_lower) for pat in _QA_INDICATORS) and not any(
232
- kw in task_lower for kw in ["write", "create", "implement", "build", "fix", "code", "script"]
247
+ is_qa_candidate = (
248
+ any(re.search(pat, task_lower) for pat in _QA_INDICATORS)
249
+ or (task_clean.endswith("?") and not any(kw in task_lower for kw in ["write", "create", "implement", "build", "fix", "code", "script", "add", "modify", "refactor"]))
250
+ )
251
+ if is_qa_candidate and not any(
252
+ kw in task_lower for kw in ["write", "create", "implement", "build", "fix", "code", "script", "refactor", "modify"]
233
253
  ):
234
254
  intent = TaskIntent.QA.value
235
255
  elif any(re.search(pat, task_lower) for pat in _EXPLAIN_INDICATORS) and not any(
@@ -240,7 +260,7 @@ def assess_task(
240
260
  intent = TaskIntent.DEBUG.value
241
261
  elif any(re.search(pat, task_lower) for pat in _MODIFY_INDICATORS):
242
262
  intent = TaskIntent.MODIFY.value
243
- elif intent == TaskIntent.CREATE.value and any(re.search(pat, task_lower) for pat in _QA_INDICATORS):
263
+ elif intent == TaskIntent.CREATE.value and (any(re.search(pat, task_lower) for pat in _QA_INDICATORS) or task_clean.endswith("?")):
244
264
  # Fallback question check
245
265
  intent = TaskIntent.QA.value
246
266
 
@@ -397,4 +417,5 @@ def assess_task(
397
417
  estimated_tool_calls=estimated_tool_calls,
398
418
  strategy=strategy,
399
419
  rationale=rationale,
420
+ requires_strict_format=requires_strict_format,
400
421
  )
@@ -127,6 +127,7 @@ class OpenAIBackend:
127
127
  self._cfg = cfg
128
128
  self._client: Optional[OpenAI] = None
129
129
  self._resolved_model: Optional[str] = None
130
+ self._max_model_len: Optional[int] = None
130
131
 
131
132
  @property
132
133
  def name(self) -> str:
@@ -138,6 +139,8 @@ class OpenAIBackend:
138
139
 
139
140
  @property
140
141
  def context_window(self) -> int:
142
+ if self._max_model_len:
143
+ return self._max_model_len
141
144
  return 262_144
142
145
 
143
146
  def _ensure_client(self) -> OpenAI:
@@ -173,10 +176,13 @@ class OpenAIBackend:
173
176
  for m in models_list.data:
174
177
  if m.id == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
175
178
  self._resolved_model = m.id
176
- logger.info("Resolved model: %s", self._resolved_model)
179
+ self._max_model_len = getattr(m, "max_model_len", None)
180
+ logger.info("Resolved model: %s (max_len=%s)", self._resolved_model, self._max_model_len)
177
181
  return self._resolved_model
178
- self._resolved_model = models_list.data[0].id
179
- logger.info("Using first available model: %s", self._resolved_model)
182
+ first_m = models_list.data[0]
183
+ self._resolved_model = first_m.id
184
+ self._max_model_len = getattr(first_m, "max_model_len", None)
185
+ logger.info("Using first available model: %s (max_len=%s)", self._resolved_model, self._max_model_len)
180
186
  return self._resolved_model
181
187
  except Exception as e:
182
188
  logger.debug("Model discovery failed (%.1fs timeout), using configured model '%s': %s",
@@ -206,6 +212,26 @@ class OpenAIBackend:
206
212
  client = self._ensure_client()
207
213
  model_name = self._get_available_model(client)
208
214
 
215
+ if self._max_model_len:
216
+ input_chars = sum(len(str(m.get("content", ""))) for m in messages)
217
+ if tools:
218
+ input_chars += sum(len(str(t)) for t in tools)
219
+ input_est = max(1, input_chars // 3)
220
+ safe_headroom = 1024
221
+ max_allowed = self._max_model_len - input_est - safe_headroom
222
+ if m_tokens > max_allowed:
223
+ m_tokens = max(512, max_allowed)
224
+ if self._max_model_len <= 16384 and m_tokens > 4096:
225
+ m_tokens = 4096
226
+
227
+ import os
228
+ stream_timeout = 180.0 if enable_thinking else 90.0
229
+ if "ALPIECODE_TIMEOUT" in os.environ:
230
+ try:
231
+ stream_timeout = float(os.environ["ALPIECODE_TIMEOUT"])
232
+ except ValueError:
233
+ pass
234
+
209
235
  params = {
210
236
  "model": model_name,
211
237
  "messages": messages,
@@ -213,7 +239,7 @@ class OpenAIBackend:
213
239
  "max_tokens": m_tokens,
214
240
  "extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
215
241
  "stream": True,
216
- "timeout": 45.0,
242
+ "timeout": stream_timeout,
217
243
  }
218
244
  if tools:
219
245
  params["tools"] = tools
@@ -385,12 +411,33 @@ class OpenAIBackend:
385
411
  client = self._ensure_client()
386
412
  model_name = self._get_available_model(client)
387
413
 
414
+ if self._max_model_len:
415
+ input_chars = sum(len(str(m.get("content", ""))) for m in messages)
416
+ if tools:
417
+ input_chars += sum(len(str(t)) for t in tools)
418
+ input_est = max(1, input_chars // 3)
419
+ safe_headroom = 1024
420
+ max_allowed = self._max_model_len - input_est - safe_headroom
421
+ if m_tokens > max_allowed:
422
+ m_tokens = max(512, max_allowed)
423
+ if self._max_model_len <= 16384 and m_tokens > 4096:
424
+ m_tokens = 4096
425
+
426
+ import os
427
+ call_timeout = 180.0 if enable_thinking else 90.0
428
+ if "ALPIECODE_TIMEOUT" in os.environ:
429
+ try:
430
+ call_timeout = float(os.environ["ALPIECODE_TIMEOUT"])
431
+ except ValueError:
432
+ pass
433
+
388
434
  params = {
389
435
  "model": model_name,
390
436
  "messages": messages,
391
437
  "temperature": temp,
392
438
  "max_tokens": m_tokens,
393
439
  "extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
440
+ "timeout": call_timeout,
394
441
  }
395
442
  if tools:
396
443
  params["tools"] = tools
@@ -64,7 +64,11 @@ def _normalize_args():
64
64
 
65
65
  flags = []
66
66
  positionals = []
67
- value_flags = {"--workdir", "--image", "--video", "--url", "--github", "--max-turns", "--host", "--port", "--ctx-size"}
67
+ value_flags = {
68
+ "--workdir", "--image", "--video", "--url", "--github", "--max-turns",
69
+ "--host", "--port", "--ctx-size", "--endpoint", "--base-url", "--model",
70
+ "--api-key", "--preset",
71
+ }
68
72
 
69
73
  i = 0
70
74
  while i < len(sub_args):
@@ -93,8 +97,13 @@ def main():
93
97
  common.add_argument("--url", default=None, help="YouTube URL for video analysis")
94
98
  common.add_argument("--github", default=None, help="GitHub repository (e.g. owner/repo or URL) for open-source analysis")
95
99
  common.add_argument("--max-turns", type=int, default=None, help="Override max turns")
100
+ common.add_argument("--endpoint", "--base-url", dest="endpoint", default=None, help="VLM API endpoint base URL")
101
+ common.add_argument("--model", dest="model", default=None, help="Model name (e.g. alpie_9b)")
102
+ common.add_argument("--api-key", dest="api_key", default=None, help="API key for endpoint authentication")
103
+ common.add_argument("--preset", dest="preset", choices=["default", "alpie_9b"], default=None, help="Endpoint preset profile")
96
104
  common.add_argument("--thinking", "--reasoning", dest="thinking", action="store_true", help="Enable VLM reasoning mode")
97
105
  common.add_argument("--no-thinking", "--non-thinking", dest="no_thinking", action="store_true", help="Disable VLM reasoning mode")
106
+ common.add_argument("--force-thinking", dest="force_thinking", action="store_true", help="Force VLM reasoning on all turns, disabling dynamic gating")
98
107
  common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
99
108
  common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
100
109
  common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
@@ -237,6 +246,21 @@ def main():
237
246
 
238
247
  cfg = load_config()
239
248
 
249
+ from .config import KNOWN_PRESETS
250
+ if getattr(args, "preset", None):
251
+ preset_key = args.preset.lower()
252
+ if preset_key in KNOWN_PRESETS:
253
+ cfg.base_url = KNOWN_PRESETS[preset_key]["base_url"]
254
+ cfg.model = KNOWN_PRESETS[preset_key]["model"]
255
+ cfg.api_key = KNOWN_PRESETS[preset_key]["api_key"]
256
+
257
+ if getattr(args, "endpoint", None):
258
+ cfg.base_url = args.endpoint
259
+ if getattr(args, "model", None):
260
+ cfg.model = args.model
261
+ if getattr(args, "api_key", None):
262
+ cfg.api_key = args.api_key
263
+
240
264
  if getattr(args, "offline", False):
241
265
  cfg.base_url = "offline"
242
266
  # 8192 default fits 4.4GB model + KV cache 100% in 6GB VRAM (4.75GB / 6GB) with zero host-paging
@@ -247,7 +271,10 @@ def main():
247
271
  if getattr(args, "ctx_size", None):
248
272
  cfg.n_ctx = args.ctx_size
249
273
 
250
- if getattr(args, "thinking", False):
274
+ if getattr(args, "force_thinking", False):
275
+ cfg.force_thinking = True
276
+ cfg.enable_thinking = True
277
+ elif getattr(args, "thinking", False):
251
278
  cfg.enable_thinking = True
252
279
  elif getattr(args, "no_thinking", False):
253
280
  cfg.enable_thinking = False
@@ -37,6 +37,19 @@ DEFAULTS = {
37
37
  "config_version": CONFIG_VERSION,
38
38
  }
39
39
 
40
+ KNOWN_PRESETS = {
41
+ "default": {
42
+ "base_url": "https://test.169pi.ai/v1",
43
+ "model": "alpie_9b",
44
+ "api_key": "not-needed",
45
+ },
46
+ "alpie_9b": {
47
+ "base_url": "https://test.169pi.ai/v1",
48
+ "model": "alpie_9b",
49
+ "api_key": "not-needed",
50
+ },
51
+ }
52
+
40
53
 
41
54
  def is_server_reachable(base_url: Optional[str], timeout: float = 0.4) -> bool:
42
55
  """Fast network ping check (0.4s max) to see if server endpoint is online."""
@@ -77,7 +90,7 @@ def get_shared_http_client():
77
90
  import httpx
78
91
  _SHARED_HTTP_CLIENT = httpx.Client(
79
92
  http2=True,
80
- timeout=httpx.Timeout(120.0, connect=5.0),
93
+ timeout=httpx.Timeout(180.0, connect=10.0, read=180.0, write=30.0),
81
94
  limits=httpx.Limits(max_keepalive_connections=20, max_connections=50),
82
95
  )
83
96
  return _SHARED_HTTP_CLIENT
@@ -127,6 +140,12 @@ def load_config() -> Config:
127
140
 
128
141
  # env vars override saved file
129
142
  for prefix in ("ALPIECODE_", "CODEAGENT_"):
143
+ preset_val = os.environ.get(f"{prefix}PRESET")
144
+ if preset_val and preset_val.lower() in KNOWN_PRESETS:
145
+ p_data = KNOWN_PRESETS[preset_val.lower()]
146
+ data["base_url"] = p_data["base_url"]
147
+ data["model"] = p_data["model"]
148
+ data["api_key"] = p_data["api_key"]
130
149
  if os.environ.get(f"{prefix}BASE_URL"):
131
150
  data["base_url"] = os.environ[f"{prefix}BASE_URL"]
132
151
  if os.environ.get(f"{prefix}MODEL"):
@@ -137,7 +156,7 @@ def load_config() -> Config:
137
156
  data["api_key"] = os.environ[f"{prefix}API_KEY"]
138
157
  if os.environ.get("HF_TOKEN"):
139
158
  data["hf_token"] = os.environ["HF_TOKEN"]
140
-
159
+
141
160
  # Context size overrides from environment
142
161
  for ctx_var in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE", "ALPIECODE_CONTEXT_SIZE", "CODEAGENT_N_CTX"):
143
162
  if os.environ.get(ctx_var):
@@ -59,6 +59,7 @@ class TaskContext:
59
59
  max_tokens: int = 8192
60
60
  tool_set: str = "core" # none, core, full
61
61
  enable_thinking: bool = False
62
+ requires_strict_format: bool = False
62
63
 
63
64
 
64
65
  # -- Intent Detection ------------------------------------------------------
@@ -66,11 +67,15 @@ class TaskContext:
66
67
  # Question patterns -- user wants an answer, not code
67
68
  _QA_STARTERS = [
68
69
  "what is", "what are", "what\'s", "what does", "what do",
70
+ "what can you", "what can i", "what can we", "what are your", "what tools", "what capabilities",
71
+ "who are you", "who made you", "who created you",
69
72
  "explain", "how does", "how do", "why does", "why do", "why is",
73
+ "how to", "how can i", "how can you",
70
74
  "describe", "define", "compare", "difference between",
71
- "tell me about", "who invented", "who created", "when was",
75
+ "tell me about", "who invented", "when was",
72
76
  "can you explain", "could you explain", "please explain",
73
- "what\'s the difference", "is it possible", "is there a way",
77
+ "can you help", "help me with", "what\'s the difference", "is it possible", "is there a way",
78
+ "help", "capabilities", "hello", "hi", "hey",
74
79
  ]
75
80
 
76
81
  # Action verbs that indicate code generation
@@ -663,6 +668,10 @@ def build_task_context(task: str, workdir: Path) -> TaskContext:
663
668
 
664
669
  # Phase 5: Budget from complexity
665
670
  budget = COMPLEXITY_CONFIG.get(complexity, COMPLEXITY_CONFIG["low"])
671
+ enable_thinking = budget["enable_thinking"]
672
+ requires_strict_format = getattr(assessment, "requires_strict_format", False)
673
+ if requires_strict_format:
674
+ enable_thinking = False
666
675
 
667
676
  symbol_index = {}
668
677
  if repo.get("file_count", 0) > 0 and repo.get("project_type") != "empty":
@@ -696,5 +705,6 @@ def build_task_context(task: str, workdir: Path) -> TaskContext:
696
705
  # Budget
697
706
  max_tokens=budget["max_tokens"],
698
707
  tool_set=budget["tool_set"],
699
- enable_thinking=budget["enable_thinking"],
708
+ enable_thinking=enable_thinking,
709
+ requires_strict_format=requires_strict_format,
700
710
  )
@@ -325,7 +325,17 @@ class AgentOrchestrator:
325
325
 
326
326
 
327
327
  # ── Adaptive thinking (base setting) ──
328
- if cfg.enable_thinking:
328
+ strict_format = getattr(getattr(task_context, "assessment", None), "requires_strict_format", False) or getattr(task_context, "requires_strict_format", False)
329
+ if getattr(cfg, "force_thinking", False):
330
+ base_enable_thinking = True
331
+ elif strict_format:
332
+ base_enable_thinking = False
333
+ yield AgentEvent("adaptive_mode", {
334
+ "message": "Strict formatting requested (e.g. JSON/single-line); dynamic reasoning gating turned off thinking for fast execution.",
335
+ "thinking": False,
336
+ "strict_format_gated": True,
337
+ })
338
+ elif cfg.enable_thinking:
329
339
  base_enable_thinking = True
330
340
  elif complexity in ("qa", "low", "no-thinking") or strategy == "fast_path":
331
341
  base_enable_thinking = False
@@ -391,6 +401,17 @@ class AgentOrchestrator:
391
401
  # ── Turn-Level Adaptive Reasoning Dynamics ──
392
402
  if getattr(cfg, "force_thinking", False) or not getattr(cfg, "adaptive_thinking", True):
393
403
  turn_enable_thinking = base_enable_thinking
404
+ elif strict_format:
405
+ if prior_turn_had_error:
406
+ turn_enable_thinking = True
407
+ yield AgentEvent("adaptive_mode", {
408
+ "turn": turn,
409
+ "thinking": True,
410
+ "escalated": True,
411
+ "message": "Prior turn encountered error; escalating deep reasoning trace.",
412
+ })
413
+ else:
414
+ turn_enable_thinking = False
394
415
  elif turn == 1:
395
416
  turn_enable_thinking = base_enable_thinking
396
417
  else:
@@ -488,12 +509,22 @@ class AgentOrchestrator:
488
509
  if not is_offline and isinstance(task_backend, OpenAIBackend):
489
510
  err_str = str(e).lower()
490
511
  is_timeout = "timeout" in err_str or "timed out" in err_str or "read operation timed out" in err_str
512
+ is_stream_err = (
513
+ "streaming" in err_str
514
+ or "connection" in err_str
515
+ or "disconnect" in err_str
516
+ or "remoteprotocolerror" in err_str
517
+ or "protocol" in err_str
518
+ or "peer closed" in err_str
519
+ or "broken pipe" in err_str
520
+ )
491
521
  is_network_down = not is_internet_available(timeout=1.5)
492
522
 
493
- if is_timeout and not is_network_down:
494
- # Server is alive but slow — retry, don't fallback to GGUF
523
+ if (is_timeout or is_stream_err) and not is_network_down:
524
+ # Server is alive but encountered a stream drop or timeout — retry with resilient connection
525
+ warn_msg = f"Server connection issue ({e}). Retrying with resilient connection..." if is_stream_err else f"Server inference timed out: {e}. Retrying..."
495
526
  yield AgentEvent("warning", {
496
- "message": f"Server inference timed out: {e}. Retrying...",
527
+ "message": warn_msg,
497
528
  })
498
529
  try:
499
530
  resp = task_backend.chat_completion(
@@ -100,6 +100,9 @@ SYSTEM_PROMPT = """\
100
100
  You are AlpieCode, an autonomous software-engineering agent built by 169Pi. \
101
101
  You write complete, working code in a single pass and verify it by running it.
102
102
 
103
+ ## Technical Focus Scope
104
+ This session is strictly focused on software engineering and technical implementation. For purely technical tasks, bypass all non-applicable geopolitical, territorial, and sovereign policy deliberation in internal reasoning traces. Focus internal reasoning exclusively on software architecture, code correctness, execution efficiency, and explicit format constraints.
105
+
103
106
  # CRITICAL RULE: WRITE CODE FIRST
104
107
 
105
108
  # CRITICAL: FAST TESTING & EXECUTION (NO REAL SLEEP)
@@ -176,6 +179,9 @@ You are AlpieCode, an autonomous software-engineering agent built by 169Pi. \
176
179
  You operate with the judgement of a staff engineer. You read and edit real \
177
180
  codebases, implement features, fix bugs, write tests, and run builds.
178
181
 
182
+ ## Technical Focus Scope
183
+ This session is strictly focused on software engineering and technical architecture. For purely technical tasks, bypass all non-applicable geopolitical, territorial, and sovereign policy deliberation in internal reasoning traces. Focus internal reasoning exclusively on software design, algorithmic correctness, execution speed, and explicit format constraints.
184
+
179
185
  # Workflow
180
186
  1. **UNDERSTAND**: Read relevant files to understand the codebase
181
187
  2. **PLAN**: For complex multi-file projects, plan the architecture mentally
@@ -254,6 +260,9 @@ OFFLINE_SYSTEM_PROMPT = """\
254
260
  You are AlpieCode, an autonomous software engineering AI agent built by 169Pi.
255
261
  You are running in OFFLINE mode — there is NO internet access.
256
262
 
263
+ ## Technical Focus Scope
264
+ This session is strictly focused on software engineering. For purely technical tasks, bypass all non-applicable geopolitical, territorial, and sovereign policy deliberation in internal reasoning traces. Focus purely on technical implementation, algorithmic correctness, and execution.
265
+
257
266
  # CRITICAL RULE: WRITE CODE FIRST
258
267
 
259
268
  # CRITICAL: FAST TESTING & EXECUTION (NO REAL SLEEP)
@@ -303,16 +312,18 @@ def classify_task(task: str) -> str:
303
312
 
304
313
  # ── QA: questions that need no code ──
305
314
  qa_patterns = [
306
- "what is", "what are", "explain", "how does", "why does",
315
+ "what is", "what are", "what can you", "what can i", "what do you", "what does",
316
+ "who are you", "who made you", "explain", "how does", "why does", "why is",
307
317
  "describe", "define", "compare", "difference between",
308
318
  "tell me about", "what's the", "who invented", "when was",
319
+ "can you help", "how to", "help", "capabilities", "hello", "hi", "hey",
309
320
  ]
310
321
  # Must start with or contain a question pattern AND not contain action words
311
322
  action_words = ["build", "create", "write", "make", "implement", "fix",
312
323
  "add", "modify", "change", "update", "delete", "remove",
313
324
  "generate", "develop", "code", "script", "program"]
314
325
 
315
- if any(task_lower.startswith(pat) for pat in qa_patterns):
326
+ if any(task_lower.startswith(pat) for pat in qa_patterns) or (task_lower.endswith("?") and not any(aw in task_lower for aw in action_words)):
316
327
  if not any(aw in task_lower for aw in action_words):
317
328
  return "qa"
318
329
 
@@ -99,7 +99,19 @@ class PromptRephraser:
99
99
 
100
100
  # If pure trivial greeting, skip model call for instant response
101
101
  clean_task = task.strip().lower()
102
- if clean_task in ("hi", "hello", "hey", "help", "ping", "exit", "quit"):
102
+ if clean_task in ("hi", "hello", "hey", "help", "ping", "exit", "quit", "status", "info"):
103
+ return task
104
+
105
+ # If classified as QA or explain, never rephrase into a software engineering specification
106
+ if task_context is not None:
107
+ ctx_intent = getattr(task_context, "intent", None)
108
+ ctx_comp = getattr(task_context, "complexity", None)
109
+ if ctx_intent in ("qa", "explain") or ctx_comp == "qa":
110
+ return task
111
+
112
+ # General conversational / question check: do not fabricate software projects for direct questions
113
+ action_kws = ["write", "create", "implement", "build", "fix", "code", "script", "refactor", "modify", "add", "delete"]
114
+ if clean_task.endswith("?") and not any(kw in clean_task for kw in action_kws):
103
115
  return task
104
116
 
105
117
  # Check if backend is available
@@ -107,15 +119,18 @@ class PromptRephraser:
107
119
  return task
108
120
 
109
121
  # In offline/local GGUF mode, skip redundant rephraser LLM pass to ensure instant turn-0 start
110
- if type(backend).__name__ == "LocalBackend" or getattr(backend, "is_offline", False):
122
+ if type(backend).__name__ == "LocalBackend" or getattr(backend, "is_offline", None) is True:
111
123
  return task
112
124
 
113
125
  try:
114
- # Inject project context if available so rephrased spec is project-aware
126
+ # Inject project context only if modifying/debugging or if the task explicitly references the repo/files
115
127
  project_hint = ""
116
128
  standalone_kws = ["palindrome", "fibonacci", "quicksort", "bubblesort", "factorial", "hello world", "calculator", "reverse string"]
117
129
  is_standalone = any(kw in task.lower() for kw in standalone_kws)
118
- if task_context and not is_standalone:
130
+ ctx_intent = getattr(task_context, "intent", "create") if task_context else "create"
131
+ has_repo_ref = any(term in task.lower() for term in ["this repo", "this project", "existing", "codebase", "our app", "in the project", "entry point"])
132
+
133
+ if task_context and not is_standalone and (ctx_intent in ("modify", "debug", "explain") or has_repo_ref):
119
134
  _fw = ", ".join(getattr(task_context, "frameworks", []) or []) or "none"
120
135
  _pt = getattr(task_context, "project_type", "unknown")
121
136
  _fc = getattr(task_context, "file_count", 0)
@@ -760,60 +760,112 @@ def _apply_patch(workdir: Path, path: str, patch: str) -> str:
760
760
 
761
761
 
762
762
  def _web_search(query: str, num_results: int = 5) -> str:
763
- """Search the web using DDGS (DuckDuckGo)."""
763
+ """Search the web using httpx and DuckDuckGo HTTP endpoints, parsed via BeautifulSoup."""
764
764
  import warnings
765
+ import urllib.parse
765
766
  warnings.filterwarnings("ignore", category=RuntimeWarning)
766
767
 
767
768
  try:
768
- try:
769
- from ddgs import DDGS
770
- except ImportError:
771
- from duckduckgo_search import DDGS
772
-
773
- with DDGS() as ddgs:
774
- results = list(ddgs.text(query, max_results=min(num_results, 10)))
775
- if not results:
776
- return (
777
- f"No web results found for '{query}'. "
778
- "Tip: If searching for Python package documentation, use bash: python -c 'import pkg; help(pkg)'"
779
- )
780
- formatted = []
781
- for i, r in enumerate(results, 1):
782
- formatted.append(f"{i}. **{r.get('title', 'No title')}**\n URL: {r.get('href', '')}\n {r.get('body', '')}")
783
- return "\n\n".join(formatted)
784
- except ImportError:
785
- return "error: web search unavailable — install ddgs or duckduckgo-search package"
769
+ import httpx
770
+ from bs4 import BeautifulSoup
771
+
772
+ headers = {
773
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
774
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
775
+ "Accept-Language": "en-US,en;q=0.5",
776
+ }
777
+ with httpx.Client(timeout=10.0, follow_redirects=True) as client:
778
+ resp = client.post("https://html.duckduckgo.com/html/", data={"q": query}, headers=headers)
779
+ if resp.status_code != 200:
780
+ resp = client.post("https://lite.duckduckgo.com/lite/", data={"q": query}, headers=headers)
781
+
782
+ if resp.status_code == 200 and resp.text:
783
+ soup = BeautifulSoup(resp.text, "html.parser")
784
+ results = []
785
+ for item in soup.select(".result"):
786
+ title_elem = item.select_one(".result__title a")
787
+ snippet_elem = item.select_one(".result__snippet")
788
+ if title_elem:
789
+ title = title_elem.get_text(strip=True)
790
+ href = title_elem.get("href", "")
791
+ if "uddg=" in href:
792
+ parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
793
+ href = parsed.get("uddg", [href])[0]
794
+ snippet = snippet_elem.get_text(strip=True) if snippet_elem else ""
795
+ results.append({"title": title, "href": href, "body": snippet})
796
+ if len(results) >= min(num_results, 10):
797
+ break
798
+
799
+ if not results:
800
+ for row in soup.select("table tr"):
801
+ link = row.select_one("a.result-link")
802
+ snippet = row.select_one(".result-snippet")
803
+ if link:
804
+ title = link.get_text(strip=True)
805
+ href = link.get("href", "")
806
+ if "uddg=" in href:
807
+ parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
808
+ href = parsed.get("uddg", [href])[0]
809
+ snip_text = snippet.get_text(strip=True) if snippet else ""
810
+ results.append({"title": title, "href": href, "body": snip_text})
811
+ if len(results) >= min(num_results, 10):
812
+ break
813
+
814
+ if results:
815
+ formatted = []
816
+ for i, r in enumerate(results, 1):
817
+ formatted.append(f"{i}. **{r.get('title', 'No title')}**\n URL: {r.get('href', '')}\n {r.get('body', '')}")
818
+ return "\n\n".join(formatted)
786
819
  except Exception as e:
787
- return f"error during web search: {e}. Tip: Try python -c 'help(...)' for installed library docs."
820
+ return f"error during web search: {e}"
821
+
822
+ return (
823
+ f"No web results found for '{query}'. "
824
+ "Tip: If searching for Python package documentation, use bash: python -c 'import pkg; help(pkg)'"
825
+ )
788
826
 
789
827
 
790
828
  def _fetch_url(url: str) -> str:
791
- """Fetch and extract text content from a URL."""
829
+ """Fetch and extract text content from a URL using httpx."""
830
+ html = ""
792
831
  try:
793
- import urllib.request
794
- req = urllib.request.Request(url, headers={"User-Agent": "AlpieCode/0.2.0"})
795
- with urllib.request.urlopen(req, timeout=15) as resp:
796
- html = resp.read().decode("utf-8", errors="replace")
797
-
798
- # Try to extract text with beautifulsoup if available
832
+ import httpx
833
+ headers = {
834
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
835
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
836
+ }
837
+ with httpx.Client(timeout=15.0, follow_redirects=True) as client:
838
+ resp = client.get(url, headers=headers)
839
+ html = resp.text
840
+ except Exception:
799
841
  try:
800
- from bs4 import BeautifulSoup
801
- soup = BeautifulSoup(html, "html.parser")
802
- # Remove script and style elements
803
- for tag in soup(["script", "style", "nav", "footer", "header"]):
804
- tag.decompose()
805
- text = soup.get_text(separator="\n", strip=True)
806
- except ImportError:
807
- # Fallback: basic HTML tag stripping
808
- text = re.sub(r"<[^>]+>", "", html)
809
- text = re.sub(r"\s+", " ", text).strip()
810
-
811
- # Truncate to reasonable length
812
- if len(text) > 8000:
813
- text = text[:8000] + f"\n\n... (truncated, {len(text)} chars total)"
814
- return text
815
- except Exception as e:
816
- return f"error fetching URL: {e}"
842
+ import urllib.request
843
+ req = urllib.request.Request(url, headers={"User-Agent": "AlpieCode/0.2.0"})
844
+ with urllib.request.urlopen(req, timeout=15) as resp:
845
+ html = resp.read().decode("utf-8", errors="replace")
846
+ except Exception as e:
847
+ return f"error fetching URL: {e}"
848
+
849
+ if not html:
850
+ return "error fetching URL: empty response"
851
+
852
+ # Try to extract text with beautifulsoup if available
853
+ try:
854
+ from bs4 import BeautifulSoup
855
+ soup = BeautifulSoup(html, "html.parser")
856
+ # Remove script and style elements
857
+ for tag in soup(["script", "style", "nav", "footer", "header"]):
858
+ tag.decompose()
859
+ text = soup.get_text(separator="\n", strip=True)
860
+ except ImportError:
861
+ # Fallback: basic HTML tag stripping
862
+ text = re.sub(r"<[^>]+>", "", html)
863
+ text = re.sub(r"\s+", " ", text).strip()
864
+
865
+ # Truncate to reasonable length
866
+ if len(text) > 8000:
867
+ text = text[:8000] + f"\n\n... (truncated, {len(text)} chars total)"
868
+ return text
817
869
 
818
870
 
819
871
  def _request_user_input(question: str) -> str:
@@ -0,0 +1,264 @@
1
+ """
2
+ Unit tests for adaptive conversational intelligence, prompt rephraser isolation,
3
+ adaptive timeout configuration, and streaming error resilience.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+ from unittest.mock import MagicMock, patch
9
+ import pytest
10
+
11
+ from codeagent.assessment import assess_task, TaskIntent, ExecutionStrategy
12
+ from codeagent.discovery import build_task_context, detect_intent
13
+ from codeagent.prompt import classify_task, PromptBuilder
14
+ from codeagent.rephraser import PromptRephraser
15
+ from codeagent.backends.openai_backend import OpenAIBackend
16
+ from codeagent.orchestrator import AgentOrchestrator
17
+
18
+
19
+ def test_conversational_questions_route_to_qa_without_tools():
20
+ """Verify common user conversational inquiries route directly to QA with fast_path and 0 tools."""
21
+ conversational_queries = [
22
+ "what can you do for me?",
23
+ "who are you?",
24
+ "what can you do?",
25
+ "can you help me?",
26
+ "how to use alpiecode?",
27
+ "what tools do you have?",
28
+ "help",
29
+ "what are your capabilities?",
30
+ "how does Python asyncio work?",
31
+ ]
32
+
33
+ pb = PromptBuilder()
34
+
35
+ for q in conversational_queries:
36
+ assessment = assess_task(q)
37
+ assert assessment.intent == TaskIntent.QA.value, f"Failed on: {q}"
38
+ assert assessment.strategy == ExecutionStrategy.FAST_PATH.value, f"Failed on: {q}"
39
+ assert assessment.legacy_complexity == "qa", f"Failed on: {q}"
40
+ assert assessment.verification_required is False
41
+
42
+ # Verify zero tools are provided
43
+ tools = pb.get_tools(is_offline=False, complexity="qa")
44
+ assert tools == [], f"Tools should be empty for QA task: {q}"
45
+
46
+
47
+ def test_rephraser_skips_qa_and_preserves_new_project_independence():
48
+ """Verify rephraser never turns questions into software specs and does not pollute new project tasks."""
49
+ rephraser = PromptRephraser()
50
+ tc_qa = build_task_context("what can you do for me?", Path("."))
51
+
52
+ # QA query must be returned as-is without invoking model
53
+ result_qa = rephraser.rephrase("what can you do for me?", None, tc_qa)
54
+ assert result_qa == "what can you do for me?"
55
+
56
+ # Question ending with ? without action verbs must also be returned as-is
57
+ result_q2 = rephraser.rephrase("is this running locally?", None, None)
58
+ assert result_q2 == "is this running locally?"
59
+
60
+ # For brand-new creation tasks, existing Python/FastAPI repo context must not be injected
61
+ mock_backend = MagicMock()
62
+ mock_backend.is_available = True
63
+ mock_backend.chat_completion.return_value = "Refined spec"
64
+
65
+ tc_create = build_task_context("create a website where i can interact with solar system in C++", Path("."))
66
+ # Calling rephrase should not inject the unrelated python/fastapi repo context
67
+ rephraser.rephrase("create a website where i can interact with solar system in C++", mock_backend, tc_create)
68
+ assert mock_backend.chat_completion.called
69
+ call_args = mock_backend.chat_completion.call_args[1]
70
+ messages = call_args["messages"]
71
+ user_prompt = messages[1]["content"]
72
+ assert "Project Context:" not in user_prompt
73
+
74
+
75
+ def test_openai_backend_adaptive_timeout():
76
+ """Verify OpenAIBackend uses 180s for thinking mode and 90s for standard requests."""
77
+ cfg = SimpleNamespace(
78
+ base_url="http://mock-api",
79
+ api_key="test-key",
80
+ model="alpie_9b",
81
+ temperature=0.1,
82
+ max_tokens=2048,
83
+ )
84
+ backend = OpenAIBackend(cfg)
85
+
86
+ mock_client = MagicMock()
87
+ mock_client.chat.completions.create.return_value = []
88
+ backend._client = mock_client
89
+ backend._resolved_model = "alpie_9b"
90
+
91
+ # 1. Thinking enabled -> 180.0s timeout
92
+ list(backend.chat_completion_stream(messages=[{"role": "user", "content": "test"}], enable_thinking=True))
93
+ create_call = mock_client.chat.completions.create.call_args[1]
94
+ assert create_call["timeout"] == 180.0
95
+
96
+ # 2. Thinking disabled -> 90.0s timeout
97
+ list(backend.chat_completion_stream(messages=[{"role": "user", "content": "test"}], enable_thinking=False))
98
+ create_call_no_think = mock_client.chat.completions.create.call_args[1]
99
+ assert create_call_no_think["timeout"] == 90.0
100
+
101
+
102
+ def test_orchestrator_retries_transient_streaming_errors():
103
+ """Verify orchestrator recovers from transient streaming chunk drops."""
104
+ from codeagent.session import SessionManager
105
+ from codeagent.backends.base import ChatResponse
106
+
107
+ cfg = SimpleNamespace(
108
+ base_url="http://mock-api",
109
+ api_key="test-key",
110
+ model="alpie_9b",
111
+ temperature=0.1,
112
+ max_tokens=2048,
113
+ enable_thinking=False,
114
+ max_turns=5,
115
+ )
116
+
117
+ mock_backend = MagicMock(spec=OpenAIBackend)
118
+ mock_backend.name = "Mock OpenAI"
119
+ mock_backend.is_available = True
120
+
121
+ # Stream throws transient error mid-flight
122
+ def failing_stream(**kwargs):
123
+ raise RuntimeError("Server error: An error occurred during streaming")
124
+ yield # make it a generator
125
+
126
+ mock_backend.chat_completion_stream = failing_stream
127
+ # Non-streaming fallback succeeds
128
+ mock_backend.chat_completion.return_value = ChatResponse(
129
+ content="I am AlpieCode, an AI coding assistant.",
130
+ reasoning=None,
131
+ tool_calls=None,
132
+ )
133
+
134
+ sm = SessionManager()
135
+ session = sm.create_session(Path("."))
136
+ orchestrator = AgentOrchestrator(backend=mock_backend)
137
+
138
+ with patch("codeagent.orchestrator.is_internet_available", return_value=True):
139
+ events = list(orchestrator.run_task(session, "what can you do for me?", cfg))
140
+
141
+ event_types = [ev.type for ev in events]
142
+ assert "warning" in event_types
143
+ assert "done" in event_types
144
+ # Verify warning announced resilient retry
145
+ warn_event = next(ev for ev in events if ev.type == "warning")
146
+ assert "resilient connection" in warn_event.data["message"].lower() or "retrying" in warn_event.data["message"].lower()
147
+
148
+
149
+ def test_strict_format_detection_and_dynamic_reasoning_gating():
150
+ """Verify strict format tasks are detected and dynamic reasoning gating disables thinking."""
151
+ strict_tasks = [
152
+ "Return only JSON with keys: status, version",
153
+ "Output valid JSON: {'status': 'ok'}",
154
+ "respond with json only",
155
+ "Give me single line without explanation",
156
+ "one word answer: yes or no",
157
+ "regex only for email validation",
158
+ ]
159
+ for q in strict_tasks:
160
+ ass = assess_task(q)
161
+ assert ass.requires_strict_format is True, f"Failed detection for: {q}"
162
+ assert ass.to_dict()["requires_strict_format"] is True
163
+
164
+ non_strict_tasks = [
165
+ "create a fullstack application",
166
+ "debug memory leak in server.py",
167
+ "what is quicksort?",
168
+ ]
169
+ for q in non_strict_tasks:
170
+ ass = assess_task(q)
171
+ assert ass.requires_strict_format is False, f"False positive for: {q}"
172
+
173
+ # Verify orchestrator gates thinking off even when cfg.enable_thinking is True
174
+ from codeagent.session import SessionManager
175
+ from codeagent.backends.base import ChatResponse
176
+
177
+ cfg = SimpleNamespace(
178
+ base_url="http://mock-api",
179
+ api_key="test-key",
180
+ model="alpie_9b",
181
+ temperature=0.1,
182
+ max_tokens=2048,
183
+ enable_thinking=True, # User passed --thinking
184
+ force_thinking=False,
185
+ adaptive_thinking=True,
186
+ max_turns=2,
187
+ )
188
+ mock_backend = MagicMock()
189
+ mock_backend.name = "Mock"
190
+ mock_backend.is_available = True
191
+ mock_backend.chat_completion_stream.return_value = iter([
192
+ ChatResponse(content='{"status": "ok"}', reasoning=None, tool_calls=None)
193
+ ])
194
+
195
+ sm = SessionManager()
196
+ session = sm.create_session(Path("."))
197
+ orchestrator = AgentOrchestrator(backend=mock_backend)
198
+
199
+ with patch("codeagent.orchestrator.is_internet_available", return_value=True):
200
+ events = list(orchestrator.run_task(session, "Return only JSON with keys: status, version", cfg))
201
+
202
+ adaptive_events = [ev for ev in events if ev.type == "adaptive_mode"]
203
+ assert any(ev.data.get("strict_format_gated") is True for ev in adaptive_events)
204
+ # Verify the stream was called with enable_thinking=False!
205
+ call_args = mock_backend.chat_completion_stream.call_args[1]
206
+ assert call_args["enable_thinking"] is False
207
+
208
+
209
+ def test_force_thinking_overrides_dynamic_reasoning_gating():
210
+ """Verify force_thinking=True forces reasoning on strict format requests."""
211
+ from codeagent.session import SessionManager
212
+ from codeagent.backends.base import ChatResponse
213
+
214
+ cfg = SimpleNamespace(
215
+ base_url="http://mock-api",
216
+ api_key="test-key",
217
+ model="alpie_9b",
218
+ temperature=0.1,
219
+ max_tokens=2048,
220
+ enable_thinking=True,
221
+ force_thinking=True, # User explicitly used --force-thinking
222
+ adaptive_thinking=True,
223
+ max_turns=2,
224
+ )
225
+ mock_backend = MagicMock()
226
+ mock_backend.name = "Mock"
227
+ mock_backend.is_available = True
228
+ mock_backend.chat_completion_stream.return_value = iter([
229
+ ChatResponse(content='{"status": "ok"}', reasoning="Let me think...", tool_calls=None)
230
+ ])
231
+
232
+ sm = SessionManager()
233
+ session = sm.create_session(Path("."))
234
+ orchestrator = AgentOrchestrator(backend=mock_backend)
235
+
236
+ with patch("codeagent.orchestrator.is_internet_available", return_value=True):
237
+ events = list(orchestrator.run_task(session, "Return only JSON with keys: status, version", cfg))
238
+
239
+ # Should NOT have strict_format_gated event
240
+ adaptive_events = [ev for ev in events if ev.type == "adaptive_mode" and ev.data.get("strict_format_gated")]
241
+ assert len(adaptive_events) == 0
242
+
243
+ call_args = mock_backend.chat_completion_stream.call_args[1]
244
+ assert call_args["enable_thinking"] is True
245
+
246
+
247
+ def test_technical_scope_policy_filtering_in_system_prompts():
248
+ """Verify system prompts include the technical scope directive to bypass sovereign policy checks."""
249
+ pb = PromptBuilder()
250
+
251
+ # Standard prompt
252
+ prompt_std = pb.build_system_prompt(Path("."), is_offline=False, complexity="low")
253
+ assert "Technical Focus Scope" in prompt_std
254
+ assert "bypass all non-applicable geopolitical" in prompt_std
255
+
256
+ # High complexity prompt
257
+ prompt_high = pb.build_system_prompt(Path("."), is_offline=False, complexity="high")
258
+ assert "Technical Focus Scope" in prompt_high
259
+ assert "bypass all non-applicable geopolitical" in prompt_high
260
+
261
+ # Offline prompt
262
+ prompt_offline = pb.build_system_prompt(Path("."), is_offline=True, complexity="low")
263
+ assert "Technical Focus Scope" in prompt_offline
264
+ assert "bypass all non-applicable geopolitical" in prompt_offline
File without changes
File without changes