2026-02-01 16:28:45 +00:00
|
|
|
"""Subagent manager for background task execution."""
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import json
|
2026-04-15 09:15:27 +08:00
|
|
|
import time
|
2026-02-01 16:28:45 +00:00
|
|
|
import uuid
|
2026-04-15 09:15:27 +08:00
|
|
|
from dataclasses import dataclass, field
|
2026-02-01 16:28:45 +00:00
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from loguru import logger
|
|
|
|
|
|
2026-03-26 19:39:57 +00:00
|
|
|
from nanobot.agent.hook import AgentHook, AgentHookContext
|
2026-04-28 07:25:47 +00:00
|
|
|
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
2026-03-15 15:13:41 +00:00
|
|
|
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
|
2026-02-28 20:55:43 +08:00
|
|
|
from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool
|
|
|
|
|
from nanobot.agent.tools.registry import ToolRegistry
|
2026-04-02 15:37:57 +00:00
|
|
|
from nanobot.agent.tools.search import GlobTool, GrepTool
|
2026-02-28 20:55:43 +08:00
|
|
|
from nanobot.agent.tools.shell import ExecTool
|
|
|
|
|
from nanobot.agent.tools.web import WebFetchTool, WebSearchTool
|
2026-02-01 16:28:45 +00:00
|
|
|
from nanobot.bus.events import InboundMessage
|
|
|
|
|
from nanobot.bus.queue import MessageBus
|
2026-04-29 17:53:28 +08:00
|
|
|
from nanobot.config.schema import AgentDefaults, ExecToolConfig, WebToolsConfig
|
2026-02-01 16:28:45 +00:00
|
|
|
from nanobot.providers.base import LLMProvider
|
2026-04-28 07:25:47 +00:00
|
|
|
from nanobot.utils.prompt_templates import render_template
|
2026-02-01 16:28:45 +00:00
|
|
|
|
|
|
|
|
|
2026-04-15 09:15:27 +08:00
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class SubagentStatus:
|
|
|
|
|
"""Real-time status of a running subagent."""
|
2026-03-29 22:56:02 +08:00
|
|
|
|
2026-04-15 09:15:27 +08:00
|
|
|
task_id: str
|
|
|
|
|
label: str
|
|
|
|
|
task_description: str
|
|
|
|
|
started_at: float # time.monotonic()
|
|
|
|
|
phase: str = "initializing" # initializing | awaiting_tools | tools_completed | final_response | done | error
|
|
|
|
|
iteration: int = 0
|
|
|
|
|
tool_events: list = field(default_factory=list) # [{name, status, detail}, ...]
|
|
|
|
|
usage: dict = field(default_factory=dict) # token usage
|
|
|
|
|
stop_reason: str | None = None
|
|
|
|
|
error: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _SubagentHook(AgentHook):
|
|
|
|
|
"""Hook for subagent execution — logs tool calls and updates status."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, task_id: str, status: SubagentStatus | None = None) -> None:
|
2026-04-08 15:38:41 +00:00
|
|
|
super().__init__()
|
2026-03-29 22:56:02 +08:00
|
|
|
self._task_id = task_id
|
2026-04-15 09:15:27 +08:00
|
|
|
self._status = status
|
2026-03-29 22:56:02 +08:00
|
|
|
|
|
|
|
|
async def before_execute_tools(self, context: AgentHookContext) -> None:
|
|
|
|
|
for tool_call in context.tool_calls:
|
|
|
|
|
args_str = json.dumps(tool_call.arguments, ensure_ascii=False)
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Subagent [{}] executing: {} with arguments: {}",
|
|
|
|
|
self._task_id, tool_call.name, args_str,
|
|
|
|
|
)
|
2026-02-01 16:28:45 +00:00
|
|
|
|
2026-04-15 09:15:27 +08:00
|
|
|
async def after_iteration(self, context: AgentHookContext) -> None:
|
|
|
|
|
if self._status is None:
|
|
|
|
|
return
|
|
|
|
|
self._status.iteration = context.iteration
|
|
|
|
|
self._status.tool_events = list(context.tool_events)
|
|
|
|
|
self._status.usage = dict(context.usage)
|
|
|
|
|
if context.error:
|
|
|
|
|
self._status.error = str(context.error)
|
|
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
|
|
|
|
|
class SubagentManager:
|
2026-02-25 17:04:08 +00:00
|
|
|
"""Manages background subagent execution."""
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
provider: LLMProvider,
|
|
|
|
|
workspace: Path,
|
|
|
|
|
bus: MessageBus,
|
2026-04-01 19:12:49 +00:00
|
|
|
max_tool_result_chars: int,
|
2026-02-01 16:28:45 +00:00
|
|
|
model: str | None = None,
|
2026-03-30 15:16:58 +08:00
|
|
|
web_config: "WebToolsConfig | None" = None,
|
2026-02-04 03:45:26 +00:00
|
|
|
exec_config: "ExecToolConfig | None" = None,
|
2026-02-06 09:28:08 +00:00
|
|
|
restrict_to_workspace: bool = False,
|
2026-04-12 02:02:39 +00:00
|
|
|
disabled_skills: list[str] | None = None,
|
2026-04-29 17:53:28 +08:00
|
|
|
max_iterations: int | None = None,
|
2026-02-01 16:28:45 +00:00
|
|
|
):
|
2026-05-05 21:11:27 +08:00
|
|
|
defaults = AgentDefaults()
|
2026-02-01 16:28:45 +00:00
|
|
|
self.provider = provider
|
|
|
|
|
self.workspace = workspace
|
|
|
|
|
self.bus = bus
|
|
|
|
|
self.model = model or provider.get_default_model()
|
2026-03-30 15:16:58 +08:00
|
|
|
self.web_config = web_config or WebToolsConfig()
|
2026-04-01 19:12:49 +00:00
|
|
|
self.max_tool_result_chars = max_tool_result_chars
|
2026-02-04 03:45:26 +00:00
|
|
|
self.exec_config = exec_config or ExecToolConfig()
|
2026-02-06 09:28:08 +00:00
|
|
|
self.restrict_to_workspace = restrict_to_workspace
|
2026-04-12 02:02:39 +00:00
|
|
|
self.disabled_skills = set(disabled_skills or [])
|
2026-04-29 17:53:28 +08:00
|
|
|
self.max_iterations = (
|
|
|
|
|
max_iterations
|
|
|
|
|
if max_iterations is not None
|
2026-05-05 21:11:27 +08:00
|
|
|
else defaults.max_tool_iterations
|
2026-04-29 17:53:28 +08:00
|
|
|
)
|
2026-05-05 21:11:27 +08:00
|
|
|
self.max_concurrent_subagents = defaults.max_concurrent_subagents
|
2026-03-26 18:44:53 +00:00
|
|
|
self.runner = AgentRunner(provider)
|
2026-02-01 16:28:45 +00:00
|
|
|
self._running_tasks: dict[str, asyncio.Task[None]] = {}
|
2026-04-15 09:15:27 +08:00
|
|
|
self._task_statuses: dict[str, SubagentStatus] = {}
|
2026-02-25 17:53:54 +08:00
|
|
|
self._session_tasks: dict[str, set[str]] = {} # session_key -> {task_id, ...}
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-04-26 14:18:37 +00:00
|
|
|
def set_provider(self, provider: LLMProvider, model: str) -> None:
|
|
|
|
|
self.provider = provider
|
|
|
|
|
self.model = model
|
|
|
|
|
self.runner.provider = provider
|
|
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
async def spawn(
|
|
|
|
|
self,
|
|
|
|
|
task: str,
|
|
|
|
|
label: str | None = None,
|
|
|
|
|
origin_channel: str = "cli",
|
|
|
|
|
origin_chat_id: str = "direct",
|
2026-02-25 17:53:54 +08:00
|
|
|
session_key: str | None = None,
|
2026-03-23 21:26:24 +08:00
|
|
|
origin_message_id: str | None = None,
|
2026-02-01 16:28:45 +00:00
|
|
|
) -> str:
|
2026-02-25 17:04:08 +00:00
|
|
|
"""Spawn a subagent to execute a task in the background."""
|
2026-02-01 16:28:45 +00:00
|
|
|
task_id = str(uuid.uuid4())[:8]
|
|
|
|
|
display_label = label or task[:30] + ("..." if len(task) > 30 else "")
|
2026-04-20 11:53:29 +08:00
|
|
|
origin = {"channel": origin_channel, "chat_id": origin_chat_id, "session_key": session_key}
|
2026-02-25 17:04:08 +00:00
|
|
|
|
2026-04-15 09:15:27 +08:00
|
|
|
status = SubagentStatus(
|
|
|
|
|
task_id=task_id,
|
|
|
|
|
label=display_label,
|
|
|
|
|
task_description=task,
|
|
|
|
|
started_at=time.monotonic(),
|
|
|
|
|
)
|
|
|
|
|
self._task_statuses[task_id] = status
|
|
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
bg_task = asyncio.create_task(
|
2026-03-23 21:26:24 +08:00
|
|
|
self._run_subagent(task_id, task, display_label, origin, status, origin_message_id)
|
2026-02-01 16:28:45 +00:00
|
|
|
)
|
|
|
|
|
self._running_tasks[task_id] = bg_task
|
2026-02-25 17:53:54 +08:00
|
|
|
if session_key:
|
|
|
|
|
self._session_tasks.setdefault(session_key, set()).add(task_id)
|
|
|
|
|
|
|
|
|
|
def _cleanup(_: asyncio.Task) -> None:
|
|
|
|
|
self._running_tasks.pop(task_id, None)
|
2026-04-15 09:15:27 +08:00
|
|
|
self._task_statuses.pop(task_id, None)
|
2026-02-25 17:04:08 +00:00
|
|
|
if session_key and (ids := self._session_tasks.get(session_key)):
|
|
|
|
|
ids.discard(task_id)
|
|
|
|
|
if not ids:
|
|
|
|
|
del self._session_tasks[session_key]
|
2026-02-25 17:53:54 +08:00
|
|
|
|
|
|
|
|
bg_task.add_done_callback(_cleanup)
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-02-20 07:55:34 +00:00
|
|
|
logger.info("Spawned subagent [{}]: {}", task_id, display_label)
|
2026-02-01 16:28:45 +00:00
|
|
|
return f"Subagent [{display_label}] started (id: {task_id}). I'll notify you when it completes."
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
async def _run_subagent(
|
|
|
|
|
self,
|
|
|
|
|
task_id: str,
|
|
|
|
|
task: str,
|
|
|
|
|
label: str,
|
|
|
|
|
origin: dict[str, str],
|
2026-04-15 09:15:27 +08:00
|
|
|
status: SubagentStatus,
|
2026-03-23 21:26:24 +08:00
|
|
|
origin_message_id: str | None = None,
|
2026-02-01 16:28:45 +00:00
|
|
|
) -> None:
|
|
|
|
|
"""Execute the subagent task and announce the result."""
|
2026-02-20 07:55:34 +00:00
|
|
|
logger.info("Subagent [{}] starting task: {}", task_id, label)
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-04-15 09:15:27 +08:00
|
|
|
async def _on_checkpoint(payload: dict) -> None:
|
|
|
|
|
status.phase = payload.get("phase", status.phase)
|
|
|
|
|
status.iteration = payload.get("iteration", status.iteration)
|
|
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
try:
|
|
|
|
|
# Build subagent tools (no message tool, no spawn tool)
|
|
|
|
|
tools = ToolRegistry()
|
2026-03-16 23:55:19 -07:00
|
|
|
allowed_dir = self.workspace if (self.restrict_to_workspace or self.exec_config.sandbox) else None
|
2026-03-15 15:13:41 +00:00
|
|
|
extra_read = [BUILTIN_SKILLS_DIR] if allowed_dir else None
|
2026-05-01 18:58:41 +08:00
|
|
|
# Subagent gets its own FileStates so its read-dedup cache is
|
|
|
|
|
# isolated from the parent loop's sessions (issue #3571).
|
|
|
|
|
from nanobot.agent.tools.file_state import FileStates
|
|
|
|
|
file_states = FileStates()
|
|
|
|
|
tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir, extra_allowed_dirs=extra_read, file_states=file_states))
|
|
|
|
|
tools.register(WriteFileTool(workspace=self.workspace, allowed_dir=allowed_dir, file_states=file_states))
|
|
|
|
|
tools.register(EditFileTool(workspace=self.workspace, allowed_dir=allowed_dir, file_states=file_states))
|
|
|
|
|
tools.register(ListDirTool(workspace=self.workspace, allowed_dir=allowed_dir, file_states=file_states))
|
|
|
|
|
tools.register(GlobTool(workspace=self.workspace, allowed_dir=allowed_dir, file_states=file_states))
|
|
|
|
|
tools.register(GrepTool(workspace=self.workspace, allowed_dir=allowed_dir, file_states=file_states))
|
2026-03-31 00:09:01 +08:00
|
|
|
if self.exec_config.enable:
|
|
|
|
|
tools.register(ExecTool(
|
|
|
|
|
working_dir=str(self.workspace),
|
|
|
|
|
timeout=self.exec_config.timeout,
|
|
|
|
|
restrict_to_workspace=self.restrict_to_workspace,
|
2026-04-05 18:53:17 +00:00
|
|
|
sandbox=self.exec_config.sandbox,
|
2026-03-31 00:09:01 +08:00
|
|
|
path_append=self.exec_config.path_append,
|
2026-04-17 00:28:14 -03:00
|
|
|
allowed_env_keys=self.exec_config.allowed_env_keys,
|
2026-05-03 00:27:17 +08:00
|
|
|
allow_patterns=self.exec_config.allow_patterns,
|
|
|
|
|
deny_patterns=self.exec_config.deny_patterns,
|
2026-03-31 00:09:01 +08:00
|
|
|
))
|
2026-03-30 15:16:58 +08:00
|
|
|
if self.web_config.enable:
|
2026-04-22 09:11:57 +00:00
|
|
|
tools.register(
|
|
|
|
|
WebSearchTool(
|
|
|
|
|
config=self.web_config.search,
|
|
|
|
|
proxy=self.web_config.proxy,
|
|
|
|
|
user_agent=self.web_config.user_agent,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
tools.register(
|
|
|
|
|
WebFetchTool(
|
2026-04-22 09:28:30 +00:00
|
|
|
config=self.web_config.fetch,
|
2026-04-22 09:11:57 +00:00
|
|
|
proxy=self.web_config.proxy,
|
|
|
|
|
user_agent=self.web_config.user_agent,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-02-28 16:32:50 +00:00
|
|
|
system_prompt = self._build_subagent_prompt()
|
2026-02-01 16:28:45 +00:00
|
|
|
messages: list[dict[str, Any]] = [
|
|
|
|
|
{"role": "system", "content": system_prompt},
|
|
|
|
|
{"role": "user", "content": task},
|
|
|
|
|
]
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-03-26 18:44:53 +00:00
|
|
|
result = await self.runner.run(AgentRunSpec(
|
|
|
|
|
initial_messages=messages,
|
|
|
|
|
tools=tools,
|
|
|
|
|
model=self.model,
|
2026-04-29 17:53:28 +08:00
|
|
|
max_iterations=self.max_iterations,
|
2026-04-01 19:12:49 +00:00
|
|
|
max_tool_result_chars=self.max_tool_result_chars,
|
2026-04-15 09:15:27 +08:00
|
|
|
hook=_SubagentHook(task_id, status),
|
2026-03-26 18:44:53 +00:00
|
|
|
max_iterations_message="Task completed but no final response was generated.",
|
|
|
|
|
error_message=None,
|
|
|
|
|
fail_on_tool_error=True,
|
2026-04-15 09:15:27 +08:00
|
|
|
checkpoint_callback=_on_checkpoint,
|
2026-03-26 18:44:53 +00:00
|
|
|
))
|
2026-04-15 09:15:27 +08:00
|
|
|
status.phase = "done"
|
|
|
|
|
status.stop_reason = result.stop_reason
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-04-15 09:15:27 +08:00
|
|
|
if result.stop_reason == "tool_error":
|
|
|
|
|
status.tool_events = list(result.tool_events)
|
|
|
|
|
await self._announce_result(
|
|
|
|
|
task_id, label, task,
|
|
|
|
|
self._format_partial_progress(result),
|
2026-03-23 21:26:24 +08:00
|
|
|
origin, "error", origin_message_id,
|
2026-04-15 09:15:27 +08:00
|
|
|
)
|
|
|
|
|
elif result.stop_reason == "error":
|
|
|
|
|
await self._announce_result(
|
|
|
|
|
task_id, label, task,
|
|
|
|
|
result.error or "Error: subagent execution failed.",
|
2026-03-23 21:26:24 +08:00
|
|
|
origin, "error", origin_message_id,
|
2026-04-15 09:15:27 +08:00
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
final_result = result.final_content or "Task completed but no final response was generated."
|
|
|
|
|
logger.info("Subagent [{}] completed successfully", task_id)
|
2026-03-23 21:26:24 +08:00
|
|
|
await self._announce_result(task_id, label, task, final_result, origin, "ok", origin_message_id)
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
except Exception as e:
|
2026-04-15 09:15:27 +08:00
|
|
|
status.phase = "error"
|
|
|
|
|
status.error = str(e)
|
2026-02-19 17:19:36 -03:00
|
|
|
logger.error("Subagent [{}] failed: {}", task_id, e)
|
2026-03-23 21:26:24 +08:00
|
|
|
await self._announce_result(task_id, label, task, f"Error: {e}", origin, "error", origin_message_id)
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
async def _announce_result(
|
|
|
|
|
self,
|
|
|
|
|
task_id: str,
|
|
|
|
|
label: str,
|
|
|
|
|
task: str,
|
|
|
|
|
result: str,
|
|
|
|
|
origin: dict[str, str],
|
|
|
|
|
status: str,
|
2026-03-23 21:26:24 +08:00
|
|
|
origin_message_id: str | None = None,
|
2026-02-01 16:28:45 +00:00
|
|
|
) -> None:
|
|
|
|
|
"""Announce the subagent result to the main agent via the message bus."""
|
|
|
|
|
status_text = "completed successfully" if status == "ok" else "failed"
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-04-04 00:56:22 +08:00
|
|
|
announce_content = render_template(
|
|
|
|
|
"agent/subagent_announce.md",
|
|
|
|
|
label=label,
|
|
|
|
|
status_text=status_text,
|
|
|
|
|
task=task,
|
|
|
|
|
result=result,
|
|
|
|
|
)
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-04-20 10:56:27 +08:00
|
|
|
# Inject as system message to trigger main agent.
|
2026-04-20 11:53:29 +08:00
|
|
|
# Use session_key_override to align with the main agent's effective
|
|
|
|
|
# session key (which accounts for unified sessions) so the result is
|
|
|
|
|
# routed to the correct pending queue (mid-turn injection) instead of
|
|
|
|
|
# being dispatched as a competing independent task.
|
|
|
|
|
override = origin.get("session_key") or f"{origin['channel']}:{origin['chat_id']}"
|
2026-03-23 21:26:24 +08:00
|
|
|
metadata: dict[str, Any] = {
|
|
|
|
|
"injected_event": "subagent_result",
|
|
|
|
|
"subagent_task_id": task_id,
|
|
|
|
|
}
|
|
|
|
|
if origin_message_id:
|
|
|
|
|
metadata["origin_message_id"] = origin_message_id
|
2026-02-01 16:28:45 +00:00
|
|
|
msg = InboundMessage(
|
|
|
|
|
channel="system",
|
|
|
|
|
sender_id="subagent",
|
|
|
|
|
chat_id=f"{origin['channel']}:{origin['chat_id']}",
|
|
|
|
|
content=announce_content,
|
2026-04-20 11:53:29 +08:00
|
|
|
session_key_override=override,
|
2026-03-23 21:26:24 +08:00
|
|
|
metadata=metadata,
|
2026-02-01 16:28:45 +00:00
|
|
|
)
|
2026-02-28 20:55:43 +08:00
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
await self.bus.publish_inbound(msg)
|
2026-02-20 07:55:34 +00:00
|
|
|
logger.debug("Subagent [{}] announced result to {}:{}", task_id, origin['channel'], origin['chat_id'])
|
2026-03-26 18:44:53 +00:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _format_partial_progress(result) -> str:
|
|
|
|
|
completed = [e for e in result.tool_events if e["status"] == "ok"]
|
|
|
|
|
failure = next((e for e in reversed(result.tool_events) if e["status"] == "error"), None)
|
|
|
|
|
lines: list[str] = []
|
|
|
|
|
if completed:
|
|
|
|
|
lines.append("Completed steps:")
|
|
|
|
|
for event in completed[-3:]:
|
|
|
|
|
lines.append(f"- {event['name']}: {event['detail']}")
|
|
|
|
|
if failure:
|
|
|
|
|
if lines:
|
|
|
|
|
lines.append("")
|
|
|
|
|
lines.append("Failure:")
|
|
|
|
|
lines.append(f"- {failure['name']}: {failure['detail']}")
|
|
|
|
|
if result.error and not failure:
|
|
|
|
|
if lines:
|
|
|
|
|
lines.append("")
|
|
|
|
|
lines.append("Failure:")
|
|
|
|
|
lines.append(f"- {result.error}")
|
|
|
|
|
return "\n".join(lines) or (result.error or "Error: subagent execution failed.")
|
2026-03-29 22:56:02 +08:00
|
|
|
|
2026-02-28 16:32:50 +00:00
|
|
|
def _build_subagent_prompt(self) -> str:
|
2026-02-01 16:28:45 +00:00
|
|
|
"""Build a focused system prompt for the subagent."""
|
2026-02-28 16:32:50 +00:00
|
|
|
from nanobot.agent.context import ContextBuilder
|
|
|
|
|
from nanobot.agent.skills import SkillsLoader
|
2026-02-12 07:49:36 +00:00
|
|
|
|
2026-02-28 16:32:50 +00:00
|
|
|
time_ctx = ContextBuilder._build_runtime_context(None, None)
|
2026-04-12 02:02:39 +00:00
|
|
|
skills_summary = SkillsLoader(
|
|
|
|
|
self.workspace,
|
|
|
|
|
disabled_skills=self.disabled_skills,
|
|
|
|
|
).build_skills_summary()
|
2026-04-04 00:56:22 +08:00
|
|
|
return render_template(
|
|
|
|
|
"agent/subagent_system.md",
|
|
|
|
|
time_ctx=time_ctx,
|
|
|
|
|
workspace=str(self.workspace),
|
|
|
|
|
skills_summary=skills_summary or "",
|
|
|
|
|
)
|
2026-03-11 09:56:18 +08:00
|
|
|
|
2026-02-25 17:53:54 +08:00
|
|
|
async def cancel_by_session(self, session_key: str) -> int:
|
2026-02-25 17:04:08 +00:00
|
|
|
"""Cancel all subagents for the given session. Returns count cancelled."""
|
|
|
|
|
tasks = [self._running_tasks[tid] for tid in self._session_tasks.get(session_key, [])
|
|
|
|
|
if tid in self._running_tasks and not self._running_tasks[tid].done()]
|
|
|
|
|
for t in tasks:
|
|
|
|
|
t.cancel()
|
|
|
|
|
if tasks:
|
|
|
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
|
return len(tasks)
|
2026-02-25 17:53:54 +08:00
|
|
|
|
2026-02-01 16:28:45 +00:00
|
|
|
def get_running_count(self) -> int:
|
|
|
|
|
"""Return the number of currently running subagents."""
|
|
|
|
|
return len(self._running_tasks)
|
2026-04-14 22:25:43 +08:00
|
|
|
|
|
|
|
|
def get_running_count_by_session(self, session_key: str) -> int:
|
|
|
|
|
"""Return the number of currently running subagents for a session."""
|
|
|
|
|
tids = self._session_tasks.get(session_key, set())
|
|
|
|
|
return sum(
|
|
|
|
|
1 for tid in tids
|
|
|
|
|
if tid in self._running_tasks and not self._running_tasks[tid].done()
|
|
|
|
|
)
|