Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
44d2364
feat: enhanced subagent
elecvoid243 Mar 26, 2026
01b78d7
Merge branch 'master' of https://github.com/elecvoid243/AstrBot
elecvoid243 Mar 26, 2026
c70d0a5
no message
elecvoid243 Mar 26, 2026
f195754
no message
elecvoid243 Mar 27, 2026
3b005da
no message
elecvoid243 Mar 28, 2026
2b67fd3
no message
elecvoid243 Mar 28, 2026
9d38048
formatting
elecvoid243 Mar 28, 2026
378a6e6
no message
elecvoid243 Mar 28, 2026
2d33670
no message
elecvoid243 Mar 28, 2026
5e45821
test
elecvoid243 Mar 28, 2026
ad34dce
no message
elecvoid243 Mar 28, 2026
2389f27
Merge branch 'master' into master
elecvoid243 Mar 29, 2026
549ff85
no message
elecvoid243 Mar 29, 2026
38a6b2e
Merge branch 'master' of https://github.com/elecvoid243/AstrBot
elecvoid243 Mar 29, 2026
58d5141
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Mar 29, 2026
a9cf8d3
添加Reset工具
elecvoid243 Mar 29, 2026
923a58e
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Mar 29, 2026
bf88368
结构化的共享上下文;更新subagent后台任务逻辑
elecvoid243 Mar 30, 2026
97cfedf
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Mar 30, 2026
9f7e208
主动等待机制;Prompt优化
elecvoid243 Apr 2, 2026
163ce55
Merge branch 'master' of https://github.com/elecvoid243/AstrBot
elecvoid243 Apr 2, 2026
ee0a85e
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 2, 2026
fb39593
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 5, 2026
06cb7ea
subagent运行状态监控
elecvoid243 Apr 6, 2026
a579ac3
Merge branch 'master' into master
elecvoid243 Apr 6, 2026
3f02d54
rollback local.py
elecvoid243 Apr 6, 2026
03deaac
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 7, 2026
d3cee00
merge
elecvoid243 Apr 7, 2026
47b61ec
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 8, 2026
7dbfb6e
后台subagent任务:若主agent已结束,回退到原始唤醒机制
elecvoid243 Apr 9, 2026
6a6d030
移除logger;prompt简化
elecvoid243 Apr 11, 2026
cea5611
subagent历史带工具调用信息
elecvoid243 Apr 11, 2026
a9dd635
Merge branch 'master' into master
elecvoid243 Apr 11, 2026
1222aae
formatting
elecvoid243 Apr 11, 2026
25200f2
no message
elecvoid243 Apr 11, 2026
603072c
no message
elecvoid243 Apr 11, 2026
2e2a60b
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 12, 2026
65bb50c
no message
elecvoid243 Apr 13, 2026
7d567b6
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 13, 2026
4366d37
Merge branch 'master' of https://github.com/elecvoid243/AstrBot
elecvoid243 Apr 13, 2026
7490ea3
添加subagent超时保护(总时长)
elecvoid243 Apr 14, 2026
cd912b3
Merge branch 'AstrBotDevs:master' into master
elecvoid243 Apr 14, 2026
f4fabc2
Merge branch 'master' of https://github.com/elecvoid243/AstrBot
elecvoid243 Apr 14, 2026
7ef86e4
formatting
elecvoid243 Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 129 additions & 12 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ async def reset(
fallback_providers: list[Provider] | None = None,
tool_result_overflow_dir: str | None = None,
read_tool: FunctionTool | None = None,
# external abort signal for SubAgent control (does not affect main agent)
external_abort_signal: asyncio.Event | None = None,
**kwargs: T.Any,
) -> None:
self.req = request
Expand Down Expand Up @@ -267,7 +269,12 @@ async def reset(
self.agent_hooks = agent_hooks
self.run_context = run_context
self._aborted = False
self._abort_signal = asyncio.Event()
# Use external abort signal if provided (for SubAgent), otherwise create new one
self._abort_signal = (
external_abort_signal
if external_abort_signal is not None
else asyncio.Event()
)
self._pending_follow_ups: list[FollowUpTicket] = []
self._follow_up_seq = 0
self._last_tool_name: str | None = None
Expand Down Expand Up @@ -917,15 +924,22 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
if not req.func_tool:
return

if (
self.tool_schema_mode == "skills_like"
and self._skill_like_raw_tool_set
):
# in 'skills_like' mode, raw.func_tool is light schema, does not have handler
# so we need to get the tool from the raw tool set
func_tool = self._skill_like_raw_tool_set.get_tool(func_tool_name)
else:
func_tool = req.func_tool.get_tool(func_tool_name)
# Prefer dynamic tools when available
func_tool = self._resolve_dynamic_subagent_tool(func_tool_name)

# If not found in dynamic tools, check regular tool sets
if func_tool is None:
if (
self.tool_schema_mode == "skills_like"
and self._skill_like_raw_tool_set
):
# in 'skills_like' mode, raw.func_tool is light schema, does not have handler
# so we need to get the tool from the raw tool set
func_tool = self._skill_like_raw_tool_set.get_tool(
func_tool_name
)
else:
func_tool = req.func_tool.get_tool(func_tool_name)

logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")

Expand Down Expand Up @@ -1045,6 +1059,12 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
"The tool has returned a data type that is not supported."
)
if result_parts:
result_content = "\n\n".join(result_parts)
# Check for dynamic tool creation marker
self._maybe_register_dynamic_tool_from_result(
result_content
)

