Skip to content

Conversation

@advent259141
Copy link
Member

@advent259141 advent259141 commented Jan 26, 2026

Motivations / 动机

目前框架采取将所有tool全部挂载在主llm上,主llm既负责与用户对话,又要负责组织和调用大量工具。若tool数量多,容易导致prompt长度爆炸以及llm调用失误等情况。

若用户可以创建subagent,每个subagent负责挂载一定的工具,主llm只需负责聊天以及向subagent委派任务即可,具体工具由subagent负责调用。这样可以极大得减少主llm的prompt长度以及执行任务时的出错概率。

Modifications / 改动点

1.在webui上增加subagent编排,用户可在页面上创建subagent,指定该subagent使用的工具,subagent的systemprompt以及在主llm处subagent的描述,以及自定义subagent使用的模型

2.支持开关subagent功能,开启时,主 LLM 仅负责对话与委派,只会看到 transfer_to_* 这类委派工具;需要调用工具时,会把任务交给对应 SubAgent 执行。SubAgent 负责真正的工具调用与结果整理,并把结论回传给主 LLM。关闭时则像原来一样把全部工具挂载到主llm上。

3.注册的subagent会自动成为一个名为transfer_to_(subagent名称)的tool,复用框架原有handleoff功能,主llm调用transfer_to_(agent名称)即将任务委派给指定subagent。

  • 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.

Comment on lines 135 to 136
# Be tolerant of different header casing / formatting.
token = token.strip().removeprefix("Bearer ").strip()
Copy link
Contributor

Choose a reason for hiding this comment

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

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

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

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

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

Suggested change
# Be tolerant of different header casing / formatting.
token = token.strip().removeprefix("Bearer ").strip()
# Be tolerant of different header casing / formatting.
raw = token.strip()
if raw.lower().startswith("bearer "):
raw = raw[7:]
token = raw.strip()
Original comment in English

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.:

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.

Suggested change
# Be tolerant of different header casing / formatting.
token = token.strip().removeprefix("Bearer ").strip()
# Be tolerant of different header casing / formatting.
raw = token.strip()
if raw.lower().startswith("bearer "):
raw = raw[7:]
token = raw.strip()

@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

return r
token = token.removeprefix("Bearer ")
# Be tolerant of different header casing / formatting.
token = token.strip().removeprefix("Bearer ").strip()
Copy link
Member

Choose a reason for hiding this comment

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

这一行就按原来那样吧

name=f"transfer_to_{agent.name}",
parameters=parameters or self.default_parameters(),
description=agent.instructions or self.default_description(agent.name),
description=description,
Copy link
Member

Choose a reason for hiding this comment

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

这里直接这样改吧:

  1. 新增可为None的关键字参数,"tool_description",然后这里就 tool_description or self.default_description(agent.name)



@dataclass(frozen=True)
class SubAgentConfig:
Copy link
Member

Choose a reason for hiding this comment

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

好像没有使用过可以去掉

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 里面,用一个方法代替。

},
# SubAgent orchestrator mode: the main LLM only delegates tasks to subagents
# (via transfer_to_{agent} tools). Domain tools are mounted on subagents.
"subagent_orchestrator": {
Copy link
Member

Choose a reason for hiding this comment

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

感觉可以和provider_settings平级

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. size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants