Skip to content

Conversation

@advent259141
Copy link
Member

@advent259141 advent259141 commented Jan 26, 2026

AstrBot 现在拥有完善的 Proactive Agent(主动 Agent)系统和简单的 SubAgent 编排功能,并且具备更好的将多媒体文件发送给用户的能力!

动机

  1. SubAgent: 目前框架采取将所有 tool 全部挂载在主 agent 上,主 agent 既负责与用户对话,又要负责组织和调用大量工具。若 tool 数量多,容易导致 prompt 长度爆炸以及 llm 调用失误等情况。若用户可以创建 subagent,每个 subagent 负责挂载一定的工具,主llm只需负责聊天以及向subagent委派任务即可,具体工具由subagent负责调用。这样可以极大得减少主llm的prompt长度以及执行任务时的出错概率。
  2. FutureTask: 目前框架的定时任务功能非常简单,不能满足 proactive agent 的要求。

改动点

  1. 支持 SubAgent 功能,开启时,主 Agent 仅负责对话与委派,只会看到 transfer_to_* 这类委派工具;需要调用工具时,会把任务交给对应 SubAgent 执行。SubAgent 负责真正的工具调用与结果整理,并把结果回传给主 Agent。注册的 SubAgent 会自动成为一个名为 transfer_to_(subagent名称) 的 tool,复用框架原有 handleoff 功能,主 Agent 调用 transfer_to_(agent名称) 即将任务委派给指定 Subagent。
  2. 支持 FutureTask 功能,支持主 Agent 控制一个全局的 Cron Job List,给未来的自己设置任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。目前仅支持 telegram, onebot, slack, 飞书 lark, discord, misskey, satori。未来会支持 ChatUI。
  3. 默认会提供主 Agent 一个 send_message_to_user tool,来方便地将图片 / 文件 / 音频等多媒体文件直接发送给用户(仅支持上述平台)。

Motivations

  1. SubAgent
    In the current architecture, all tools are mounted on the main agent. The main agent is responsible for user interaction and for organizing and invoking a large number of tools. As the number of tools grows, this easily leads to prompt bloat and higher LLM failure rates.

    By allowing users to create SubAgents, each SubAgent can be responsible for a specific subset of tools. The main agent only needs to handle conversation and task delegation, while actual tool execution is handled by SubAgents. This design greatly reduces the prompt length of the main agent and significantly lowers the probability of execution errors.

  2. FutureTask
    The existing scheduling mechanism is very limited and cannot meet the requirements of a true proactive agent.

Modifications

  1. SubAgent Support
    When SubAgent mode is enabled, the main agent is responsible only for conversation and delegation. It can see and use only delegation tools such as transfer_to_*.
    When a task requires tool usage, the main agent delegates it to the appropriate SubAgent. The SubAgent performs the actual tool calls, organizes the results, and returns them to the main agent.

    Each registered SubAgent is automatically exposed as a tool named transfer_to_<subagent_name>, reusing the framework’s existing handoff mechanism. The main agent can delegate tasks simply by calling transfer_to_<agent_name>.

  2. FutureTask Support
    The main agent can now manage a global Cron Job List, allowing it to assign tasks to its future self. AstrBot will automatically wake up at the scheduled time, execute the task, and report the results back to the task creator.

    Currently supported platforms include Telegram, OneBot, Slack, Feishu (Lark), Discord, Misskey, and Satori.
    ChatUI support is planned for the future.

  3. Multimedia Message Delivery Tool
    By default, the main agent is provided with a send_message_to_user tool, which allows it to conveniently send images, files, audio, and other multimedia content directly to users (supported on the platforms listed above).

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

image image image

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Summary by Sourcery

引入一种可配置的子代理(subagent)编排模式,在该模式下主 LLM 通过动态交接(handoff)工具进行委派,而不是直接挂载所有工具。

New Features:

  • 新增子代理编排器后端,从配置中加载子代理定义并注册动态的 transfer_to_* 交接工具,支持可选的按子代理级别的 provider 覆盖。
  • 暴露 REST API 和 WebUI 的 SubAgent 页面,用于创建、编辑和持久化子代理配置,并为每个子代理分配工具。
  • 新增仅主 LLM 委派模式支持,在该模式下主 LLM 只看到交接工具,并通过路由系统提示(router system prompt)将任务路由到各子代理。

