autoforge-ai 0.1.20 → 0.1.22
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.
- package/.claude/templates/auto_improve_prompt.template.md +160 -0
- package/agent.py +14 -2
- package/autonomous_agent_demo.py +27 -1
- package/client.py +7 -2
- package/package.json +1 -1
- package/prompts.py +24 -0
- package/registry.py +143 -15
- package/server/routers/projects.py +58 -10
- package/server/routers/settings.py +8 -0
- package/server/schemas.py +21 -2
- package/server/services/assistant_chat_session.py +3 -1
- package/server/services/browser_view_service.py +280 -280
- package/server/services/expand_chat_session.py +3 -1
- package/server/services/process_manager.py +17 -0
- package/server/services/scheduler_service.py +154 -4
- package/server/services/spec_chat_session.py +3 -1
- package/ui/dist/assets/index-BvNxzjlP.js +96 -0
- package/ui/dist/assets/index-hSFqqmJF.css +1 -0
- package/ui/dist/assets/{vendor-flow-CSXy01ye.js → vendor-flow-4rkFkfFX.js} +1 -1
- package/ui/dist/assets/vendor-radix-CXoPacKb.js +45 -0
- package/ui/dist/assets/vendor-utils-D_WdX4_S.js +2 -0
- package/ui/dist/index.html +5 -5
- package/ui/dist/assets/index-B5-x5jW8.js +0 -96
- package/ui/dist/assets/index-DkJ1zszK.css +0 -1
- package/ui/dist/assets/vendor-radix-DIVIznMB.js +0 -45
- package/ui/dist/assets/vendor-utils-CnTXttNm.js +0 -2
|
@@ -15,6 +15,7 @@ from typing import Optional
|
|
|
15
15
|
|
|
16
16
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
17
17
|
from apscheduler.triggers.cron import CronTrigger
|
|
18
|
+
from apscheduler.triggers.interval import IntervalTrigger
|
|
18
19
|
|
|
19
20
|
# Add parent directory for imports
|
|
20
21
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
@@ -73,19 +74,39 @@ class SchedulerService:
|
|
|
73
74
|
logger.info("Scheduler service stopped")
|
|
74
75
|
|
|
75
76
|
async def _load_all_schedules(self):
|
|
76
|
-
"""Load schedules for all registered projects."""
|
|
77
|
+
"""Load schedules and auto-improve jobs for all registered projects."""
|
|
77
78
|
from registry import list_registered_projects
|
|
78
79
|
|
|
79
80
|
try:
|
|
80
81
|
projects = list_registered_projects()
|
|
81
82
|
total_loaded = 0
|
|
83
|
+
total_auto_improve = 0
|
|
82
84
|
for project_name, info in projects.items():
|
|
83
85
|
project_path = Path(info.get("path", ""))
|
|
84
|
-
if project_path.exists():
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
if not project_path.exists():
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Windowed schedules (cron-based)
|
|
90
|
+
count = await self._load_project_schedules(project_name, project_path)
|
|
91
|
+
total_loaded += count
|
|
92
|
+
|
|
93
|
+
# Auto-improve interval jobs (stored in the registry directly)
|
|
94
|
+
if info.get("auto_improve_enabled"):
|
|
95
|
+
interval = int(info.get("auto_improve_interval_minutes", 10) or 10)
|
|
96
|
+
try:
|
|
97
|
+
await self.register_auto_improve(project_name, project_path, interval)
|
|
98
|
+
total_auto_improve += 1
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(
|
|
101
|
+
f"Failed to register auto-improve for {project_name}: {e}"
|
|
102
|
+
)
|
|
103
|
+
|
|
87
104
|
if total_loaded > 0:
|
|
88
105
|
logger.info(f"Loaded {total_loaded} schedule(s) across all projects")
|
|
106
|
+
if total_auto_improve > 0:
|
|
107
|
+
logger.info(
|
|
108
|
+
f"Registered {total_auto_improve} auto-improve job(s) across all projects"
|
|
109
|
+
)
|
|
89
110
|
except Exception as e:
|
|
90
111
|
logger.error(f"Error loading schedules: {e}")
|
|
91
112
|
|
|
@@ -205,6 +226,135 @@ class SchedulerService:
|
|
|
205
226
|
else:
|
|
206
227
|
logger.warning(f"No jobs found to remove for schedule {schedule_id}")
|
|
207
228
|
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
# Auto-improve interval jobs
|
|
231
|
+
# ------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _auto_improve_job_id(project_name: str) -> str:
|
|
235
|
+
return f"auto_improve_{project_name}"
|
|
236
|
+
|
|
237
|
+
async def register_auto_improve(
|
|
238
|
+
self,
|
|
239
|
+
project_name: str,
|
|
240
|
+
project_dir: Path,
|
|
241
|
+
interval_minutes: int,
|
|
242
|
+
):
|
|
243
|
+
"""Register or replace the auto-improve interval job for a project.
|
|
244
|
+
|
|
245
|
+
Uses APScheduler IntervalTrigger to fire every ``interval_minutes``.
|
|
246
|
+
On each tick, a single one-shot auto-improve agent session is started
|
|
247
|
+
(unless the project's agent is already running, in which case the tick
|
|
248
|
+
is silently skipped).
|
|
249
|
+
"""
|
|
250
|
+
if interval_minutes < 1 or interval_minutes > 1440:
|
|
251
|
+
raise ValueError("interval_minutes must be between 1 and 1440")
|
|
252
|
+
|
|
253
|
+
job_id = self._auto_improve_job_id(project_name)
|
|
254
|
+
trigger = IntervalTrigger(minutes=interval_minutes)
|
|
255
|
+
|
|
256
|
+
self.scheduler.add_job(
|
|
257
|
+
self._handle_auto_improve_tick,
|
|
258
|
+
trigger,
|
|
259
|
+
id=job_id,
|
|
260
|
+
args=[project_name, str(project_dir)],
|
|
261
|
+
replace_existing=True,
|
|
262
|
+
misfire_grace_time=300,
|
|
263
|
+
max_instances=1, # Never overlap ticks
|
|
264
|
+
coalesce=True, # Collapse missed ticks during long runs
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
job = self.scheduler.get_job(job_id)
|
|
268
|
+
next_run = job.next_run_time if job else None
|
|
269
|
+
logger.info(
|
|
270
|
+
f"Registered auto-improve for {project_name}: "
|
|
271
|
+
f"every {interval_minutes} min (next: {next_run})"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def remove_auto_improve(self, project_name: str):
|
|
275
|
+
"""Remove the auto-improve interval job for a project (no-op if missing)."""
|
|
276
|
+
job_id = self._auto_improve_job_id(project_name)
|
|
277
|
+
try:
|
|
278
|
+
self.scheduler.remove_job(job_id)
|
|
279
|
+
logger.info(f"Removed auto-improve job for {project_name}")
|
|
280
|
+
except Exception:
|
|
281
|
+
logger.debug(f"No auto-improve job to remove for {project_name}")
|
|
282
|
+
|
|
283
|
+
async def _handle_auto_improve_tick(
|
|
284
|
+
self,
|
|
285
|
+
project_name: str,
|
|
286
|
+
project_dir_str: str,
|
|
287
|
+
):
|
|
288
|
+
"""Fire one auto-improve agent session for a project.
|
|
289
|
+
|
|
290
|
+
Silently skips if the agent is already running (manual run, another
|
|
291
|
+
tick still executing, etc.). The scheduler's ``max_instances=1`` and
|
|
292
|
+
``coalesce=True`` settings make sure ticks never stack up.
|
|
293
|
+
"""
|
|
294
|
+
logger.info(f"Auto-improve tick for {project_name}")
|
|
295
|
+
project_dir = Path(project_dir_str)
|
|
296
|
+
|
|
297
|
+
if not project_dir.exists():
|
|
298
|
+
logger.warning(
|
|
299
|
+
f"Auto-improve tick: project dir missing for {project_name}, skipping"
|
|
300
|
+
)
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
from .process_manager import get_manager
|
|
305
|
+
|
|
306
|
+
root_dir = Path(__file__).parent.parent.parent
|
|
307
|
+
manager = get_manager(project_name, project_dir, root_dir)
|
|
308
|
+
|
|
309
|
+
if manager.status in ("running", "paused", "pausing", "paused_graceful"):
|
|
310
|
+
logger.info(
|
|
311
|
+
f"Auto-improve tick for {project_name}: agent already "
|
|
312
|
+
f"{manager.status}, skipping this tick"
|
|
313
|
+
)
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# Resolve effective yolo/model from global settings, mirroring the
|
|
317
|
+
# agent router's _get_settings_defaults() pattern so auto-improve
|
|
318
|
+
# respects whatever the user has configured globally.
|
|
319
|
+
yolo_mode, model = self._resolve_agent_defaults()
|
|
320
|
+
|
|
321
|
+
logger.info(
|
|
322
|
+
f"Starting auto-improve agent for {project_name} "
|
|
323
|
+
f"(yolo={yolo_mode}, model={model})"
|
|
324
|
+
)
|
|
325
|
+
success, msg = await manager.start(
|
|
326
|
+
yolo_mode=yolo_mode,
|
|
327
|
+
model=model,
|
|
328
|
+
max_concurrency=1,
|
|
329
|
+
testing_agent_ratio=0,
|
|
330
|
+
playwright_headless=True,
|
|
331
|
+
auto_improve=True,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if success:
|
|
335
|
+
logger.info(f"Auto-improve agent started for {project_name}")
|
|
336
|
+
else:
|
|
337
|
+
logger.warning(
|
|
338
|
+
f"Auto-improve agent failed to start for {project_name}: {msg}"
|
|
339
|
+
)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Error in auto-improve tick for {project_name}: {e}")
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _resolve_agent_defaults() -> tuple[bool, str]:
|
|
345
|
+
"""Resolve (yolo_mode, model) from global settings.
|
|
346
|
+
|
|
347
|
+
Kept separate from the agent router's helper so the scheduler never
|
|
348
|
+
has to import FastAPI routers. Mirrors the parsing behavior of
|
|
349
|
+
``server/routers/agent.py::_get_settings_defaults``.
|
|
350
|
+
"""
|
|
351
|
+
from registry import DEFAULT_MODEL, get_all_settings
|
|
352
|
+
|
|
353
|
+
settings = get_all_settings()
|
|
354
|
+
yolo_mode = (settings.get("yolo_mode") or "false").lower() == "true"
|
|
355
|
+
model = settings.get("api_model") or settings.get("model", DEFAULT_MODEL)
|
|
356
|
+
return yolo_mode, model
|
|
357
|
+
|
|
208
358
|
async def _handle_scheduled_start(
|
|
209
359
|
self, project_name: str, schedule_id: int, project_dir_str: str
|
|
210
360
|
):
|
|
@@ -147,8 +147,9 @@ class SpecChatSession:
|
|
|
147
147
|
system_cli = shutil.which("claude")
|
|
148
148
|
|
|
149
149
|
# Build environment overrides for API configuration
|
|
150
|
-
from registry import DEFAULT_MODEL, get_effective_sdk_env
|
|
150
|
+
from registry import DEFAULT_MODEL, get_effective_sdk_env, get_effort_setting
|
|
151
151
|
sdk_env = get_effective_sdk_env()
|
|
152
|
+
effort = get_effort_setting()
|
|
152
153
|
|
|
153
154
|
# Determine model from SDK env (provider-aware) or fallback to env/default
|
|
154
155
|
model = sdk_env.get("ANTHROPIC_DEFAULT_OPUS_MODEL") or os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", DEFAULT_MODEL)
|
|
@@ -157,6 +158,7 @@ class SpecChatSession:
|
|
|
157
158
|
self.client = ClaudeSDKClient(
|
|
158
159
|
options=ClaudeAgentOptions(
|
|
159
160
|
model=model,
|
|
161
|
+
effort=effort, # type: ignore[arg-type] # SDK 0.1.61 Literal omits "xhigh"
|
|
160
162
|
cli_path=system_cli,
|
|
161
163
|
# System prompt loaded from CLAUDE.md via setting_sources
|
|
162
164
|
# Include "user" for global skills and subagents from ~/.claude/
|