inline_result = "\n\n".join(result_parts)
inline_result = await self._materialize_large_tool_result(
tool_call_id=func_tool_id,
Expand Down Expand Up @@ -1241,8 +1261,18 @@ def request_stop(self) -> None:
self._abort_signal.set()

def _is_stop_requested(self) -> bool:
# Check if abort signal is set
return self._abort_signal.is_set()

def is_abort_signal_set(self) -> bool:
"""检查 abort_signal 是否已被设置(用于外部控制)"""
return self._abort_signal.is_set()

def reset_abort_signal(self) -> None:
"""重置 abort_signal(用于复用 runner)"""
self._abort_signal.clear()
self._aborted = False

def was_aborted(self) -> bool:
return self._aborted

Expand All @@ -1252,15 +1282,37 @@ def get_final_llm_resp(self) -> LLMResponse | None:
async def _finalize_aborted_step(
self,
llm_resp: LLMResponse | None = None,
manual_stop: bool = False,
) -> AgentResponse:
logger.info("Agent execution was requested to stop by user.")
"""终结被中断的步骤

Args:
llm_resp: LLM响应对象
manual_stop: 是否是主Agent手动停止SubAgent(True时使用不同的消息提示)
"""
if manual_stop:
logger.info("SubAgent execution was manually stopped by main agent.")
else:
logger.info("Agent execution was requested to stop by user.")

if llm_resp is None:
llm_resp = LLMResponse(role="assistant", completion_text="")

# 根据停止类型选择不同的消息
if llm_resp.role != "assistant":
if manual_stop:
# SubAgent被主Agent手动停止,使用更简洁的消息
interruption_msg = (
"[SYSTEM: SubAgent was manually stopped by main agent. "
"Partial output before interruption is preserved.]"
)
else:
interruption_msg = self.USER_INTERRUPTION_MESSAGE
llm_resp = LLMResponse(
role="assistant",
completion_text=self.USER_INTERRUPTION_MESSAGE,
completion_text=interruption_msg,
)

self.final_llm_resp = llm_resp
self._aborted = True
self._transition_state(AgentState.DONE)
Expand Down Expand Up @@ -1337,3 +1389,68 @@ async def _iter_tool_executor_results(
abort_task.cancel()
with suppress(asyncio.CancelledError):
await abort_task

def _resolve_dynamic_subagent_tool(self, func_tool_name: str):
run_context_context = getattr(self.run_context, "context", None)
if run_context_context is None:
return None

event = getattr(run_context_context, "event", None)
if event is None:
return None

session_id = getattr(event, "unified_msg_origin", None)
if not session_id:
return None

try:
from astrbot.core.dynamic_subagent_manager import DynamicSubAgentManager

dynamic_handoffs = DynamicSubAgentManager.get_handoff_tools_for_session(
session_id
)
except Exception:
return None

for h in dynamic_handoffs:
if h.name == func_tool_name or f"transfer_to_{h.name}" == func_tool_name:
return h
return None

def _maybe_register_dynamic_tool_from_result(self, result_content: str) -> None:
if not result_content.startswith("__DYNAMIC_TOOL_CREATED__:"):
return

parts = result_content.split(":", 3)
if len(parts) < 4:
return

new_tool_name = parts[1]
new_tool_obj_name = parts[2]
logger.info(f"[EnhancedSubAgent] Tool created: {new_tool_name}")

run_context_context = getattr(self.run_context, "context", None)
event = (
getattr(run_context_context, "event", None) if run_context_context else None
)
session_id = getattr(event, "unified_msg_origin", None) if event else None
if not session_id:
return

try:
from astrbot.core.dynamic_subagent_manager import DynamicSubAgentManager

handoffs = DynamicSubAgentManager.get_handoff_tools_for_session(session_id)
except Exception as e:
logger.warning(f"[EnhancedSubAgent] Failed to load dynamic handoffs: {e}")
return

for handoff in handoffs:
if (
handoff.name == new_tool_obj_name
or handoff.name == new_tool_name.replace("transfer_to_", "")
):
if self.req.func_tool:
self.req.func_tool.add_tool(handoff)
logger.info(f"[EnhancedSubAgent] Added {handoff.name} to func_tool set")
break
4 changes: 3 additions & 1 deletion astrbot/core/astr_agent_context.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from pydantic import Field
from pydantic.dataclasses import dataclass

Expand All @@ -14,7 +16,7 @@ class AstrAgentContext:
"""The star context instance"""
event: AstrMessageEvent
"""The message event associated with the agent context."""
extra: dict[str, str] = Field(default_factory=dict)
extra: dict[str, Any] = Field(default_factory=dict)
"""Customized extra data."""


Expand Down
Loading