This commit is contained in:
team3
2026-06-30 18:06:06 +02:00
parent a2a5da25df
commit fa718b7d6c
8 changed files with 231 additions and 219 deletions

View File

@@ -13,9 +13,11 @@ import signal
import tempfile
import time
import urllib.request
from contextlib import asynccontextmanager
from pathlib import Path
from config import PROVIDERS, DEFAULT_PROVIDER, MAX_CONCURRENT_AGENTS, MAX_CONCURRENT_INTERACTIVE
from config import (PROVIDERS, DEFAULT_PROVIDER, MAX_CONCURRENT_AGENTS,
MAX_CONCURRENT_AGENTS_PER_TOPIC, MAX_CONCURRENT_INTERACTIVE)
log = logging.getLogger("creator.agents")
@@ -43,6 +45,24 @@ def _scope_cancelled(agent_key: str) -> bool:
# does not count against the agent timeout.
_batch_sem = asyncio.Semaphore(MAX_CONCURRENT_AGENTS)
_interactive_sem = asyncio.Semaphore(MAX_CONCURRENT_INTERACTIVE)
# Per-topic caps (lazily created): each topic gets its own batch semaphore of size
# MAX_CONCURRENT_AGENTS_PER_TOPIC, nested INSIDE the global _batch_sem.
_topic_sems: dict[str, asyncio.Semaphore] = {}
@asynccontextmanager
async def _batch_gate(scope: str | None):
"""Acquire a batch slot: per-topic semaphore FIRST, then the global one. The order matters —
a waiter holds only its (per-topic) slot while queueing for the global cap, so a saturated topic
never blocks other topics on the global semaphore. scope=None → global cap only."""
topic_sem = _topic_sems.setdefault(scope, asyncio.Semaphore(MAX_CONCURRENT_AGENTS_PER_TOPIC)) if scope else None
if topic_sem is None:
async with _batch_sem:
yield
else:
async with topic_sem:
async with _batch_sem:
yield
# Serialize OpenCode starts: processes starting simultaneously collide on the
# internal session DB ("database is locked", exit after <1s). The short
@@ -116,6 +136,7 @@ async def run_agent(
role: str = "fast",
capabilities: str = "none",
lane: str = "batch",
scope: str | None = None,
) -> tuple[int, str, str]:
if _scope_cancelled(agent_key): # before queueing: don't even enter the queue
return 1, "", "cancelled"
@@ -123,8 +144,8 @@ async def run_agent(
return 1, "", f"Unknown provider: {provider}"
if shutil.which(PROVIDERS[provider]["cli"]) is None:
return 1, "", f"CLI '{PROVIDERS[provider]['cli']}' not installed (provider: {provider})"
sem = _interactive_sem if lane == "interactive" else _batch_sem
async with sem:
gate = _interactive_sem if lane == "interactive" else _batch_gate(scope)
async with gate:
if _scope_cancelled(agent_key): # after the acquire: cancelled in the queue → no spawn
return 1, "", "cancelled"
if PROVIDERS[provider]["cli"] == "opencode":