Enhancements:

  • 确保在执行交接时,在存在按子代理级别 chat provider 覆盖配置时能够正确遵守该配置。
  • 在核心生命周期启动过程中初始化和重新加载子代理编排器,按需清理并重新注册动态工具。
  • 提升 JWT 处理的健壮性,并确保基于 fetch 的仪表盘请求自动包含 Authorization 头。
  • 在仪表盘侧边栏中为新的 SubAgent 管理页面添加导航入口和路由。

Build:

  • monaco-editor 添加为仪表盘依赖,以在 UI 中提供更强大的编辑器能力。
Original summary in English

Summary by Sourcery

Introduce a configurable subagent orchestration mode where the main LLM delegates via dynamic handoff tools instead of mounting all tools directly.

New Features:

  • Add subagent orchestrator backend that loads subagent definitions from config and registers dynamic transfer_to_* handoff tools, including optional per-subagent provider overrides.
  • Expose REST APIs and a WebUI SubAgent page to create, edit, and persist subagent configurations and assign tools to each subagent.
  • Add support for main-LLM-only delegation mode where the main LLM sees only handoff tools and uses a router system prompt to route tasks to subagents.

Enhancements:

  • Ensure handoff execution respects per-subagent chat provider overrides when present.
  • Initialize and reload the subagent orchestrator during core lifecycle startup, cleaning up and re-registering dynamic tools as needed.
  • Improve JWT handling robustness and ensure fetch-based dashboard requests automatically include the Authorization header.
  • Add navigation entry and routing for the new SubAgent management page in the dashboard sidebar.

Build:

  • Add monaco-editor as a dashboard dependency for enhanced editor capabilities in the UI.

@auto-assign auto-assign bot requested review from Soulter and anka-afk January 26, 2026 14:27
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jan 26, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 3 个问题,并给出了一些整体性的反馈:

  • dashboard/src/main.ts 里的全局 window.fetch 包装器在第一个参数是 Request 对象时会丢失 methodbody 和其他 Request 属性;建议检测参数是否为 Request,如果是则原样转发(只克隆/增强 headers),以避免一些隐蔽的破坏性行为。
  • SubAgentOrchestrator.reload_from_config 中,通过 func_listremove_func 手动移除/添加工具绕过了 FunctionToolManager 的正常注册不变式;更安全的做法是暴露并使用专门的 API 来注册 HandoffTool 实例,从而保证它们仍然可被发现,并与其它工具保持一致。
  • SubAgentRoute.update_config 中保存子代理配置时,目前没有对 agent 名称做校验(例如非空、唯一性、允许字符集),这可能导致 transfer_to_* 工具名冲突或生成非法标识符;添加基本的服务端校验会让编排逻辑更健壮。
给 AI 代理的 Prompt
Please address the comments from this code review:

## Overall Comments
- `dashboard/src/main.ts` 里的全局 `window.fetch` 包装器在第一个参数是 `Request` 对象时会丢失 `method``body` 和其他 `Request` 属性;建议检测参数是否为 `Request`,如果是则原样转发(只克隆/增强 headers),以避免一些隐蔽的破坏性行为。
-`SubAgentOrchestrator.reload_from_config` 中,通过 `func_list``remove_func` 手动移除/添加工具绕过了 `FunctionToolManager` 的正常注册不变式;更安全的做法是暴露并使用专门的 API 来注册 `HandoffTool` 实例,从而保证它们仍然可被发现,并与其它工具保持一致。
-`SubAgentRoute.update_config` 中保存子代理配置时,目前没有对 agent 名称做校验(例如非空、唯一性、允许字符集),这可能导致 `transfer_to_*` 工具名冲突或生成非法标识符;添加基本的服务端校验会让编排逻辑更健壮。

## Individual Comments

### Comment 1
<location> `astrbot/dashboard/server.py:135-136` </location>
<code_context>
             r.status_code = 401
             return r
-        token = token.removeprefix("Bearer ")
+        # Be tolerant of different header casing / formatting.
+        token = token.strip().removeprefix("Bearer ").strip()
         try:
             payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
</code_context>

<issue_to_address>
**suggestion:** 尽管注释里说明要兼容不同的 header 大小写,但当前 JWT 提取对 `Bearer` 前缀仍然是大小写敏感的。

