alpiecode 7.0.7__tar.gz → 8.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {alpiecode-7.0.7 → alpiecode-8.0.1}/PKG-INFO +1 -1
  2. {alpiecode-7.0.7 → alpiecode-8.0.1}/pyproject.toml +1 -1
  3. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. alpiecode-8.0.1/src/codeagent/backends/openai_backend.py +319 -0
  5. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/extension/alpiecode.vsix +0 -0
  6. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/orchestrator.py +107 -8
  7. alpiecode-7.0.7/src/codeagent/backends/openai_backend.py +0 -152
  8. {alpiecode-7.0.7 → alpiecode-8.0.1}/README.md +0 -0
  9. {alpiecode-7.0.7 → alpiecode-8.0.1}/setup.cfg +0 -0
  10. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode/__init__.py +0 -0
  11. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/SOURCES.txt +0 -0
  12. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  13. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  14. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/requires.txt +0 -0
  15. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  16. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/__init__.py +0 -0
  17. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/agent.py +0 -0
  18. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/backends/__init__.py +0 -0
  19. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/backends/base.py +0 -0
  20. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/backends/local_backend.py +0 -0
  21. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/cache.py +0 -0
  22. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/cli.py +0 -0
  23. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/client.py +0 -0
  24. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/compaction.py +0 -0
  25. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/config.py +0 -0
  26. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/context.py +0 -0
  27. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/discovery.py +0 -0
  28. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/doctor.py +0 -0
  29. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/executor.py +0 -0
  30. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/git_ops.py +0 -0
  31. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/github.py +0 -0
  32. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/guardian.py +0 -0
  33. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/ipython_ext.py +0 -0
  34. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/local_model.py +0 -0
  35. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/media.py +0 -0
  36. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/memory.py +0 -0
  37. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/progress.py +0 -0
  38. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/prompt.py +0 -0
  39. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/rephraser.py +0 -0
  40. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/server.py +0 -0
  41. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/session.py +0 -0
  42. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/tools.py +0 -0
  43. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/updater.py +0 -0
  44. {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/vscode_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 7.0.7
3
+ Version: 8.0.1
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "7.0.7"
7
+ version = "8.0.1"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 7.0.7
3
+ Version: 8.0.1
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -0,0 +1,319 @@
1
+ """
2
+ OpenAI-compatible API inference backend with smart model resolution.
3
+ """
4
+
5
+ from typing import Any, List, Optional
6
+ from openai import OpenAI, NotFoundError
7
+
8
+ from ..config import Config, get_shared_http_client, is_server_reachable
9
+ from .base import ChatResponse, ToolCall
10
+
11
+
12
+ class OpenAIBackend:
13
+ """Backend for remote OpenAI-compatible servers (vLLM, Ollama, etc.)."""
14
+
15
+ def __init__(self, cfg: Config):
16
+ self._cfg = cfg
17
+ self._client: Optional[OpenAI] = None
18
+ self._resolved_model: Optional[str] = None
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return f"Online API ({self._cfg.base_url})"
23
+
24
+ @property
25
+ def is_available(self) -> bool:
26
+ return is_server_reachable(self._cfg.base_url)
27
+
28
+ @property
29
+ def context_window(self) -> int:
30
+ return 262_144
31
+
32
+ def _ensure_client(self) -> OpenAI:
33
+ if self._client is None:
34
+ self._client = OpenAI(
35
+ base_url=self._cfg.base_url,
36
+ api_key=self._cfg.api_key or "not-needed",
37
+ http_client=get_shared_http_client(),
38
+ )
39
+ return self._client
40
+
41
+ def _get_available_model(self, client: OpenAI) -> str:
42
+ """Auto-discover the served model name from /v1/models."""
43
+ if self._resolved_model:
44
+ return self._resolved_model
45
+ try:
46
+ models_list = client.models.list()
47
+ if models_list.data:
48
+ for m in models_list.data:
49
+ if m.id == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
50
+ self._resolved_model = m.id
51
+ return self._resolved_model
52
+ self._resolved_model = models_list.data[0].id
53
+ return self._resolved_model
54
+ except Exception:
55
+ pass
56
+ return self._cfg.model
57
+
58
+ def chat_completion_stream(
59
+ self,
60
+ messages: List[dict],
61
+ tools: Optional[List[dict]] = None,
62
+ temperature: Optional[float] = None,
63
+ max_tokens: Optional[int] = None,
64
+ enable_thinking: bool = True,
65
+ ):
66
+ """
67
+ True real-time SSE streaming completion yielding progressive thinking deltas,
68
+ duration, content tokens, and final ChatResponse.
69
+ """
70
+ import time
71
+ import json
72
+ start_time = time.time()
73
+ temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.1)
74
+ m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 8192)
75
+
76
+ client = self._ensure_client()
77
+ model_name = self._get_available_model(client)
78
+
79
+ params = {
80
+ "model": model_name,
81
+ "messages": messages,
82
+ "temperature": temp,
83
+ "max_tokens": m_tokens,
84
+ "extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
85
+ "stream": True,
86
+ }
87
+ if tools:
88
+ params["tools"] = tools
89
+ params["tool_choice"] = "auto"
90
+
91
+ try:
92
+ response = client.chat.completions.create(**params)
93
+ except NotFoundError:
94
+ self._resolved_model = None
95
+ model_name = self._get_available_model(client)
96
+ params["model"] = model_name
97
+ response = client.chat.completions.create(**params)
98
+
99
+ in_think = enable_thinking
100
+ think_started = False
101
+ full_content = []
102
+ full_reasoning = []
103
+ tool_calls_dict = {}
104
+
105
+ for chunk in response:
106
+ if not chunk.choices:
107
+ continue
108
+ delta = chunk.choices[0].delta
109
+ if not delta:
110
+ continue
111
+
112
+ # Tool call deltas
113
+ if delta.tool_calls:
114
+ for tc in delta.tool_calls:
115
+ idx = tc.index
116
+ if idx not in tool_calls_dict:
117
+ tool_calls_dict[idx] = {
118
+ "id": tc.id or f"call_{idx}",
119
+ "name": tc.function.name if tc.function and tc.function.name else "",
120
+ "arguments": ""
121
+ }
122
+ else:
123
+ if tc.id:
124
+ tool_calls_dict[idx]["id"] = tc.id
125
+ if tc.function and tc.function.name:
126
+ tool_calls_dict[idx]["name"] += tc.function.name
127
+ if tc.function and tc.function.arguments:
128
+ tool_calls_dict[idx]["arguments"] += tc.function.arguments
129
+
130
+ # Explicit reasoning field from some models
131
+ explicit_reasoning = getattr(delta, "reasoning", None) or getattr(delta, "reasoning_content", None)
132
+ if explicit_reasoning:
133
+ if not think_started:
134
+ think_started = True
135
+ yield ("thinking_start", {"time": start_time})
136
+ full_reasoning.append(explicit_reasoning)
137
+ yield ("thinking_delta", {"delta": explicit_reasoning})
138
+
139
+ # Content deltas (contains <think>...</think> on vLLM/Qwen models)
140
+ if delta.content:
141
+ text = delta.content
142
+ if in_think:
143
+ if not think_started:
144
+ think_started = True
145
+ yield ("thinking_start", {"time": start_time})
146
+
147
+ if "</think>" in text:
148
+ before, after = text.split("</think>", 1)
149
+ if before:
150
+ full_reasoning.append(before)
151
+ yield ("thinking_delta", {"delta": before})
152
+ in_think = False
153
+ elapsed = time.time() - start_time
154
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
155
+ if after.strip():
156
+ full_content.append(after)
157
+ yield ("token", {"delta": after})
158
+ elif "DONE:" in text:
159
+ # Model output DONE: directly inside thinking without </think> tag
160
+ before, after = text.split("DONE:", 1)
161
+ if before:
162
+ full_reasoning.append(before)
163
+ yield ("thinking_delta", {"delta": before})
164
+ in_think = False
165
+ elapsed = time.time() - start_time
166
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
167
+ done_chunk = "DONE:" + after
168
+ full_content.append(done_chunk)
169
+ yield ("token", {"delta": done_chunk})
170
+ else:
171
+ clean_text = text.replace("<think>", "")
172
+ full_reasoning.append(clean_text)
173
+ yield ("thinking_delta", {"delta": clean_text})
174
+ else:
175
+ full_content.append(text)
176
+ yield ("token", {"delta": text})
177
+
178
+ # Close thinking if stream ended while still in think block
179
+ if in_think and think_started:
180
+ elapsed = time.time() - start_time
181
+ yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
182
+
183
+ tool_calls = None
184
+ if tool_calls_dict:
185
+ tool_calls = []
186
+ for idx in sorted(tool_calls_dict.keys()):
187
+ item = tool_calls_dict[idx]
188
+ try:
189
+ args = json.loads(item["arguments"])
190
+ except Exception:
191
+ args = {}
192
+ tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
193
+
194
+ final_content_str = "".join(full_content).strip()
195
+ final_reasoning_str = "".join(full_reasoning).strip()
196
+
197
+ # CRITICAL RECOVERY: If content is empty but reasoning has text,
198
+ # extract any DONE: or answer that was mistakenly trapped in reasoning!
199
+ if not final_content_str and final_reasoning_str:
200
+ if "DONE:" in final_reasoning_str:
201
+ idx = final_reasoning_str.find("DONE:")
202
+ final_content_str = final_reasoning_str[idx:].strip()
203
+ final_reasoning_str = final_reasoning_str[:idx].strip()
204
+ yield ("token", {"delta": final_content_str})
205
+ elif "</think>" in final_reasoning_str:
206
+ parts = final_reasoning_str.split("</think>", 1)
207
+ final_reasoning_str = parts[0].strip()
208
+ final_content_str = parts[1].strip()
209
+ yield ("token", {"delta": final_content_str})
210
+ elif any(m in final_reasoning_str for m in ["The codebase is complete", "All JavaScript files", "I have implemented", "Verified the", "All files"]):
211
+ final_content_str = final_reasoning_str
212
+ final_reasoning_str = ""
213
+ yield ("token", {"delta": final_content_str})
214
+
215
+ final_content = final_content_str
216
+ final_reasoning = final_reasoning_str if final_reasoning_str else None
217
+
218
+ yield ("done", ChatResponse(
219
+ content=final_content,
220
+ reasoning=final_reasoning,
221
+ tool_calls=tool_calls,
222
+ raw=None
223
+ ))
224
+
225
+ def chat_completion(
226
+ self,
227
+ messages: List[dict],
228
+ tools: Optional[List[dict]] = None,
229
+ temperature: Optional[float] = None,
230
+ max_tokens: Optional[int] = None,
231
+ enable_thinking: bool = True,
232
+ ) -> ChatResponse:
233
+ temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.1)
234
+ m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 8192)
235
+
236
+ client = self._ensure_client()
237
+ model_name = self._get_available_model(client)
238
+
239
+ params = {
240
+ "model": model_name,
241
+ "messages": messages,
242
+ "temperature": temp,
243
+ "max_tokens": m_tokens,
244
+ "extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
245
+ }
246
+ if tools:
247
+ params["tools"] = tools
248
+ params["tool_choice"] = "auto"
249
+
250
+ try:
251
+ resp = client.chat.completions.create(**params)
252
+ except NotFoundError:
253
+ # Model name mismatch -> re-discover model
254
+ self._resolved_model = None
255
+ model_name = self._get_available_model(client)
256
+ params["model"] = model_name
257
+ resp = client.chat.completions.create(**params)
258
+
259
+ msg = resp.choices[0].message
260
+
261
+ tool_calls = None
262
+ if msg.tool_calls:
263
+ import json
264
+ tool_calls = []
265
+ for tc in msg.tool_calls:
266
+ try:
267
+ args = json.loads(tc.function.arguments or "{}")
268
+ except (json.JSONDecodeError, TypeError):
269
+ args = {}
270
+ if not isinstance(args, dict):
271
+ args = {}
272
+ tool_calls.append(
273
+ ToolCall(
274
+ id=tc.id,
275
+ name=tc.function.name,
276
+ arguments=args,
277
+ )
278
+ )
279
+
280
+ reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
281
+ content = msg.content or ""
282
+
283
+ # Extract embedded thinking/reasoning if not provided in separate fields
284
+ if not reasoning and content:
285
+ import re
286
+ if "</think>" in content:
287
+ # Handled when model starts thinking implicitly or with <think>
288
+ parts = content.split("</think>", 1)
289
+ reasoning = parts[0].replace("<think>", "").strip()
290
+ content = parts[1].strip()
291
+ elif "<think>" in content:
292
+ think_match = re.search(r"<think>(.*?)</think>", content, flags=re.DOTALL)
293
+ if think_match:
294
+ reasoning = think_match.group(1).strip()
295
+ content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
296
+ elif content.strip().lower().startswith("thinking process:") or "\nthinking process:" in content.lower():
297
+ if tool_calls:
298
+ reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
299
+ content = ""
300
+ else:
301
+ code_match = re.search(r"\n(?=```|Here is|Below is|I will|The following|DONE:|#!/usr|/\*|package\s|import\s[a-z]|from\s[a-z])", content, flags=re.IGNORECASE)
302
+ if code_match:
303
+ think_part = content[:code_match.start()]
304
+ content_part = content[code_match.start():]
305
+ reasoning = re.sub(r"(?i)^thinking process:\s*", "", think_part).strip()
306
+ content = content_part.strip()
307
+ else:
308
+ reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
309
+ content = ""
310
+
311
+ return ChatResponse(
312
+ content=content,
313
+ reasoning=reasoning,
314
+ tool_calls=tool_calls,
315
+ raw=resp,
316
+ )
317
+
318
+ def shutdown(self) -> None:
319
+ self._client = None
@@ -1,3 +1,37 @@
1
+ def generate_walkthrough_markdown(task: str, summary: str, files: list, commands: list) -> str:
2
+ """Generate Antigravity-style Walkthrough markdown document."""
3
+ lines = [
4
+ f"# Walkthrough: {task}",
5
+ "",
6
+ "## Overview",
7
+ summary.strip() if summary else "All requested changes and verification steps have been completed.",
8
+ "",
9
+ "## Changes Made",
10
+ ]
11
+ if files:
12
+ lines.append("### Modified / Created Files")
13
+ for f in files:
14
+ lines.append(f"- `{f}`")
15
+ lines.append("")
16
+ else:
17
+ lines.append("No files were modified during this session.")
18
+ lines.append("")
19
+
20
+ if commands:
21
+ lines.append("## Verification Results")
22
+ for c in commands:
23
+ cmd = c.get("command") or c.get("cmd") or ""
24
+ code = c.get("exit_code", 0)
25
+ status = "✓ Passed" if code == 0 else f"✗ Failed (exit code {code})"
26
+ lines.append(f"- `{cmd}` — **{status}**")
27
+ lines.append("")
28
+
29
+ lines.append("## How to Run / Verify")
30
+ lines.append("Review the files above or run your test/build commands to verify project execution.")
31
+ lines.append("")
32
+ return "\n".join(lines)
33
+
34
+
1
35
  """
2
36
  Agent orchestrator for AlpieCode.
3
37
 
@@ -81,6 +115,10 @@ class AgentOrchestrator:
81
115
  # Safety ceiling: hard emergency brake (should never be hit naturally)
82
116
  safety_ceiling = cfg.max_turns if cfg.max_turns != 200 else 200
83
117
 
118
+ # Track touched files and executed commands for Antigravity Walkthrough
119
+ session_touched_files = set()
120
+ session_executed_commands = []
121
+
84
122
  # ── Response cache check ──
85
123
  is_cacheable = not any([image_path, video_path, url, github_repo])
86
124
  if is_cacheable:
@@ -211,13 +249,27 @@ class AgentOrchestrator:
211
249
  else:
212
250
  max_tokens = 2048 if is_offline else effective_max_tokens
213
251
 
214
- resp = self.backend.chat_completion(
215
- messages=session.context.messages,
216
- tools=active_tools if active_tools else None,
217
- temperature=cfg.temperature,
218
- max_tokens=max_tokens,
219
- enable_thinking=enable_thinking,
220
- )
252
+ if hasattr(self.backend, "chat_completion_stream") and not is_offline:
253
+ resp = None
254
+ for event_type, data in self.backend.chat_completion_stream(
255
+ messages=session.context.messages,
256
+ tools=active_tools if active_tools else None,
257
+ temperature=cfg.temperature,
258
+ max_tokens=max_tokens,
259
+ enable_thinking=enable_thinking,
260
+ ):
261
+ if event_type == "done":
262
+ resp = data
263
+ else:
264
+ yield AgentEvent(event_type, data)
265
+ else:
266
+ resp = self.backend.chat_completion(
267
+ messages=session.context.messages,
268
+ tools=active_tools if active_tools else None,
269
+ temperature=cfg.temperature,
270
+ max_tokens=max_tokens,
271
+ enable_thinking=enable_thinking,
272
+ )
221
273
  except Exception as e:
222
274
  # Online error -> fallback to local
223
275
  if not is_offline and isinstance(self.backend, OpenAIBackend):
@@ -241,11 +293,25 @@ class AgentOrchestrator:
241
293
  yield AgentEvent("error", {"error": str(e)})
242
294
  return
243
295
 
244
- if resp.reasoning:
296
+ if resp.reasoning and not (hasattr(self.backend, "chat_completion_stream") and not is_offline):
245
297
  yield AgentEvent("thinking", {"content": resp.reasoning})
246
298
 
247
299
  session.context.add_assistant_response(resp)
248
300
 
301
+ # Normalize content and reasoning: rescue any DONE: or answer trapped in reasoning
302
+ if (not resp.content or not resp.content.strip()) and resp.reasoning:
303
+ if "DONE:" in resp.reasoning.upper():
304
+ idx = resp.reasoning.upper().find("DONE:")
305
+ resp.content = resp.reasoning[idx:].strip()
306
+ resp.reasoning = resp.reasoning[:idx].strip() or None
307
+ elif "</think>" in resp.reasoning:
308
+ parts = resp.reasoning.split("</think>", 1)
309
+ resp.reasoning = parts[0].strip() or None
310
+ resp.content = parts[1].strip()
311
+ elif any(m in resp.reasoning for m in ["The codebase is complete", "All JavaScript files", "I have implemented", "Verified the"]):
312
+ resp.content = resp.reasoning.strip()
313
+ resp.reasoning = None
314
+
249
315
  # ── DONE detection in assistant content ──
250
316
  if resp.content and "DONE:" in resp.content.upper():
251
317
  # Model said DONE — finish even if there are tool calls
@@ -266,6 +332,28 @@ class AgentOrchestrator:
266
332
  yield AgentEvent("message", {"content": resp.content})
267
333
  extract_and_save_memories(session.workdir, session.context.messages)
268
334
 
335
+ # Generate and write real walkthrough.md file to the project workspace
336
+ from pathlib import Path as _Path
337
+ try:
338
+ w_path = _Path(session.workdir) / "walkthrough.md"
339
+ w_content = generate_walkthrough_markdown(
340
+ task=task,
341
+ summary=resp.content,
342
+ files=sorted(list(session_touched_files)),
343
+ commands=session_executed_commands,
344
+ )
345
+ w_path.write_text(w_content, encoding="utf-8")
346
+ session_touched_files.add("walkthrough.md")
347
+ except Exception:
348
+ pass
349
+
350
+ yield AgentEvent("walkthrough", {
351
+ "path": "walkthrough.md",
352
+ "summary": resp.content,
353
+ "files": sorted(list(session_touched_files)),
354
+ "commands": session_executed_commands,
355
+ })
356
+
269
357
  # Cache if single-turn
270
358
  if is_cacheable and turn == 0:
271
359
  try:
@@ -301,6 +389,17 @@ class AgentOrchestrator:
301
389
  })
302
390
  session.context.add_tool_result(res.tool_call_id, res.content)
303
391
 
392
+ # Track touched files and commands for walkthrough
393
+ for tc in tool_calls:
394
+ if tc.name in ("write_file", "edit_file", "apply_patch"):
395
+ p = tc.arguments.get("path") or tc.arguments.get("filename")
396
+ if p:
397
+ session_touched_files.add(p)
398
+ elif tc.name == "bash":
399
+ c = tc.arguments.get("command", "")
400
+ if c:
401
+ session_executed_commands.append({"command": c, "exit_code": 0})
402
+
304
403
  # ── Record progress for stall detection ──
305
404
  tc_dicts = [{"name": tc.name, "arguments": tc.arguments} for tc in tool_calls]
306
405
  res_dicts = [{"content": res.content, "name": res.name} for res in results]
@@ -1,152 +0,0 @@
1
- """
2
- OpenAI-compatible API inference backend with smart model resolution.
3
- """
4
-
5
- from typing import Any, List, Optional
6
- from openai import OpenAI, NotFoundError
7
-
8
- from ..config import Config, get_shared_http_client, is_server_reachable
9
- from .base import ChatResponse, ToolCall
10
-
11
-
12
- class OpenAIBackend:
13
- """Backend for remote OpenAI-compatible servers (vLLM, Ollama, etc.)."""
14
-
15
- def __init__(self, cfg: Config):
16
- self._cfg = cfg
17
- self._client: Optional[OpenAI] = None
18
- self._resolved_model: Optional[str] = None
19
-
20
- @property
21
- def name(self) -> str:
22
- return f"Online API ({self._cfg.base_url})"
23
-
24
- @property
25
- def is_available(self) -> bool:
26
- return is_server_reachable(self._cfg.base_url)
27
-
28
- @property
29
- def context_window(self) -> int:
30
- return 262_144
31
-
32
- def _ensure_client(self) -> OpenAI:
33
- if self._client is None:
34
- self._client = OpenAI(
35
- base_url=self._cfg.base_url,
36
- api_key=self._cfg.api_key or "not-needed",
37
- http_client=get_shared_http_client(),
38
- )
39
- return self._client
40
-
41
- def _get_available_model(self, client: OpenAI) -> str:
42
- """Auto-discover the served model name from /v1/models."""
43
- if self._resolved_model:
44
- return self._resolved_model
45
- try:
46
- models_list = client.models.list()
47
- if models_list.data:
48
- for m in models_list.data:
49
- if m.id == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
50
- self._resolved_model = m.id
51
- return self._resolved_model
52
- self._resolved_model = models_list.data[0].id
53
- return self._resolved_model
54
- except Exception:
55
- pass
56
- return self._cfg.model
57
-
58
- def chat_completion(
59
- self,
60
- messages: List[dict],
61
- tools: Optional[List[dict]] = None,
62
- temperature: Optional[float] = None,
63
- max_tokens: Optional[int] = None,
64
- enable_thinking: bool = True,
65
- ) -> ChatResponse:
66
- temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.1)
67
- m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 8192)
68
-
69
- client = self._ensure_client()
70
- model_name = self._get_available_model(client)
71
-
72
- params = {
73
- "model": model_name,
74
- "messages": messages,
75
- "temperature": temp,
76
- "max_tokens": m_tokens,
77
- "extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
78
- }
79
- if tools:
80
- params["tools"] = tools
81
- params["tool_choice"] = "auto"
82
-
83
- try:
84
- resp = client.chat.completions.create(**params)
85
- except NotFoundError:
86
- # Model name mismatch -> re-discover model
87
- self._resolved_model = None
88
- model_name = self._get_available_model(client)
89
- params["model"] = model_name
90
- resp = client.chat.completions.create(**params)
91
-
92
- msg = resp.choices[0].message
93
-
94
- tool_calls = None
95
- if msg.tool_calls:
96
- import json
97
- tool_calls = []
98
- for tc in msg.tool_calls:
99
- try:
100
- args = json.loads(tc.function.arguments or "{}")
101
- except (json.JSONDecodeError, TypeError):
102
- args = {}
103
- if not isinstance(args, dict):
104
- args = {}
105
- tool_calls.append(
106
- ToolCall(
107
- id=tc.id,
108
- name=tc.function.name,
109
- arguments=args,
110
- )
111
- )
112
-
113
- reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
114
- content = msg.content or ""
115
-
116
- # Extract embedded thinking/reasoning if not provided in separate fields
117
- if not reasoning and content:
118
- import re
119
- if "</think>" in content:
120
- # Handled when model starts thinking implicitly or with <think>
121
- parts = content.split("</think>", 1)
122
- reasoning = parts[0].replace("<think>", "").strip()
123
- content = parts[1].strip()
124
- elif "<think>" in content:
125
- think_match = re.search(r"<think>(.*?)</think>", content, flags=re.DOTALL)
126
- if think_match:
127
- reasoning = think_match.group(1).strip()
128
- content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
129
- elif content.strip().lower().startswith("thinking process:") or "\nthinking process:" in content.lower():
130
- if tool_calls:
131
- reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
132
- content = ""
133
- else:
134
- code_match = re.search(r"\n(?=```|Here is|Below is|I will|The following|DONE:|#!/usr|/\*|package\s|import\s[a-z]|from\s[a-z])", content, flags=re.IGNORECASE)
135
- if code_match:
136
- think_part = content[:code_match.start()]
137
- content_part = content[code_match.start():]
138
- reasoning = re.sub(r"(?i)^thinking process:\s*", "", think_part).strip()
139
- content = content_part.strip()
140
- else:
141
- reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
142
- content = ""
143
-
144
- return ChatResponse(
145
- content=content,
146
- reasoning=reasoning,
147
- tool_calls=tool_calls,
148
- raw=resp,
149
- )
150
-
151
- def shutdown(self) -> None:
152
- self._client = None
File without changes
File without changes