update
This commit is contained in:
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user