代码只会移除精确匹配的 "Bearer " 前缀,因此像 `authorization: bearer <token>``Authorization: BEARER <token>` 这样的 header 会失败,这与注释不符。建议在去前缀前先做归一化,例如:

```py
raw = token.strip()
if raw.lower().startswith("bearer "):
    raw = raw[7:]
token = raw.strip()
```

这样与注释中宣称的“对 header 大小写/格式更宽容”是一致的,也能避免来自非标准客户端的意外认证失败。

```suggestion
        # Be tolerant of different header casing / formatting.
        raw = token.strip()
        if raw.lower().startswith("bearer "):
            raw = raw[7:]
        token = raw.strip()
```
</issue_to_address>

### Comment 2
<location> `astrbot/dashboard/routes/subagent.py:93-105` </location>
<code_context>
+        UI can use this to build a multi-select list for subagent tool assignment.
+        """
+        try:
+            tool_mgr = self.core_lifecycle.provider_manager.llm_tools
+            tools_dict = []
+            for tool in tool_mgr.func_list:
+                tools_dict.append(
+                    {
+                        "name": tool.name,
+                        "description": tool.description,
+                        "parameters": tool.parameters,
+                        "active": tool.active,
+                        "handler_module_path": tool.handler_module_path,
+                    }
+                )
</code_context>

<issue_to_address>
**suggestion (bug_risk):** available-tools 接口会暴露 handoff 工具,使得子代理可以选择 `transfer_to_*` 工具,从而创建递归路由。

由于该接口返回 `func_list` 中的所有条目,它也会暴露 `HandoffTool` 实例(即 `transfer_to_*` 工具)。这让 UI 能够把 handoff 工具分配为子代理工具,从而可能形成令人困惑或递归的委托循环。为避免这种情况,请过滤掉这些工具——例如跳过 `handler_module_path == "core.subagent_orchestrator"` 的工具,或者如果可以的话,排除 `isinstance(tool, HandoffTool)` 的工具。

```suggestion
        try:
            tool_mgr = self.core_lifecycle.provider_manager.llm_tools
            tools_dict = []
            for tool in tool_mgr.func_list:
                # Skip handoff tools (e.g., transfer_to_*), which live in the subagent orchestrator.
                # These should not be assignable as subagent tools to avoid recursive delegation.
                if getattr(tool, "handler_module_path", None) == "core.subagent_orchestrator":
                    continue

                tools_dict.append(
                    {
                        "name": tool.name,
                        "description": tool.description,
                        "parameters": tool.parameters,
                        "active": tool.active,
                        "handler_module_path": tool.handler_module_path,
                    }
                )
```
</issue_to_address>

### Comment 3
<location> `astrbot/core/subagent_orchestrator.py:37` </location>
<code_context>
+        self._tool_mgr = tool_mgr
+        self._registered_handoff_names: set[str] = set()
+
+    def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
+        cfg = provider_settings.get("subagent_orchestrator", {})
+        enabled = bool(cfg.get("main_enable", False))
</code_context>

<issue_to_address>
**issue (complexity):** 建议重构 orchestrator,将工具注册/移除的职责委托给 `FunctionToolManager`,并通过 `SubAgentConfig` 统一子代理配置的解析逻辑,从而让 `reload_from_config` 更加简洁、聚焦。

可以通过加强与 `FunctionToolManager` 的交互,并真正把 `SubAgentConfig` 作为标准化后的配置类型来使用,从而显著简化当前实现。

### 1) 去掉 add/remove/append 的“舞蹈”

与其这样:

```python
self._tool_mgr.add_func(
    name=handoff.name,
    func_args=[...],
    desc=handoff.description,
    handler=handoff.handler,
)

self._tool_mgr.remove_func(handoff.name)
self._tool_mgr.func_list.append(handoff)
```

不如在 `FunctionToolManager` 中增加一个接受预构造工具并且负责替换的轻量 API:

```python
# in func_tool_manager.py
class FunctionToolManager:
    ...

    def add_tool(self, tool: FunctionTool) -> None:
        """Register a pre-constructed tool, replacing by name if it exists."""
        # remove any existing tool with same name
        self.func_list = [t for t in self.func_list if t.name != tool.name]
        self.func_list.append(tool)
```

这样你的 orchestrator 就变成单一、清晰的注册路径:

```python
# in SubAgentOrchestrator.reload_from_config
handoff = HandoffTool(agent=agent, description=public_description or None)
handoff.provider_id = provider_id
handoff.handler_module_path = "core.subagent_orchestrator"

self._tool_mgr.add_tool(handoff)
self._registered_handoff_names.add(handoff.name)
```

既保留了原有行为,又避免了对 `func_list` 的直接操作,以及“先 `add_func` 再立刻撤销”的模式。

### 2) 尽量避免手动状态跟踪

如果可以在工具对象中编码来源信息(你已经设置了 `handler_module_path`),就可以把移除逻辑集中到 manager 里,从而去掉 `_registered_handoff_names` 这套手动跟踪:

```python
# in func_tool_manager.py
class FunctionToolManager:
    ...

    def remove_by_module(self, module_path: str) -> None:
        self.func_list = [
            t for t in self.func_list
            if getattr(t, "handler_module_path", None) != module_path
        ]
```

然后在 orchestrator 中:

```python
def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
    cfg = provider_settings.get("subagent_orchestrator", {})
    enabled = bool(cfg.get("main_enable", False))

    # clean up previously registered tools from this orchestrator
    self._tool_mgr.remove_by_module("core.subagent_orchestrator")

    if not enabled:
        return

    ...
    self._tool_mgr.add_tool(handoff)
```

如果采用这种方式,就可以完全删除 `_registered_handoff_names`### 3) 使用 `SubAgentConfig` 简化解析逻辑

当前的 `reload_from_config` 混合了配置解析和编排逻辑。你已经有 `SubAgentConfig`,如果用它来承载标准化后的配置,方法会更短、更易读。

示例重构:

```python
@dataclass(frozen=True)
class SubAgentConfig:
    name: str
    instructions: str
    public_description: str
    tools: list[str]
    provider_id: str | None = None
    enabled: bool = True
```

解析辅助函数:

```python
def _parse_agent_config(self, item: dict[str, Any]) -> SubAgentConfig | None:
    if not isinstance(item, dict):
        return None
    if not item.get("enabled", True):
        return None

    name = str(item.get("name", "")).strip()
    if not name:
        return None

    instructions = str(item.get("system_prompt", "")).strip()
    public_description = str(item.get("public_description", "")).strip()

    provider_id = item.get("provider_id")
    if provider_id is not None:
        provider_id = str(provider_id).strip() or None

    tools = item.get("tools", [])
    if not isinstance(tools, list):
        tools = []
    tools = [str(t).strip() for t in tools if str(t).strip()]

    return SubAgentConfig(
        name=name,
        instructions=instructions,
        public_description=public_description,
        tools=tools,
        provider_id=provider_id,
        enabled=True,
    )
```

然后 `reload_from_config` 就可以聚焦在编排本身:

```python
def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
    cfg = provider_settings.get("subagent_orchestrator", {})
    enabled = bool(cfg.get("main_enable", False))

    self._tool_mgr.remove_by_module("core.subagent_orchestrator")
    if not enabled:
        return

    agents = cfg.get("agents", [])
    if not isinstance(agents, list):
        logger.warning("subagent_orchestrator.agents must be a list")
        return

    for item in agents:
        cfg_item = self._parse_agent_config(item)
        if cfg_item is None:
            continue

        agent = Agent[AstrAgentContext](
            name=cfg_item.name,
            instructions=cfg_item.instructions,
            tools=cfg_item.tools,
        )

        handoff = HandoffTool(
            agent=agent,
            description=cfg_item.public_description or None,
        )
        handoff.provider_id = cfg_item.provider_id
        handoff.handler_module_path = "core.subagent_orchestrator"

        self._tool_mgr.add_tool(handoff)
        logger.info(f"Registered subagent handoff tool: {handoff.name}")
```

这样可以让 `SubAgentConfig` 成为真正的抽象(而不是闲置代码),在保持行为不变的前提下,降低 `reload_from_config` 的认知负担。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这次评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进评审质量。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • The global window.fetch wrapper in dashboard/src/main.ts loses method, body, and other Request properties when the first argument is a Request object; consider detecting Request and forwarding it unchanged (only cloning/augmenting headers) to avoid subtle breakage.
  • In SubAgentOrchestrator.reload_from_config, manually removing/adding tools via func_list and remove_func bypasses FunctionToolManager's normal registration invariants; it would be safer to expose and use a dedicated API for registering HandoffTool instances so they remain discoverable and consistent with other tools.
  • When saving subagent config in SubAgentRoute.update_config, there is currently no validation of agent names (e.g., non-empty, uniqueness, allowed characters), which could lead to conflicting transfer_to_* tool names or invalid identifiers; adding basic server-side validation would make the orchestration more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The global `window.fetch` wrapper in `dashboard/src/main.ts` loses `method`, `body`, and other `Request` properties when the first argument is a `Request` object; consider detecting `Request` and forwarding it unchanged (only cloning/augmenting headers) to avoid subtle breakage.
- In `SubAgentOrchestrator.reload_from_config`, manually removing/adding tools via `func_list` and `remove_func` bypasses `FunctionToolManager`'s normal registration invariants; it would be safer to expose and use a dedicated API for registering `HandoffTool` instances so they remain discoverable and consistent with other tools.
- When saving subagent config in `SubAgentRoute.update_config`, there is currently no validation of agent names (e.g., non-empty, uniqueness, allowed characters), which could lead to conflicting `transfer_to_*` tool names or invalid identifiers; adding basic server-side validation would make the orchestration more robust.

## Individual Comments

### Comment 1
<location> `astrbot/dashboard/server.py:135-136` </location>
<code_context>
             r.status_code = 401
             return r
-        token = token.removeprefix("Bearer ")
+        # Be tolerant of different header casing / formatting.
+        token = token.strip().removeprefix("Bearer ").strip()
         try:
             payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
</code_context>

<issue_to_address>
**suggestion:** JWT extraction is still case-sensitive for the 'Bearer' prefix despite the comment about header casing.

The code only removes an exact "Bearer " prefix, so headers like `authorization: bearer <token>` or `Authorization: BEARER <token>` will fail despite the comment. Consider normalizing before stripping, e.g.:

```py
raw = token.strip()
if raw.lower().startswith("bearer "):
    raw = raw[7:]
token = raw.strip()
```

This matches the stated tolerance for header casing/formatting and avoids unexpected auth failures from non-standard clients.

```suggestion
        # Be tolerant of different header casing / formatting.
        raw = token.strip()
        if raw.lower().startswith("bearer "):
            raw = raw[7:]
        token = raw.strip()
```
</issue_to_address>

### Comment 2
<location> `astrbot/dashboard/routes/subagent.py:93-105` </location>
<code_context>
+        UI can use this to build a multi-select list for subagent tool assignment.
+        """
+        try:
+            tool_mgr = self.core_lifecycle.provider_manager.llm_tools
+            tools_dict = []
+            for tool in tool_mgr.func_list:
+                tools_dict.append(
+                    {
+                        "name": tool.name,
+                        "description": tool.description,
+                        "parameters": tool.parameters,
+                        "active": tool.active,
+                        "handler_module_path": tool.handler_module_path,
+                    }
+                )
</code_context>

