Skip to content

Commit 8a8ec49

Browse files
feat: supports spawn subagent as a background task that not block the main agent workflow (#5081)
* feat:为subagent添加后台任务参数 * ruff * fix: update terminology from 'handoff mission' to 'background task' and refactor related logic * fix: update terminology from 'background_mission' to 'background_task' in HandoffTool and related logic * fix(HandoffTool): update background_task description for clarity on usage --------- Co-authored-by: Soulter <905617992@qq.com>
1 parent 02c1443 commit 8a8ec49

File tree

2 files changed

+145
-21
lines changed

2 files changed

+145
-21
lines changed

astrbot/core/agent/handoff.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ def default_parameters(self) -> dict:
4444
"type": "string",
4545
"description": "The input to be handed off to another agent. This should be a clear and concise request or task.",
4646
},
47+
"background_task": {
48+
"type": "boolean",
49+
"description": (
50+
"Defaults to false. "
51+
"Set to true if the task may take noticeable time, involves external tools, or the user does not need to wait. "
52+
"Use false only for quick, immediate tasks."
53+
),
54+
},
4755
},
4856
}
4957

astrbot/core/astr_agent_tool_exec.py

Lines changed: 137 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ async def execute(cls, tool, run_context, **tool_args):
4545
4646
"""
4747
if isinstance(tool, HandoffTool):
48+
is_bg = tool_args.pop("background_task", False)
49+
if is_bg:
50+
async for r in cls._execute_handoff_background(
51+
tool, run_context, **tool_args
52+
):
53+
yield r
54+
return
4855
async for r in cls._execute_handoff(tool, run_context, **tool_args):
4956
yield r
5057
return
@@ -147,19 +154,93 @@ async def _execute_handoff(
147154
)
148155

149156
@classmethod
150-
async def _execute_background(
157+
async def _execute_handoff_background(
151158
cls,
152-
tool: FunctionTool,
159+
tool: HandoffTool,
160+
run_context: ContextWrapper[AstrAgentContext],
161+
**tool_args,
162+
):
163+
"""Execute a handoff as a background task.
164+
165+
Immediately yields a success response with a task_id, then runs
166+
the subagent asynchronously. When the subagent finishes, a
167+
``CronMessageEvent`` is created so the main LLM can inform the
168+
user of the result – the same pattern used by
169+
``_execute_background`` for regular background tasks.
170+
"""
171+
task_id = uuid.uuid4().hex
172+
173+
async def _run_handoff_in_background() -> None:
174+
try:
175+
await cls._do_handoff_background(
176+
tool=tool,
177+
run_context=run_context,
178+
task_id=task_id,
179+
**tool_args,
180+
)
181+
except Exception as e: # noqa: BLE001
182+
logger.error(
183+
f"Background handoff {task_id} ({tool.name}) failed: {e!s}",
184+
exc_info=True,
185+
)
186+
187+
asyncio.create_task(_run_handoff_in_background())
188+
189+
text_content = mcp.types.TextContent(
190+
type="text",
191+
text=(
192+
f"Background task dedicated to subagent '{tool.agent.name}' submitted. task_id={task_id}. "
193+
f"The subagent '{tool.agent.name}' is working on the task on hehalf you. "
194+
f"You will be notified when it finishes."
195+
),
196+
)
197+
yield mcp.types.CallToolResult(content=[text_content])
198+
199+
@classmethod
200+
async def _do_handoff_background(
201+
cls,
202+
tool: HandoffTool,
153203
run_context: ContextWrapper[AstrAgentContext],
154204
task_id: str,
155205
**tool_args,
156206
) -> None:
157-
from astrbot.core.astr_main_agent import (
158-
MainAgentBuildConfig,
159-
_get_session_conv,
160-
build_main_agent,
207+
"""Run the subagent handoff and, on completion, wake the main agent."""
208+
result_text = ""
209+
try:
210+
async for r in cls._execute_handoff(tool, run_context, **tool_args):
211+
if isinstance(r, mcp.types.CallToolResult):
212+
for content in r.content:
213+
if isinstance(content, mcp.types.TextContent):
214+
result_text += content.text + "\n"
215+
except Exception as e:
216+
result_text = (
217+
f"error: Background task execution failed, internal error: {e!s}"
218+
)
219+
220+
event = run_context.context.event
221+
222+
await cls._wake_main_agent_for_background_result(
223+
run_context=run_context,
224+
task_id=task_id,
225+
tool_name=tool.name,
226+
result_text=result_text,
227+
tool_args=tool_args,
228+
note=(
229+
event.get_extra("background_note")
230+
or f"Background task for subagent '{tool.agent.name}' finished."
231+
),
232+
summary_name=f"Dedicated to subagent `{tool.agent.name}`",
233+
extra_result_fields={"subagent_name": tool.agent.name},
161234
)
162235

236+
@classmethod
237+
async def _execute_background(
238+
cls,
239+
tool: FunctionTool,
240+
run_context: ContextWrapper[AstrAgentContext],
241+
task_id: str,
242+
**tool_args,
243+
) -> None:
163244
# run the tool
164245
result_text = ""
165246
try:
@@ -178,20 +259,52 @@ async def _execute_background(
178259
)
179260

180261
event = run_context.context.event
181-
ctx = run_context.context.context
182262

183-
note = (
184-
event.get_extra("background_note")
185-
or f"Background task {tool.name} finished."
263+
await cls._wake_main_agent_for_background_result(
264+
run_context=run_context,
265+
task_id=task_id,
266+
tool_name=tool.name,
267+
result_text=result_text,
268+
tool_args=tool_args,
269+
note=(
270+
event.get_extra("background_note")
271+
or f"Background task {tool.name} finished."
272+
),
273+
summary_name=tool.name,
186274
)
187-
extras = {
188-
"background_task_result": {
189-
"task_id": task_id,
190-
"tool_name": tool.name,
191-
"result": result_text or "",
192-
"tool_args": tool_args,
193-
}
275+
276+
@classmethod
277+
async def _wake_main_agent_for_background_result(
278+
cls,
279+
run_context: ContextWrapper[AstrAgentContext],
280+
*,
281+
task_id: str,
282+
tool_name: str,
283+
result_text: str,
284+
tool_args: dict[str, T.Any],
285+
note: str,
286+
summary_name: str,
287+
extra_result_fields: dict[str, T.Any] | None = None,
288+
) -> None:
289+
from astrbot.core.astr_main_agent import (
290+
MainAgentBuildConfig,
291+
_get_session_conv,
292+
build_main_agent,
293+
)
294+
295+
event = run_context.context.event
296+
ctx = run_context.context.context
297+
298+
task_result = {
299+
"task_id": task_id,
300+
"tool_name": tool_name,
301+
"result": result_text or "",
302+
"tool_args": tool_args,
194303
}
304+
if extra_result_fields:
305+
task_result.update(extra_result_fields)
306+
extras = {"background_task_result": task_result}
307+
195308
session = MessageSession.from_str(event.unified_msg_origin)
196309
cron_event = CronMessageEvent(
197310
context=ctx,
@@ -222,8 +335,11 @@ async def _execute_background(
222335
)
223336
req.prompt = (
224337
"Proceed according to your system instructions. "
225-
"Output using same language as previous conversation."
226-
" After completing your task, summarize and output your actions and results."
338+
"Output using same language as previous conversation. "
339+
"If you need to deliver the result to the user immediately, "
340+
"you MUST use `send_message_to_user` tool to send the message directly to the user, "
341+
"otherwise the user will not see the result. "
342+
"After completing your task, summarize and output your actions and results. "
227343
)
228344
if not req.func_tool:
229345
req.func_tool = ToolSet()
@@ -233,7 +349,7 @@ async def _execute_background(
233349
event=cron_event, plugin_context=ctx, config=config, req=req
234350
)
235351
if not result:
236-
logger.error("Failed to build main agent for background task job.")
352+
logger.error(f"Failed to build main agent for background task {tool_name}.")
237353
return
238354

239355
runner = result.agent_runner
@@ -243,7 +359,7 @@ async def _execute_background(
243359
llm_resp = runner.get_final_llm_resp()
244360
task_meta = extras.get("background_task_result", {})
245361
summary_note = (
246-
f"[BackgroundTask] {task_meta.get('tool_name', tool.name)} "
362+
f"[BackgroundTask] {summary_name} "
247363
f"(task_id={task_meta.get('task_id', task_id)}) finished. "
248364
f"Result: {task_meta.get('result') or result_text or 'no content'}"
249365
)

0 commit comments

Comments
 (0)