fix: queue concurrent subagents (#5566)

* fix: queue concurrent subagents

* chore: keep spawn schema concise
This commit is contained in:
chengyongru
2026-08-27 17:53:28 +08:00
committed by GitHub
parent 39de4594d7
commit b9e7c7f6fe
6 changed files with 197 additions and 38 deletions
+32 -1
View File
@@ -55,7 +55,8 @@ class SubagentStatus:
label: str
task_description: str
started_at: float # time.monotonic()
phase: str = "initializing" # initializing | awaiting_tools | tools_completed | final_response | done | error
# queued | initializing | awaiting_tools | tools_completed | final_response | done | error
phase: str = "initializing"
iteration: int = 0
tool_events: list[dict[str, str]] = field(default_factory=list)
usage: LLMUsage | None = None
@@ -147,6 +148,7 @@ class SubagentManager:
if max_concurrent_subagents is not None
else defaults.max_concurrent_subagents
)
self._run_slots = asyncio.Semaphore(self.max_concurrent_subagents)
self.runner = AgentRunner()
self._exec_session_manager = ExecSessionManager()
self._llm_wall_timeout_for_session = llm_wall_timeout_for_session
@@ -363,6 +365,35 @@ class SubagentManager:
workspace_scope: WorkspaceScope | None = None,
*,
announce: bool = True,
) -> str:
"""Wait for capacity, then execute one subagent task."""
status.phase = "queued"
async with self._run_slots:
status.phase = "initializing"
return await self._run_admitted_subagent(
task_id,
task,
label,
origin,
status,
runtime,
origin_message_id,
workspace_scope,
announce=announce,
)
async def _run_admitted_subagent(
self,
task_id: str,
task: str,
label: str,
origin: _SubagentOrigin,
status: SubagentStatus,
runtime: LLMRuntime,
origin_message_id: str | None = None,
workspace_scope: WorkspaceScope | None = None,
*,
announce: bool = True,
) -> str:
"""Execute the subagent task and announce the result."""
logger.info("Subagent [{}] starting task: {}", task_id, label)
+5 -8
View File
@@ -73,6 +73,11 @@ class SpawnTool(Tool):
"and use a dedicated subdirectory when helpful."
)
@property
def concurrency_safe(self) -> bool:
"""Each call owns its task state; the manager serializes capacity admission."""
return True
async def execute(
self,
task: str,
@@ -82,14 +87,6 @@ class SpawnTool(Tool):
**kwargs: Any,
) -> str:
"""Spawn a subagent to execute the given task."""
running = self._manager.get_running_count()
limit = self._manager.max_concurrent_subagents
if running >= limit:
return (
f"Cannot spawn subagent: concurrency limit reached "
f"({running}/{limit} running). Wait for a running subagent "
f"to complete before spawning a new one."
)
request_ctx = current_request_context()
if request_ctx is None or request_ctx.runtime is None:
return ToolResult.error("Error: spawn requires an active model runtime")
+1 -1
View File
@@ -128,7 +128,7 @@ class AgentDefaults(Base):
temperature: float = 0.1
fallback_models: list[FallbackCandidate] = Field(default_factory=list)
max_tool_iterations: int = 200
max_concurrent_subagents: int = Field(default=1, ge=1)
max_concurrent_subagents: int = Field(default=4, ge=1)
max_tool_result_chars: int = 16_000
provider_retry_mode: Literal["standard", "persistent"] = "standard"
tool_hint_max_length: int = Field(