<issue_to_address>
**suggestion (bug_risk):** available-tools endpoint exposes handoff tools, allowing subagents to select transfer_to_* tools and create recursive routing.

Because this endpoint returns all entries in `func_list`, it also exposes `HandoffTool` instances (the `transfer_to_*` tools). That lets the UI assign handoff tools as subagent tools, enabling confusing or recursive delegation loops. To avoid this, filter these out—for example, skip tools with `handler_module_path == "core.subagent_orchestrator"` or, if possible, exclude `isinstance(tool, HandoffTool)`.

```suggestion
        try:
            tool_mgr = self.core_lifecycle.provider_manager.llm_tools
            tools_dict = []
            for tool in tool_mgr.func_list:
                # Skip handoff tools (e.g., transfer_to_*), which live in the subagent orchestrator.
                # These should not be assignable as subagent tools to avoid recursive delegation.
                if getattr(tool, "handler_module_path", None) == "core.subagent_orchestrator":
                    continue

                tools_dict.append(
                    {
                        "name": tool.name,
                        "description": tool.description,
                        "parameters": tool.parameters,
                        "active": tool.active,
                        "handler_module_path": tool.handler_module_path,
                    }
                )
```
</issue_to_address>

### Comment 3
<location> `astrbot/core/subagent_orchestrator.py:37` </location>
<code_context>
+        self._tool_mgr = tool_mgr
+        self._registered_handoff_names: set[str] = set()
+
+    def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
+        cfg = provider_settings.get("subagent_orchestrator", {})
+        enabled = bool(cfg.get("main_enable", False))
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the orchestrator to delegate tool registration/removal to `FunctionToolManager` and normalize subagent parsing through `SubAgentConfig` for a cleaner, more focused `reload_from_config`.

You can simplify this substantially by tightening the interaction with `FunctionToolManager` and actually using `SubAgentConfig` as your normalized config type.

### 1) Remove the add/remove/append dance

Instead of:

```python
self._tool_mgr.add_func(
    name=handoff.name,
    func_args=[...],
    desc=handoff.description,
    handler=handoff.handler,
)

self._tool_mgr.remove_func(handoff.name)
self._tool_mgr.func_list.append(handoff)
```

add a small API to `FunctionToolManager` that accepts a pre‑built tool and handles replacement:

```python
# in func_tool_manager.py
class FunctionToolManager:
    ...

    def add_tool(self, tool: FunctionTool) -> None:
        """Register a pre-constructed tool, replacing by name if it exists."""
        # remove any existing tool with same name
        self.func_list = [t for t in self.func_list if t.name != tool.name]
        self.func_list.append(tool)
```

Then your orchestrator becomes a single, clear registration path:

```python
# in SubAgentOrchestrator.reload_from_config
handoff = HandoffTool(agent=agent, description=public_description or None)
handoff.provider_id = provider_id
handoff.handler_module_path = "core.subagent_orchestrator"

self._tool_mgr.add_tool(handoff)
self._registered_handoff_names.add(handoff.name)
```

This keeps all behavior but avoids reaching into `func_list` and calling `add_func` only to immediately undo it.

### 2) Avoid manual tracking where possible

If you can encode the origin in the tools (you already set `handler_module_path`), you can centralize removal in the manager and drop the `_registered_handoff_names` set:

```python
# in func_tool_manager.py
class FunctionToolManager:
    ...

    def remove_by_module(self, module_path: str) -> None:
        self.func_list = [
            t for t in self.func_list
            if getattr(t, "handler_module_path", None) != module_path
        ]
```

Then in the orchestrator:

```python
def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
    cfg = provider_settings.get("subagent_orchestrator", {})
    enabled = bool(cfg.get("main_enable", False))

    # clean up previously registered tools from this orchestrator
    self._tool_mgr.remove_by_module("core.subagent_orchestrator")

    if not enabled:
        return

    ...
    self._tool_mgr.add_tool(handoff)
```

If you adopt this, you can remove `_registered_handoff_names` completely.

### 3) Use `SubAgentConfig` to simplify parsing

Right now `reload_from_config` mixes config parsing with orchestration. You already have `SubAgentConfig`; using it makes the method shorter and easier to read.

Example refactor:

```python
@dataclass(frozen=True)
class SubAgentConfig:
    name: str
    instructions: str
    public_description: str
    tools: list[str]
    provider_id: str | None = None
    enabled: bool = True
```

Parsing helper:

```python
def _parse_agent_config(self, item: dict[str, Any]) -> SubAgentConfig | None:
    if not isinstance(item, dict):
        return None
    if not item.get("enabled", True):
        return None

    name = str(item.get("name", "")).strip()
    if not name:
        return None

    instructions = str(item.get("system_prompt", "")).strip()
    public_description = str(item.get("public_description", "")).strip()

    provider_id = item.get("provider_id")
    if provider_id is not None:
        provider_id = str(provider_id).strip() or None

    tools = item.get("tools", [])
    if not isinstance(tools, list):
        tools = []
    tools = [str(t).strip() for t in tools if str(t).strip()]

    return SubAgentConfig(
        name=name,
        instructions=instructions,
        public_description=public_description,
        tools=tools,
        provider_id=provider_id,
        enabled=True,
    )
```

Then `reload_from_config` focuses on orchestration:

```python
def reload_from_config(self, provider_settings: dict[str, Any]) -> None:
    cfg = provider_settings.get("subagent_orchestrator", {})
    enabled = bool(cfg.get("main_enable", False))

    self._tool_mgr.remove_by_module("core.subagent_orchestrator")
    if not enabled:
        return

    agents = cfg.get("agents", [])
    if not isinstance(agents, list):
        logger.warning("subagent_orchestrator.agents must be a list")
        return

    for item in agents:
        cfg_item = self._parse_agent_config(item)
        if cfg_item is None:
            continue

        agent = Agent[AstrAgentContext](
            name=cfg_item.name,
            instructions=cfg_item.instructions,
            tools=cfg_item.tools,
        )

        handoff = HandoffTool(
            agent=agent,
            description=cfg_item.public_description or None,
        )
        handoff.provider_id = cfg_item.provider_id
        handoff.handler_module_path = "core.subagent_orchestrator"

        self._tool_mgr.add_tool(handoff)
        logger.info(f"Registered subagent handoff tool: {handoff.name}")
```

This makes `SubAgentConfig` a real abstraction (not dead code), keeps behavior intact, and reduces the cognitive load in `reload_from_config`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot dosubot bot added area:core The bug / feature is about astrbot's core, backend area:webui The bug / feature is about webui(dashboard) of astrbot. labels Jan 26, 2026
Copy link
Member

@Soulter Soulter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just made some quick reviews

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感觉相关逻辑可以直接放到 tool_manager 里面,用一个方法代替。

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jan 29, 2026
- Implemented proactive cron job tools in InternalAgentSubStage for scheduling tasks.
- Created SendMessageToUserTool for sending messages to users based on cron job triggers.
- Added CreateActiveCronTool, DeleteCronJobTool, and ListCronJobsTool for cron job management.
- Introduced CronRoute for handling cron job API requests in the dashboard.
- Developed CronJobPage.vue for managing cron jobs in the dashboard UI.
- Updated SubAgentPage.vue to include persona selection for subagents.
@Soulter Soulter changed the title feat:增加用户在webui创建和配置subagent的功能 feat: implemented proactive agents and subagents orchestrator Jan 31, 2026
- Updated FunctionToolExecutor to improve background task handling and integrate new system prompts for proactive agents.
- Enhanced MainAgentBuildConfig with additional configuration options for tool management and context handling.
- Introduced new system prompts for proactive agents triggered by cron jobs and background tasks to improve user interaction.
- Refactored cron job management to utilize ProviderRequest for better context management and tool integration.
- Renamed cron job tools for clarity, changing "create_cron_job" to "create_future_task" and similar adjustments for consistency.
- Improved error handling and logging for cron job execution and agent responses.
- Added support for image captioning and persona management in agent requests.
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Feb 1, 2026
@Soulter Soulter merged commit e35a604 into AstrBotDevs:master Feb 1, 2026
6 checks passed
@dosubot
Copy link

dosubot bot commented Feb 1, 2026

Related Documentation

No published documentation to review for changes on this repository.

Write your first living document

How did I do? Any feedback?  Join Discord

@Soulter Soulter changed the title feat: implemented proactive agents and subagents orchestrator feat: Proactive Agents and Subagents Orchestrator Feb 1, 2026
astrbot-doc-agent bot pushed a commit to AstrBotDevs/AstrBot-docs that referenced this pull request Feb 2, 2026
@astrbot-doc-agent
Copy link

已为该 PR 生成文档更新 PR(待人工审核):
AstrBotDevs/AstrBot-docs#112


AI 改动摘要:

Updated the documentation to include the new SubAgent Orchestration and Proactive Agent features introduced in PR #4697.

Documentation Changes

  • SubAgent Orchestration:
    • Created zh/use/subagent.md and en/use/subagent.md.
    • Explained the motivation (reducing prompt bloat, improving accuracy).
    • Detailed the delegation mechanism via transfer_to_* tools.
    • Provided configuration steps for the WebUI and best practices.
  • Proactive Tasks & Multimedia Delivery:
    • Created zh/use/proactive-agent.md and en/use/proactive-agent.md.
    • Documented the FutureTask (Scheduled Tasks) system, including self-wakeup and task feedback.
    • Listed supported platforms (Telegram, OneBot, Slack, Lark, Discord, Misskey, Satori).
    • Documented the send_message_to_user tool for direct multimedia delivery.

Sidebar Updates

  • Updated .vitepress/config.mjs to include:
    • SubAgent 编排 / SubAgent Orchestration
    • 主动任务 (FutureTask) / Proactive Tasks
    • These are now visible under the "Usage" (使用) section in both Chinese and English locales.

Soulter added a commit to AstrBotDevs/AstrBot-docs that referenced this pull request Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend area:webui The bug / feature is about webui(dashboard) of astrbot. lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants