ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,29 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import sys
from aworld.core.factory import Factory
from aworld.logs.util import logger
class HandlerManager(Factory):
def __init__(self, type_name: str = None):
super(HandlerManager, self).__init__(type_name)
def __call__(self, name: str, asyn: bool = False, runner: 'TaskRunner' = None, **kwargs):
if name is None or runner is None:
raise ValueError("handler name or runner instance is None")
try:
if name in self._cls:
act = self._cls[name](runner)
else:
raise RuntimeError("The handler was not registered.\nPlease confirm the package has been imported.")
except Exception:
err = sys.exc_info()
logger.warning(f"Failed to create handler with name {name}:\n{err[1]}")
act = None
return act
HandlerFactory = HandlerManager("hook_type")
@@ -0,0 +1,856 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import json
import time
import traceback
import aworld.trace as trace
from typing import List, Dict, Any, Tuple
from aworld.config.conf import ToolConfig
from aworld.core.agent.base import is_agent
from aworld.agents.llm_agent import Agent
from aworld.core.common import Observation, ActionModel, ActionResult
from aworld.core.context.base import Context
from aworld.core.event.base import Message, ToolMessage, AgentMessage
from aworld.core.tool.base import ToolFactory, Tool, AsyncTool
from aworld.core.tool.tool_desc import is_tool_by_name
from aworld.core.task import Task, TaskResponse
from aworld.logs.util import logger, color_log, Color, trace_logger
from aworld.models.model_response import ToolCall
from aworld.output.base import StepOutput, ToolResultOutput
from aworld.runners.task_runner import TaskRunner
from aworld.runners.utils import endless_detect
from aworld.sandbox import Sandbox
from aworld.tools.utils import build_observation
from aworld.utils.common import override_in_subclass
from aworld.utils.serialized_util import NumpyEncoder
def action_result_transform(message: Message, sandbox: Sandbox) -> Tuple[Observation, float, bool, bool, dict]:
action_results = message.payload
result: ActionResult = action_results[-1]
# ignore image, dom_tree attribute, need to process them from action_results in the agent.
return build_observation(container_id=sandbox.sandbox_id,
observer=result.tool_name,
ability=result.action_name,
content=result.content,
action_result=action_results), 1.0, result.is_done, result.is_done, {}
class WorkflowRunner(TaskRunner):
def __init__(self, task: Task, *args, **kwargs):
super().__init__(task=task, *args, **kwargs)
async def do_run(self, context: Context = None) -> TaskResponse:
self.max_steps = self.conf.get("max_steps", 100)
resp = await self._do_run(context)
self._task_response = resp
return resp
async def _do_run(self, context: Context = None) -> TaskResponse:
"""Multi-agent sequence general process workflow.
NOTE: Use the agent's finished state(no tool calls) to control the inner loop.
Args:
observation: Observation based on env
info: Extend info by env
"""
observation = self.observation
if not observation:
raise RuntimeError("no observation, check run process")
start = time.time()
msg = None
response = None
# Use trace.span to record the entire task execution process
with trace.span(f"task_execution_{self.task.id}", attributes={
"task_id": self.task.id,
"task_name": self.task.name,
"start_time": start
}) as task_span:
try:
response = await self._common_process(task_span)
except Exception as err:
logger.error(f"Runner run failed, err is {traceback.format_exc()}")
finally:
if not self.task.is_sub_task:
logger.info(f"FINISHED|call_driven_runner|mark_completed|{self.task.id}")
await self.outputs.mark_completed()
color_log(f"task token usage: {self.context.token_usage}",
color=Color.pink,
logger_=trace_logger)
for _, tool in self.tools.items():
if isinstance(tool, AsyncTool):
await tool.close()
else:
tool.close()
task_span.set_attributes({
"end_time": time.time(),
"duration": time.time() - start,
"error": msg
})
# todo sandbox cleanup
if self.swarm and hasattr(self.swarm, 'agents') and self.swarm.agents:
for agent_name, agent in self.swarm.agents.items():
try:
if hasattr(agent, 'sandbox') and agent.sandbox:
await agent.sandbox.cleanup()
except Exception as e:
logger.warning(f"call_driven_runner Failed to cleanup sandbox for agent {agent_name}: {e}")
return response
async def _common_process(self, task_span):
start = time.time()
step = 1
pre_agent_name = None
observation = self.observation
for idx, agent in enumerate(self.swarm.ordered_agents):
observation.from_agent_name = agent.id()
observations = [observation]
policy = None
cur_agent = agent
while step <= self.max_steps:
await self.outputs.add_output(
StepOutput.build_start_output(name=f"Step{step}", step_num=step, task_id=self.task.id))
terminated = False
observation = self.swarm.action_to_observation(policy, observations)
observation.from_agent_name = observation.from_agent_name or cur_agent.id()
if observation.to_agent_name and observation.to_agent_name != cur_agent.id():
cur_agent = self.swarm.agents.get(observation.to_agent_name)
exp_id = self._get_step_span_id(step, cur_agent.id())
with trace.span(f"step_execution_{exp_id}") as step_span:
try:
step_span.set_attributes({
"exp_id": exp_id,
"task_id": self.task.id,
"task_name": self.task.name,
"trace_id": trace.get_current_span().get_trace_id(),
"step": step,
"agent_id": cur_agent.id(),
"pre_agent": pre_agent_name,
"observation": json.dumps(observation.model_dump(exclude_none=True),
ensure_ascii=False,
cls=NumpyEncoder)
})
except:
pass
pre_agent_name = cur_agent.id()
agent_message = AgentMessage(
payload=observation,
session_id=self.context.session_id,
headers={"context": self.context}
)
if not override_in_subclass('async_policy', cur_agent.__class__, Agent):
message = cur_agent.run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False),
exp_id=exp_id)
else:
message = await cur_agent.async_run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream",
False),
exp_id=exp_id)
policy = message.payload
step_span.set_attribute("actions",
json.dumps([action.model_dump() for action in policy],
ensure_ascii=False))
observation.content = None
color_log(f"{cur_agent.id()} policy: {policy}")
if not policy:
logger.warning(f"current agent {cur_agent.id()} no policy to use.")
await self.outputs.add_output(
StepOutput.build_failed_output(name=f"Step{step}",
step_num=step,
data=f"current agent {cur_agent.id()} no policy to use.",
task_id=self.context.task_id)
)
if not self.task.is_sub_task:
await self.outputs.mark_completed()
task_span.set_attributes({
"end_time": time.time(),
"duration": time.time() - start,
"status": "failed",
"error": f"current agent {cur_agent.id()} no policy to use."
})
return TaskResponse(msg=f"current agent {cur_agent.id()} no policy to use.",
answer="",
success=False,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage)
if is_agent(policy[0]):
status, info = await self._agent(agent, observation, policy, step)
if status == 'normal':
if info:
observations.append(observation)
elif status == 'break':
observation = self.swarm.action_to_observation(policy, observations)
if idx == len(self.swarm.ordered_agents) - 1:
return TaskResponse(
answer=observation.content,
success=True,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage
)
break
elif status == 'return':
await self.outputs.add_output(
StepOutput.build_finished_output(name=f"Step{step}",
step_num=step,
task_id=self.context.task_id)
)
info.time_cost = (time.time() - start)
task_span.set_attributes({
"end_time": time.time(),
"duration": info.time_cost,
"status": "success"
})
return info
elif is_tool_by_name(policy[0].tool_name):
# todo sandbox
msg, reward, terminated = await self._tool_call(policy, observations, step,
cur_agent)
step_span.set_attribute("reward", reward)
else:
logger.warning(f"Unrecognized policy: {policy[0]}")
await self.outputs.add_output(
StepOutput.build_failed_output(
name=f"Step{step}",
step_num=step,
data=f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool.",
task_id=self.context.task_id
)
)
if not self.task.is_sub_task:
logger.info(f"FINISHED|WorkflowRunner|outputs|{self.task.id} {self.task.is_sub_task}")
await self.outputs.mark_completed()
task_span.set_attributes({
"end_time": time.time(),
"duration": time.time() - start,
"status": "failed",
"error": f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool."
})
return TaskResponse(
msg=f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool.",
answer="",
success=False,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage
)
await self.outputs.add_output(
StepOutput.build_finished_output(name=f"Step{step}",
step_num=step,
task_id=self.context.task_id)
)
step += 1
if terminated and agent.finished:
logger.info(f"{agent.id()} finished")
if idx == len(self.swarm.ordered_agents) - 1:
return TaskResponse(
answer=observations[-1].content,
success=True,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage
)
break
async def _agent(self, agent: Agent, observation: Observation, policy: List[ActionModel], step: int):
# only one agent, and get agent from policy
policy_for_agent = policy[0]
agent_name = policy_for_agent.tool_name
if not agent_name:
agent_name = policy_for_agent.agent_name
cur_agent: Agent = self.swarm.agents.get(agent_name)
if not cur_agent:
raise RuntimeError(f"Can not find {agent_name} agent in swarm.")
status = "normal"
if cur_agent.id() == agent.id():
# Current agent is entrance agent, means need to exit to the outer loop
logger.info(f"{cur_agent.id()} exit the loop")
status = "break"
return status, None
if agent.handoffs and agent_name not in agent.handoffs:
# Unable to hand off, exit to the outer loop
status = "return"
return status, TaskResponse(msg=f"Can not handoffs {agent_name} agent ",
answer=observation.content,
success=False,
id=self.task.id,
usage=self.context.token_usage)
# Check if current agent done
if cur_agent.finished:
cur_agent._finished = False
logger.info(f"{cur_agent.id()} agent be be handed off, so finished state reset to False.")
con = policy_for_agent.policy_info
if policy_for_agent.params and 'content' in policy_for_agent.params:
con = policy_for_agent.params['content']
if observation:
observation.content = con
else:
observation = Observation(content=con)
return status, observation
return status, None
# todo sandbox
async def _tool_call(self, policy: List[ActionModel], observations: List[Observation], step: int, agent: Agent):
msg = None
terminated = False
# group action by tool name
tool_mapping = dict()
reward = 0.0
# Directly use or use tools after creation.
for act in policy:
if not self.tools or (self.tools and act.tool_name not in self.tools):
# dynamic only use default config in module.
conf = self.tools_conf.get(act.tool_name)
tool = ToolFactory(act.tool_name, conf=conf, asyn=conf.use_async if conf else False)
if isinstance(tool, Tool):
tool.reset()
elif isinstance(tool, AsyncTool):
await tool.reset()
tool_mapping[act.tool_name] = []
self.tools[act.tool_name] = tool
if act.tool_name not in tool_mapping:
tool_mapping[act.tool_name] = []
tool_mapping[act.tool_name].append(act)
for tool_name, action in tool_mapping.items():
tool_message = ToolMessage(
payload=action,
session_id=self.context.session_id,
headers={"context": self.context}
)
# Execute action using browser tool and unpack all return values
if isinstance(self.tools[tool_name], Tool):
message = self.tools[tool_name].step(tool_message)
elif isinstance(self.tools[tool_name], AsyncTool):
# todo sandbox
message = await self.tools[tool_name].step(tool_message, agent=agent)
else:
logger.warning(f"Unsupported tool type: {self.tools[tool_name]}")
continue
observation, reward, terminated, _, info = message.payload
# observation, reward, terminated, _, info = action_result_transform(message, sandbox=None)
observations.append(observation)
for i, item in enumerate(action):
tool_output = ToolResultOutput(
tool_type=tool_name,
tool_name=item.tool_name,
data=observation.content,
origin_tool_call=ToolCall.from_dict({
"function": {
"name": item.action_name,
"arguments": item.params,
}
})
)
await self.outputs.add_output(tool_output)
# Check if there's an exception in info
if info.get("exception"):
color_log(f"Step {step} failed with exception: {info['exception']}", color=Color.red)
msg = f"Step {step} failed with exception: {info['exception']}"
logger.info(f"step: {step} finished by tool action: {action}.")
log_ob = Observation(content='' if observation.content is None else observation.content,
action_result=observation.action_result)
trace_logger.info(f"{tool_name} observation: {log_ob}", color=Color.green)
return msg, reward, terminated
def _get_step_span_id(self, step, cur_agent_name):
key = (step, cur_agent_name)
if key not in self.step_agent_counter:
self.step_agent_counter[key] = 0
else:
self.step_agent_counter[key] += 1
exp_index = self.step_agent_counter[key]
return f"{self.task.id}_{step}_{cur_agent_name}_{exp_index}"
class LoopWorkflowRunner(WorkflowRunner):
async def _do_run(self, context: Context = None) -> TaskResponse:
observation = self.observation
if not observation:
raise RuntimeError("no observation, check run process")
start = time.time()
step = 1
msg = None
# Use trace.span to record the entire task execution process
with trace.span(f"task_execution_{self.task.id}", attributes={
"task_id": self.task.id,
"task_name": self.task.name,
"start_time": start
}) as task_span:
try:
for i in range(self.max_steps):
await self._common_process(task_span)
step += 1
except Exception as err:
logger.error(f"Runner run failed, err is {traceback.format_exc()}")
finally:
if not self.task.is_sub_task:
logger.info(f"FINISHED|LoopWorkflowRunner|outputs|{self.task.id} {self.task.is_sub_task}")
await self.outputs.mark_completed()
color_log(f"task token usage: {self.context.token_usage}",
color=Color.pink,
logger_=trace_logger)
for _, tool in self.tools.items():
if isinstance(tool, AsyncTool):
await tool.close()
else:
tool.close()
task_span.set_attributes({
"end_time": time.time(),
"duration": time.time() - start,
"error": msg
})
return TaskResponse(msg=msg,
answer=observation.content,
success=True if not msg else False,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage)
class HandoffRunner(TaskRunner):
def __init__(self, task: Task, *args, **kwargs):
super().__init__(task=task, *args, **kwargs)
async def do_run(self, context: Context = None) -> TaskResponse:
resp = await self._do_run(context)
self._task_response = resp
return resp
async def _do_run(self, context: Context = None) -> TaskResponse:
"""Multi-agent general process based on handoff.
NOTE: Use the agent's finished state to control the loop, so the agent must carefully set finished state.
Args:
context: Context of runner.
"""
start = time.time()
observation = self.observation
info = dict()
step = 0
max_steps = self.conf.get("max_steps", 100)
results = []
swarm_resp = None
self.loop_detect = []
# Use trace.span to record the entire task execution process
with trace.span(f"task_execution_{self.task.id}", attributes={
"task_id": self.task.id,
"task_name": self.task.name,
"start_time": start
}) as task_span:
try:
while step < max_steps:
# Loose protocol
result_dict = await self._process(observation=observation, info=info)
results.append(result_dict)
swarm_resp = result_dict.get("response")
logger.info(f"Step: {step} response:\n {result_dict}")
step += 1
if self.swarm.finished or endless_detect(self.loop_detect,
self.endless_threshold,
self.swarm.communicate_agent.id()):
logger.info("task done!")
break
if not swarm_resp:
logger.warning(f"Step: {step} swarm no valid response")
break
observation = result_dict.get("observation")
if not observation:
observation = Observation(content=swarm_resp)
else:
observation.content = swarm_resp
time_cost = time.time() - start
if not results:
logger.warning("task no result!")
task_span.set_attributes({
"status": "failed",
"error": f"task no result!"
})
return TaskResponse(msg=traceback.format_exc(),
answer='',
success=False,
id=self.task.id,
time_cost=time_cost,
usage=self.context.token_usage)
answer = results[-1].get('observation').content if results[-1].get('observation') else swarm_resp
return TaskResponse(answer=answer,
success=True,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage)
except Exception as e:
logger.error(f"Task execution failed with error: {str(e)}\n{traceback.format_exc()}")
task_span.set_attributes({
"status": "failed",
"error": f"Task execution failed with error: {str(e)}\n{traceback.format_exc()}"
})
return TaskResponse(msg=traceback.format_exc(),
answer='',
success=False,
id=self.task.id,
time_cost=(time.time() - start),
usage=self.context.token_usage)
finally:
color_log(f"task token usage: {self.context.token_usage}",
color=Color.pink,
logger_=trace_logger)
for _, tool in self.tools.items():
if isinstance(tool, AsyncTool):
await tool.close()
else:
tool.close()
task_span.set_attributes({
"end_time": time.time(),
"duration": time.time() - start,
})
async def _process(self, observation, info) -> Dict[str, Any]:
if not self.swarm.initialized:
raise RuntimeError("swarm needs to use `reset` to init first.")
start = time.time()
step = 0
max_steps = self.conf.get("max_steps", 100)
self.swarm.cur_agent = self.swarm.communicate_agent
pre_agent_name = None
# use communicate agent every time
agent_message = AgentMessage(
payload=observation,
session_id=self.context.session_id,
headers={"context": self.context}
)
if override_in_subclass('async_policy', self.swarm.cur_agent.__class__, Agent):
message = self.swarm.cur_agent.run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False))
else:
message = await self.swarm.cur_agent.async_run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False))
self.loop_detect.append(self.swarm.cur_agent.id())
policy = message.payload
if not policy:
logger.warning(f"current agent {self.swarm.cur_agent.id()} no policy to use.")
exp_id = self._get_step_span_id(step, self.swarm.cur_agent.id())
with trace.span(f"step_execution_{exp_id}") as step_span:
step_span.set_attributes({
"exp_id": exp_id,
"task_id": self.task.id,
"task_name": self.task.name,
"trace_id": trace.get_current_span().get_trace_id(),
"step": step,
"agent_id": self.swarm.cur_agent.id(),
"pre_agent": pre_agent_name,
"observation": json.dumps(observation.model_dump(exclude_none=True),
ensure_ascii=False,
cls=NumpyEncoder),
"actions": json.dumps([action.model_dump() for action in policy], ensure_ascii=False)
})
return {"msg": f"current agent {self.swarm.cur_agent.id()} no policy to use.",
"steps": step,
"success": False,
"time_cost": (time.time() - start)}
color_log(f"{self.swarm.cur_agent.id()} policy: {policy}")
msg = None
response = None
return_entry = False
cur_agent = None
cur_observation = observation
finished = False
try:
while step < max_steps:
terminated = False
exp_id = self._get_step_span_id(step, self.swarm.cur_agent.id())
with trace.span(f"step_execution_{exp_id}") as step_span:
try:
step_span.set_attributes({
"exp_id": exp_id,
"task_id": self.task.id,
"task_name": self.task.name,
"trace_id": trace.get_current_span().get_trace_id(),
"step": step,
"agent_id": self.swarm.cur_agent.id(),
"pre_agent": pre_agent_name,
"observation": json.dumps(cur_observation.model_dump(exclude_none=True),
ensure_ascii=False,
cls=NumpyEncoder),
"actions": json.dumps([action.model_dump() for action in policy], ensure_ascii=False)
})
except:
pass
if is_agent(policy[0]):
status, info, ob = await self._social_agent(policy, step)
if status == 'normal':
self.swarm.cur_agent = self.swarm.agents.get(policy[0].agent_name)
policy = info
cur_observation = ob
# clear observation
observation = None
elif is_tool_by_name(policy[0].tool_name):
status, terminated, info = await self._social_tool_call(policy, step)
if status == 'normal':
observation = info
cur_observation = observation
else:
logger.warning(f"Unrecognized policy: {policy[0]}")
return {"msg": f"Unrecognized policy: {policy[0]}, need to check prompt or agent / tool.",
"response": "",
"steps": step,
"success": False}
if status == 'break':
return_entry = info
break
elif status == 'return':
return info
step += 1
pre_agent_name = self.swarm.cur_agent.id()
if terminated and self.swarm.cur_agent.finished:
logger.info(f"{self.swarm.cur_agent.id()} finished")
break
if observation:
if cur_agent is None:
cur_agent = self.swarm.cur_agent
agent_message = AgentMessage(
payload=observation,
session_id=self.context.session_id,
headers={"context": self.context}
)
if not override_in_subclass('async_policy', cur_agent.__class__, Agent):
message = cur_agent.run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False))
else:
message = await cur_agent.async_run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False))
policy = message.payload
color_log(f"{cur_agent.id()} policy: {policy}")
if policy:
response = policy[0].policy_info if policy[0].policy_info else policy[0].action_name
# All agents or tools have completed their tasks
if all(agent.finished for _, agent in self.swarm.agents.items()) or (all(
tool.finished for _, tool in self.tools.items()) and len(self.swarm.agents) == 1):
logger.info("entry agent finished, swarm process finished.")
finished = True
if return_entry and not finished:
# Return to the entrance, reset current agent finished state
self.swarm.cur_agent._finished = False
return {"steps": step,
"response": response,
"observation": observation,
"msg": msg,
"success": True if not msg else False}
except Exception as e:
logger.error(f"Task execution failed with error: {str(e)}\n{traceback.format_exc()}")
return {
"msg": str(e),
"response": "",
"traceback": traceback.format_exc(),
"steps": step,
"success": False
}
async def _social_agent(self, policy: List[ActionModel], step):
# only one agent, and get agent from policy
policy_for_agent = policy[0]
agent_name = policy_for_agent.tool_name
if not agent_name:
agent_name = policy_for_agent.agent_name
cur_agent: Agent = self.swarm.agents.get(agent_name)
if not cur_agent:
raise RuntimeError(f"Can not find {agent_name} agent in swarm.")
if cur_agent.id() == self.swarm.communicate_agent.id() or cur_agent.id() == self.swarm.cur_agent.id():
# Current agent is entrance agent, means need to exit to the outer loop
logger.info(f"{cur_agent.id()} exit to the outer loop")
return 'break', True, None
if self.swarm.cur_agent.handoffs and agent_name not in self.swarm.cur_agent.handoffs:
# Unable to hand off, exit to the outer loop
return "return", {"msg": f"Can not handoffs {agent_name} agent "
f"by {cur_agent.id()} agent.",
"response": policy[0].policy_info if policy else "",
"steps": step,
"success": False}, None
# Check if current agent done
if cur_agent.finished:
cur_agent._finished = False
logger.info(f"{cur_agent.id()} agent be be handed off, so finished state reset to False.")
observation = Observation(content=policy_for_agent.policy_info)
self.loop_detect.append(cur_agent.id())
if cur_agent.step_reset:
cur_agent.reset()
agent_message = AgentMessage(
payload=observation,
session_id=self.context.session_id,
headers={"context": self.context}
)
if not override_in_subclass('async_policy', cur_agent.__class__, Agent):
message = cur_agent.run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False))
else:
message = await cur_agent.async_run(agent_message,
step=step,
outputs=self.outputs,
stream=self.conf.get("stream", False))
agent_policy = message.payload
if not agent_policy:
logger.warning(
f"{observation} can not get the valid policy in {policy_for_agent.agent_name}, exit task!")
return "return", {"msg": f"{policy_for_agent.agent_name} invalid policy",
"response": "",
"steps": step,
"success": False}, None
color_log(f"{cur_agent.id()} policy: {agent_policy}")
return 'normal', agent_policy, observation
async def _social_tool_call(self, policy: List[ActionModel], step: int):
observation = None
terminated = False
# group action by tool name
tool_mapping = dict()
# Directly use or use tools after creation.
for act in policy:
if not self.tools or (self.tools and act.tool_name not in self.tools):
# dynamic only use default config in module.
conf: ToolConfig = self.tools_conf.get(act.tool_name)
tool = ToolFactory(act.tool_name, conf=conf, asyn=conf.use_async if conf else False)
if isinstance(tool, Tool):
tool.reset()
elif isinstance(tool, AsyncTool):
await tool.reset()
tool_mapping[act.tool_name] = []
self.tools[act.tool_name] = tool
if act.tool_name not in tool_mapping:
tool_mapping[act.tool_name] = []
tool_mapping[act.tool_name].append(act)
for tool_name, action in tool_mapping.items():
tool_message = ToolMessage(
payload=action,
session_id=self.context.session_id,
headers={"context": self.context}
)
# Execute action using browser tool and unpack all return values
if isinstance(self.tools[tool_name], Tool):
message = self.tools[tool_name].step(tool_message)
elif isinstance(self.tools[tool_name], AsyncTool):
message = await self.tools[tool_name].step(tool_message)
else:
logger.warning(f"Unsupported tool type: {self.tools[tool_name]}")
continue
observation, reward, terminated, _, info = message.payload
for i, item in enumerate(action):
tool_output = ToolResultOutput(
data=observation.content,
origin_tool_call=ToolCall.from_dict({
"function": {
"name": item.action_name,
"arguments": item.params,
}
}),
task_id=self.task.id
)
await self.outputs.add_output(tool_output)
# Check if there's an exception in info
if info.get("exception"):
color_log(f"Step {step} failed with exception: {info['exception']}", color=Color.red)
logger.info(f"step: {step} finished by tool action {action}.")
log_ob = Observation(content='' if observation.content is None else observation.content,
action_result=observation.action_result)
color_log(f"{tool_name} observation: {log_ob}", color=Color.green)
# The tool results give itself, exit; give to other agents, continue
tmp_name = policy[0].agent_name
if self.swarm.cur_agent.id() == self.swarm.communicate_agent.id() and (
len(self.swarm.agents) == 1 or tmp_name is None or self.swarm.cur_agent.id() == tmp_name):
return "break", terminated, True
elif policy[0].agent_name:
policy_for_agent = policy[0]
agent_name = policy_for_agent.agent_name
if not agent_name:
agent_name = policy_for_agent.tool_name
cur_agent: Agent = self.swarm.agents.get(agent_name)
if not cur_agent:
raise RuntimeError(f"Can not find {agent_name} agent in swarm.")
if self.swarm.cur_agent.handoffs and agent_name not in self.swarm.cur_agent.handoffs:
# Unable to hand off, exit to the outer loop
return "return", {"msg": f"Can not handoffs {agent_name} agent "
f"by {cur_agent.id()} agent.",
"response": policy[0].policy_info if policy else "",
"steps": step,
"success": False}
# Check if current agent done
if cur_agent.finished:
cur_agent._finished = False
logger.info(f"{cur_agent.id()} agent be be handed off, so finished state reset to False.")
return "normal", terminated, observation
def _get_step_span_id(self, step, cur_agent_name):
key = (step, cur_agent_name)
if key not in self.step_agent_counter:
self.step_agent_counter[key] = 0
else:
self.step_agent_counter[key] += 1
exp_index = self.step_agent_counter[key]
return f"{self.task.id}_{step}_{cur_agent_name}_{exp_index}"
@@ -0,0 +1,170 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import functools
import inspect
import logging
from typing import Dict, Any, Callable, Optional, Union, Awaitable
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("callback_registry")
class CallbackRegistry:
"""Callback function registry, used to manage and execute callback functions"""
# Registry for storing decorated callback functions
_registry: Dict[str, Callable] = {}
@classmethod
def register(cls, key_name: str, func: Callable) -> Callable:
"""Register callback function to the registry
Args:
key_name: Unique identifier for the callback function
func: Callback function to register
Returns:
Registered callback function
"""
# Check if a callback function with the same key_name already exists
if key_name in cls._registry:
existing_func = cls._registry[key_name]
logger.warning(
f"Callback function '{key_name}' already exists and will be overwritten! "
f"Original function: {existing_func.__name__ if hasattr(existing_func, '__name__') else str(existing_func)}, "
f"New function: {func.__name__ if hasattr(func, '__name__') else str(func)}"
)
cls._registry[key_name] = func
return func
@classmethod
def get(cls, key_name: str) -> Optional[Callable]:
"""Get registered callback function by key_name
Args:
key_name: Unique identifier for the callback function
Returns:
Registered callback function, or None if not found
"""
return cls._registry.get(key_name)
@classmethod
async def execute(
cls,
key_name: str,
tool: Any,
args: Dict[str, Any],
tool_context: Any,
tool_response: Optional[Dict[str, Any]] = None
) -> Optional[Dict[str, Any]]:
"""Execute registered callback function
Args:
key_name: Unique identifier for the callback function
tool: Tool object
args: Tool arguments
tool_context: Tool context
tool_response: Tool response (for post-callbacks)
Returns:
Return value of the callback function, or None if the callback function doesn't exist
"""
callback = cls.get(key_name)
if not callback:
return None
# Determine parameters based on callback type
if tool_response is not None:
# Post-callback
result = callback(tool, args, tool_context, tool_response)
else:
# Pre-callback
result = callback(tool, args, tool_context)
# Handle asynchronous callbacks
if inspect.isawaitable(result):
result = await result
return result
@classmethod
def list(cls) -> Dict[str, str]:
"""List all registered callback functions
Returns:
Dictionary containing callback function names and descriptions
"""
return {
key: func.__name__ if hasattr(func, '__name__') else str(func)
for key, func in cls._registry.items()
}
def reg_callback(key_name: str):
"""Decorator for registering callback functions
Args:
key_name: Unique identifier for the callback function
Returns:
Decorator function
"""
def decorator(func):
# Register function to the global registry
CallbackRegistry.register(key_name, func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
# For backward compatibility, keep these functions
def get_callback(key_name: str) -> Optional[Callable]:
"""Get registered callback function by key_name
Args:
key_name: Unique identifier for the callback function
Returns:
Registered callback function, or None if not found
"""
return CallbackRegistry.get(key_name)
async def execute_callback(
key_name: str,
tool: Any,
args: Dict[str, Any],
tool_context: Any,
tool_response: Optional[Dict[str, Any]] = None
) -> Optional[Dict[str, Any]]:
"""Execute registered callback function
Args:
key_name: Unique identifier for the callback function
tool: Tool object
args: Tool arguments
tool_context: Tool context
tool_response: Tool response (for post-callbacks)
Returns:
Return value of the callback function, or None if the callback function doesn't exist
"""
return await CallbackRegistry.execute(key_name, tool, args, tool_context, tool_response)
def list_callbacks() -> Dict[str, str]:
"""List all registered callback functions
Returns:
Dictionary containing callback function names and descriptions
"""
return CallbackRegistry.list()
@@ -0,0 +1,84 @@
from typing import Tuple
from aworld.runners.callback.decorator import CallbackRegistry
from aworld.runners.handler.base import DefaultHandler
from aworld.core.common import Observation, CallbackItem
from aworld.core.event.base import Message, Constants
from aworld.logs.util import logger
from aworld.runners.state_manager import RuntimeStateManager, HandleResult, RunNodeStatus
class ToolCallbackHandler(DefaultHandler):
def __init__(self, runner):
self.runner = runner
async def handle(self, message):
if message.category != Constants.TOOL_CALLBACK:
return
logger.info(f"-------ToolCallbackHandler start handle message----: {message}")
self.context = message.context
observation = None
state_mng = RuntimeStateManager.instance()
if not state_mng:
logger.eror("-------ToolCallbackHandler state_mng is None----")
return
try:
payload = message.payload
if not payload:
state_mng.run_failed(message.id, "callback failed", [])
return
if isinstance(payload, CallbackItem):
observation = payload.data[0] if isinstance(payload.data, Tuple) else payload.data
elif isinstance(payload, Tuple) and isinstance(payload[0], Observation):
observation=payload[0]
if not isinstance(observation, Observation):
state_mng.run_failed(message.id, "callback failed", [])
return
if not observation.action_result:
state_mng.run_failed(message.id, "callback failed", [])
return
results = []
for res in observation.action_result:
success = False
result = HandleResult(
result=Message(payload=None,
category=Constants.TOOL_CALLBACK,
sender=self.name(),
session_id=message.context.session_id,
headers={"context": message.context}),
status=RunNodeStatus.FAILED
)
if not res or not res.content or not res.tool_name or not res.action_name:
results.append(result)
continue
callback_func = CallbackRegistry.get(res.tool_name + "__" + res.action_name)
if not callback_func:
result.status = RunNodeStatus.SUCCESS
results.append(result)
continue
callback_res = callback_func(res)
if not callback_res or callback_res.success is False:
results.append(result)
continue
result.status = RunNodeStatus.SUCCESS
result.result.payload = callback_res
results.append(result)
state_mng.run_succeed(message.id, "test callback succ", results)
except Exception as e:
# todo
logger.warning(f"ToolCallbackHandler Failed to parse payload: {e}")
state_mng.run_failed(message.id, "callback failed", [])
finally:
yield Message(
category=Constants.OUTPUT,
payload=None,
sender=self.name(),
session_id=message.session_id,
headers={"context": self.context}
)
return
@@ -0,0 +1,374 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import asyncio
import time
import traceback
from aworld.core.agent.base import BaseAgent
from aworld.core.exceptions import AWorldRuntimeException
import aworld.trace as trace
from typing import List, Callable, Any
from aworld.core.common import TaskItem, ActionModel
from aworld.core.context.base import Context
from aworld.agents.llm_agent import Agent
from aworld.core.event.base import Message, Constants, TopicType, ToolMessage, AgentMessage
from aworld.core.task import Task, TaskResponse
from aworld.events.manager import EventManager
from aworld.logs.util import logger
from aworld.replay_buffer import EventReplayBuffer
from aworld.runners import HandlerFactory
from aworld.runners.handler.base import DefaultHandler
from aworld.runners.task_runner import TaskRunner
from aworld.utils.common import override_in_subclass, new_instance
from aworld.runners.state_manager import EventRuntimeStateManager
class TaskEventRunner(TaskRunner):
"""Event driven task runner."""
def __init__(self, task: Task, *args, **kwargs):
super().__init__(task, *args, **kwargs)
self._task_response = None
self.event_mng = EventManager(self.context)
self.hooks = {}
self.handlers = []
self.init_messages = []
self.background_tasks = set()
self.state_manager = EventRuntimeStateManager.instance()
self.replay_buffer = EventReplayBuffer()
async def do_run(self, context: Context = None):
if self.swarm and not self.swarm.initialized:
raise AWorldRuntimeException("swarm needs to use `reset` to init first.")
if not self.init_messages:
raise AWorldRuntimeException("no question event to solve.")
async with trace.task_span(self.init_messages[0].session_id, self.task):
for msg in self.init_messages:
await self.event_mng.emit_message(msg)
await self._do_run()
await self._save_trajectories()
return self._response()
async def pre_run(self):
logger.debug(f"[TaskEventRunner] pre_run start {self.task.id}")
await super().pre_run()
self.event_mng.context = self.context
self.context.event_manager = self.event_mng
if self.swarm and not self.swarm.max_steps:
self.swarm.max_steps = self.task.conf.get('max_steps', 10)
observation = self.observation
if not observation:
raise RuntimeError("no observation, check run process")
self._build_first_message()
if self.swarm:
logger.debug(f"swarm: {self.swarm}")
# register agent handler
for _, agent in self.swarm.agents.items():
if override_in_subclass('async_policy', agent.__class__, Agent):
await self.event_mng.register(Constants.AGENT, agent.id(), agent.async_run)
else:
await self.event_mng.register(Constants.AGENT, agent.id(), agent.run)
# register tool handler
for key, tool in self.tools.items():
if tool.handler:
await self.event_mng.register(Constants.TOOL, tool.name(), tool.handler)
else:
await self.event_mng.register(Constants.TOOL, tool.name(), tool.step)
handlers = self.event_mng.event_bus.get_topic_handlers(
Constants.TOOL, tool.name())
if not handlers:
await self.event_mng.register(Constants.TOOL, Constants.TOOL, tool.step)
self._stopped = asyncio.Event()
# handler of process in framework
handler_list = self.conf.get("handlers")
if handler_list:
# handler class name
for hand in handler_list:
self.handlers.append(new_instance(hand, self))
else:
for handler in HandlerFactory:
self.handlers.append(HandlerFactory(handler, runner=self))
logger.debug(f"[TaskEventRunner] pre_run finish {self.task.id}")
def _build_first_message(self):
# build the first message
if self.agent_oriented:
agents = self.swarm.communicate_agent
if isinstance(agents, BaseAgent):
agents = [agents]
for agent in agents:
self.init_messages.append(AgentMessage(payload=self.observation,
sender='runner',
receiver=agent.id(),
session_id=self.context.session_id,
headers={'context': self.context}))
else:
actions: List[ActionModel] = self.observation.content
action_dict = {}
for action in actions:
if action.tool_name not in action_dict:
action_dict[action.tool_name] = []
action_dict[action.tool_name].append(action)
for tool_name, actions in action_dict.items():
self.init_messages.append(ToolMessage(payload=actions,
sender='runner',
receiver=tool_name,
session_id=self.context.session_id,
headers={'context': self.context}))
async def _common_process(self, message: Message) -> List[Message]:
logger.debug(
f"[TaskEventRunner] _common_process start {self.task.id}, message_id = {message.id}")
event_bus = self.event_mng.event_bus
key = message.category
transformer = self.event_mng.get_transform_handler(key)
if transformer:
message = await event_bus.transform(message, handler=transformer)
results = []
handlers = self.event_mng.get_handlers(key)
async with trace.message_span(message=message):
logger.debug(
f"[TaskEventRunner] start_message_node start {self.task.id}, message_id = {message.id}")
self.state_manager.start_message_node(message)
logger.debug(
f"[TaskEventRunner] start_message_node end {self.task.id}, message_id = {message.id}")
if handlers:
if message.topic:
handlers = {message.topic: handlers.get(message.topic, [])}
elif message.receiver:
handlers = {message.receiver: handlers.get(
message.receiver, [])}
else:
logger.warning(
f"{message.id} no receiver and topic, be ignored.")
handlers.clear()
handle_tasks = []
for topic, handler_list in handlers.items():
if not handler_list:
logger.warning(f"{topic} no handler, ignore.")
continue
for handler in handler_list:
t = asyncio.create_task(
self._handle_task(message, handler))
handle_tasks.append(t)
logger.debug(
f"[TaskEventRunner] _common_process handle_tasks collect finished {self.task.id}, message_id = {message.id}")
# For _handle_task case, end message node asynchronously
async def async_end_message_node():
logger.debug(
f"[TaskEventRunner] async_end_message_node STARTED {self.task.id}, message_id = {message.id}")
try:
# Wait for all _handle_task tasks to complete before ending message node
if handle_tasks:
logger.debug(
f"[TaskEventRunner] async_end_message_node {self.task.id} Before gather {len(handle_tasks)} tasks")
await asyncio.gather(*handle_tasks)
logger.debug(
f"[TaskEventRunner] async_end_message_node {self.task.id} After gather tasks completed")
logger.debug(
f"[TaskEventRunner] _common_process handle_tasks process end_message_node start {self.task.id}, message_id = {message.id}")
self.state_manager.end_message_node(message)
logger.debug(
f"[TaskEventRunner] _common_process handle_tasks process finished {self.task.id}, message_id = {message.id}")
except Exception as e:
logger.error(f"Error in async_end_message_node: {e}")
raise
end_node_task = asyncio.create_task(async_end_message_node())
self.background_tasks.add(end_node_task)
end_node_task.add_done_callback(self.background_tasks.discard)
else:
# not handler, return raw message
results.append(message)
t = asyncio.create_task(self._raw_task(results))
self.background_tasks.add(t)
t.add_done_callback(self.background_tasks.discard)
# wait until it is complete
await t
self.state_manager.end_message_node(message)
logger.debug(
f"[TaskEventRunner] _common_process return results {self.task.id}, message_id = {message.id}, ")
return results
async def _handle_task(self, message: Message, handler: Callable[..., Any]):
con = message
async with trace.handler_span(message=message, handler=handler):
try:
logger.debug(
f"event_runner _handle_task - self: {self}, swarm: {self.swarm}, event_mng: {self.event_mng}, event_bus: {self.event_mng.event_bus}, message: {message}")
logger.info(
f"[TaskEventRunner] {self.task.id} _handle_task start, message: {message.id}")
if asyncio.iscoroutinefunction(handler):
con = await handler(con)
else:
con = handler(con)
logger.info(
f"[TaskEventRunner] {self.task.id} _handle_task finished message= {message.id}, session_id = {self.task.session_id}")
if isinstance(con, Message):
# process in framework
self.state_manager.save_message_handle_result(name=handler.__name__,
message=message,
result=con)
async for event in self._inner_handler_process(
results=[con],
handlers=self.handlers
):
await self.event_mng.emit_message(event)
else:
self.state_manager.save_message_handle_result(name=handler.__name__,
message=message)
except Exception as e:
logger.warning(
f"{handler} process fail. {traceback.format_exc()}")
error_msg = Message(
category=Constants.TASK,
payload=TaskItem(msg=str(e), data=message),
sender=self.name,
session_id=self.context.session_id,
topic=TopicType.ERROR,
headers={"context": self.context}
)
self.state_manager.save_message_handle_result(name=handler.__name__,
message=message,
result=error_msg)
await self.event_mng.emit_message(error_msg)
async def _raw_task(self, messages: List[Message]):
# process in framework
async for event in self._inner_handler_process(
results=messages,
handlers=self.handlers
):
await self.event_mng.emit_message(event)
async def _inner_handler_process(self, results: List[Message], handlers: List[DefaultHandler]):
# can use runtime backend to parallel
for handler in handlers:
for result in results:
async for event in handler.handle(result):
yield event
async def _do_run(self):
logger.debug(f"[TaskEventRunner] _do_run start {self.task.id}")
"""Task execution process in real."""
start = time.time()
msg = None
answer = None
message = None
try:
while True:
if self.task.timeout > 0 and time.time() - self.start_time > self.task.timeout:
logger.warn(
f"[TaskEventRunner] {self.task.id} task timeout after {time.time() - self.start_time} seconds.")
self._task_response = TaskResponse(answer='',
success=False,
context=message.context,
id=self.task.id,
time_cost=(time.time() - self.start_time),
usage=self.context.token_usage,
msg='cancellation: task timeout',
status='cancelled')
await self.stop()
if await self.is_stopped():
logger.debug(
f"[TaskEventRunner] break snap {self.task.id}")
await self.event_mng.done()
logger.info(
f" [TaskEventRunner] stop task {self.task.id}...")
if self._task_response is None:
# send msg to output
self._task_response = TaskResponse(msg=msg,
answer=answer,
context=message.context,
success=True if not msg else False,
id=self.task.id,
time_cost=(
time.time() - start),
usage=self.context.token_usage,
status='success' if not msg else 'failed')
break
logger.debug(f"[TaskEventRunner] next snap {self.task.id}")
# consume message
message: Message = await self.event_mng.consume()
logger.debug(
f"[TaskEventRunner] next consume finished {self.task.id}, event_bus: {self.event_mng.event_bus},: message = {message}")
# use registered handler to process message
await self._common_process(message)
logger.debug(
f"[TaskEventRunner] _common_process finished {self.task.id}")
except Exception as e:
logger.error(f"consume message fail. {traceback.format_exc()}")
error_msg = Message(
category=Constants.TASK,
payload=TaskItem(msg=str(e), data=message),
sender=self.name,
session_id=self.context.session_id,
topic=TopicType.ERROR,
headers={"context": self.context}
)
self.state_manager.save_message_handle_result(name=TaskEventRunner.__name__,
message=message,
result=error_msg)
await self.event_mng.emit_message(error_msg)
finally:
logger.debug(
f"[TaskEventRunner] _do_run finished await_is_stopped {self.task.id}")
if await self.is_stopped():
logger.info(
f"[TaskEventRunner] _do_run finished is_stopped {self.task.id}")
await self.context.update_task_after_run(self._task_response)
if not self.task.is_sub_task:
logger.info(f"FINISHED|TaskEventRunner|outputs|{self.task.id} {self.task.is_sub_task}")
await self.task.outputs.mark_completed()
if self.swarm and self.swarm.agents:
for agent_name, agent in self.swarm.agents.items():
try:
if hasattr(agent, 'sandbox') and agent.sandbox:
await agent.sandbox.cleanup()
except Exception as e:
logger.warning(
f"event_runner Failed to cleanup sandbox for agent {agent_name}: {e}")
async def stop(self):
self._stopped.set()
async def is_stopped(self):
return self._stopped.is_set()
def response(self):
return self._task_response
def _response(self):
if self.context.get_task().conf and self.context.get_task().conf.resp_carry_context == False:
self._task_response.context = None
return self._task_response
async def _save_trajectories(self):
try:
messages = self.event_mng.messages_by_task_id(self.task.id)
trajectory = await self.replay_buffer.get_trajectory(messages, self.task.id, self.state_manager)
self._task_response.trajectory = trajectory
except Exception as e:
logger.error(f"Failed to get trajectories: {str(e)}.{traceback.format_exc()}")
@@ -0,0 +1,6 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from aworld.runners.handler.base import DefaultHandler
from aworld.utils.common import scan_packages
scan_packages("aworld.runners.handler", [DefaultHandler])
@@ -0,0 +1,481 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
from typing import AsyncGenerator, Tuple
from aworld.agents.loop_llm_agent import LoopableAgent
from aworld.core.agent.base import is_agent, AgentFactory
from aworld.core.agent.swarm import GraphBuildType, AgentGraph
from aworld.core.common import ActionModel, Observation, TaskItem
from aworld.core.event.base import Message, Constants, TopicType, AgentMessage
from aworld.core.exceptions import AWorldRuntimeException
from aworld.logs.util import logger
from aworld.runners import HandlerFactory
from aworld.runners.handler.base import DefaultHandler
from aworld.runners.handler.tool import DefaultToolHandler
from aworld.runners.state_manager import RunNode, RunNodeStatus, RunNodeBusiType
from aworld.runners.utils import endless_detect
from aworld.output.base import StepOutput
class AgentHandler(DefaultHandler):
__metaclass__ = abc.ABCMeta
def __init__(self, runner: 'TaskEventRunner'):
super().__init__(runner)
self.runner = runner
self.swarm = runner.swarm
self.endless_threshold = runner.endless_threshold
self.task_id = runner.task.id
self.agent_calls = []
@classmethod
def name(cls):
return "_agents_handler"
@HandlerFactory.register(name=f'__{Constants.AGENT}__')
class DefaultAgentHandler(AgentHandler):
def is_valid_message(self, message: Message):
if message.category != Constants.AGENT:
if self.swarm and message.sender in self.swarm.agents and message.sender in AgentFactory:
if self.agent_calls:
if self.agent_calls[-1] != message.sender:
self.agent_calls.append(message.sender)
else:
self.agent_calls.append(message.sender)
return False
return True
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
if not self.is_valid_message(message):
return
headers = {"context": message.context}
session_id = message.session_id
data = message.payload
if not data:
# error message, p2p
yield Message(
category=Constants.OUTPUT,
payload=StepOutput.build_failed_output(name=f"{message.caller or self.name()}",
step_num=0,
data="no data to process.",
task_id=self.task_id),
sender=self.name(),
session_id=session_id,
headers=headers
)
yield Message(
category=Constants.TASK,
payload=TaskItem(msg="no data to process.", data=data, stop=True),
sender=self.name(),
session_id=session_id,
topic=TopicType.ERROR,
headers=headers
)
return
if isinstance(data, Tuple) and isinstance(data[0], Observation):
data = data[0]
message.payload = data
# data is Observation
if isinstance(data, Observation):
if not self.swarm:
msg = Message(
category=Constants.TASK,
payload=data.content,
sender=data.observer,
session_id=session_id,
topic=TopicType.FINISHED,
headers=headers
)
logger.info(f"FINISHED|agent handler send finished message: {msg}")
yield msg
return
agent = self.swarm.agents.get(message.receiver)
# agent + tool completion protocol.
if agent and agent.finished and data.info.get('done'):
self.swarm.cur_step += 1
root_agent = self.swarm.communicate_agent
if isinstance(root_agent, list):
root_agent = root_agent[0]
if agent.id() == root_agent.id():
msg = Message(
category=Constants.TASK,
payload=data.content,
sender=agent.id(),
session_id=session_id,
topic=TopicType.FINISHED,
headers=headers
)
logger.info(f"FINISHED|agent handler send finished message: {msg}")
yield msg
else:
msg = Message(
category=Constants.AGENT,
payload=Observation(content=data.content),
sender=agent.id(),
session_id=session_id,
receiver=root_agent.id(),
headers=message.headers
)
logger.info(f"agent handler send agent message: {msg}")
yield msg
else:
if data.info.get('done'):
agent_name = self.agent_calls[-1]
async for event in self._stop_check(ActionModel(agent_name=agent_name, policy_info=data.content),
message):
yield event
elif not message.receiver:
agent_name = message.sender
async for event in self._stop_check(ActionModel(agent_name=agent_name, policy_info=data.content),
message):
yield event
else:
logger.info(f"agent handler send observation message: {message}")
yield message
return
# data is List[ActionModel]
for action in data:
if not isinstance(action, ActionModel):
# error message, p2p
yield Message(
category=Constants.OUTPUT,
payload=StepOutput.build_failed_output(name=f"{message.caller or self.name()}",
step_num=0,
data="action not a ActionModel.",
task_id=self.task_id),
sender=self.name(),
session_id=session_id,
headers=headers
)
msg = Message(
category=Constants.TASK,
payload=TaskItem(msg="action not a ActionModel.", data=data, stop=True),
sender=self.name(),
session_id=session_id,
topic=TopicType.ERROR,
headers=headers
)
logger.info(f"agent handler send task message: {msg}")
yield msg
return
tools = []
agents = []
for action in data:
if is_agent(action):
agents.append(action)
else:
tools.append(action)
if tools:
msg = Message(
category=Constants.TOOL,
payload=tools,
sender=self.name(),
session_id=session_id,
receiver=DefaultToolHandler.name(),
headers=message.headers
)
logger.info(f"agent handler send tool message: {msg}")
yield msg
else:
yield Message(
category=Constants.OUTPUT,
payload=StepOutput.build_finished_output(name=f"{message.caller or self.name()}",
step_num=0,
task_id=self.task_id),
sender=self.name(),
receiver=agents[0].tool_name,
session_id=session_id,
headers=headers
)
for agent in agents:
async for event in self._agent(agent, message):
logger.info(f"agent handler send message: {event}")
yield event
async def _agent(self, action: ActionModel, message: Message):
self.agent_calls.append(action.agent_name)
agent = self.swarm.agents.get(action.agent_name)
# be handoff
agent_name = action.tool_name
if not agent_name:
async for event in self._stop_check(action, message):
yield event
return
headers = {"context": message.context}
session_id = message.session_id
cur_agent = self.swarm.agents.get(agent_name)
if not cur_agent or not agent:
yield Message(
category=Constants.TASK,
payload=TaskItem(msg=f"Can not find {agent_name} or {action.agent_name} agent in swarm.",
data=action,
stop=True),
sender=self.name(),
session_id=session_id,
topic=TopicType.ERROR,
headers=headers
)
return
cur_agent._finished = False
con = action.policy_info
if action.params and 'content' in action.params:
con = action.params['content']
observation = Observation(content=con, observer=agent.id(), from_agent_name=agent.id())
if agent.handoffs and agent_name not in agent.handoffs:
if message.caller:
message.receiver = message.caller
message.caller = ''
yield message
else:
yield Message(category=Constants.TASK,
payload=TaskItem(msg=f"Can not handoffs {agent_name} agent ", data=observation),
sender=self.name(),
session_id=session_id,
topic=TopicType.RERUN,
headers=headers)
return
headers = message.headers.copy()
# headers.update({"agent_as_tool": True})
yield Message(
category=Constants.AGENT,
payload=observation,
caller=message.caller,
sender=action.agent_name,
session_id=session_id,
receiver=action.tool_name,
headers=headers,
)
async def _stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
if GraphBuildType.TEAM.value == self.swarm.build_type:
async for event in self._team_stop_check(action, message):
yield event
elif GraphBuildType.HANDOFF.value == self.swarm.build_type:
async for event in self._handoff_stop_check(action, message):
yield event
else:
async for event in self._workflow_stop_check(action, message):
yield event
async def _workflow_stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
# Equivalent to scheduling
session_id = message.session_id
agent_name = action.agent_name
agent = self.swarm.agents.get(agent_name)
if not agent:
yield Message(
category=Constants.TASK,
payload=TaskItem(
msg=f"Can not find {action.agent_name} agent in ordered_agents: {self.swarm.ordered_agents}.",
data=action,
stop=True),
sender=self.name(),
session_id=session_id,
topic=TopicType.ERROR,
headers=message.headers
)
return
receiver = None
# loop agent type
if isinstance(agent, LoopableAgent):
agent.cur_run_times += 1
if not agent.finished:
receiver = agent.goto
if receiver:
yield Message(
category=Constants.AGENT,
payload=Observation(content=action.policy_info),
sender=agent.id(),
session_id=session_id,
receiver=receiver,
headers=message.headers
)
else:
agent_graph: AgentGraph = self.swarm.agent_graph
# next
successor = agent_graph.successor.get(agent_name)
if not successor:
yield Message(
category=Constants.TASK,
payload=action.policy_info,
sender=agent.id(),
session_id=session_id,
topic=TopicType.FINISHED,
headers=message.headers
)
return
for k, _ in successor.items():
predecessor = agent_graph.predecessor.get(k)
if not predecessor:
raise AWorldRuntimeException(f"{k} has no predecessor {agent_name}, may changed during iteration.")
all_input = {}
pre_finished = True
for pre_k, _ in predecessor.items():
if pre_k == agent_name:
all_input[agent_name] = action.policy_info
continue
# check all predecessor agent finished
run_node: RunNode = self.runner.state_manager.query_by_task(
task_id=message.context.get_task().id,
busi_typ=RunNodeBusiType.AGENT,
busi_id=pre_k
)
if run_node:
run_node = run_node[0]
else:
raise AWorldRuntimeException(f"{pre_k} can't find in task: {message.context.get_task().id}.")
if run_node.status == RunNodeStatus.RUNNING or run_node.status == RunNodeStatus.INIT:
# mean not finished
pre_finished = False
logger.info(f"{pre_k} not finished, will wait it.")
else:
logger.info(f"{pre_k} finished, result is: {run_node.results}")
payload = run_node.results[-1].result.payload[0]
all_input[pre_k] = payload.policy_info
if pre_finished:
yield Message(
category=Constants.AGENT,
payload=Observation(content=all_input if len(all_input) > 1 else all_input.get(agent_name)),
sender=agent.id(),
session_id=session_id,
receiver=k,
headers=message.headers
)
async def _team_stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
caller = message.caller
session_id = message.session_id
agent = self.swarm.agents.get(action.agent_name)
if ((not caller or caller == self.swarm.communicate_agent.id())
and (self.swarm.cur_step >= self.swarm.max_steps or self.swarm.finished or
(agent.id() == self.swarm.agent_graph.root_agent.id() and agent.finished))):
logger.info(
f"FINISHED|_social_stop_check finished|{self.swarm.cur_step}|{self.swarm.max_steps}|{self.swarm.finished}")
yield Message(
category=Constants.TASK,
payload=action.policy_info,
sender=agent.id(),
session_id=session_id,
topic=TopicType.FINISHED,
headers={"context": message.context}
)
agent = self.swarm.agents.get(action.agent_name)
caller = self.swarm.agent_graph.root_agent.id() or message.caller
if agent.id() != self.swarm.agent_graph.root_agent.id():
logger.info(f"_stop_check Team|{agent.id()} --> {caller}")
yield Message(
category=Constants.AGENT,
payload=Observation(content=action.policy_info),
sender=agent.id(),
session_id=message.session_id,
receiver=caller,
headers=message.headers
)
async def _handoff_stop_check(self, action: ActionModel, message: Message) -> AsyncGenerator[Message, None]:
headers = {"context": message.context}
agent = self.swarm.agents.get(action.agent_name)
caller = message.caller
session_id = message.session_id
if endless_detect(self.agent_calls,
endless_threshold=self.endless_threshold,
root_agent_name=self.swarm.communicate_agent.id()):
logger.info(
f"FINISHED|_social_stop_check endless_detect|{self.agent_calls}|{self.endless_threshold}|{self.swarm.communicate_agent.id()}")
yield Message(
category=Constants.TASK,
payload=action.policy_info,
sender=agent.id(),
session_id=session_id,
topic=TopicType.FINISHED,
headers=headers
)
return
if not caller or caller == self.swarm.communicate_agent.id():
if self.swarm.cur_step >= self.swarm.max_steps or self.swarm.finished:
logger.info(
f"FINISHED|_social_stop_check finished|{self.swarm.cur_step}|{self.swarm.max_steps}|{self.swarm.finished}")
yield Message(
category=Constants.TASK,
payload=action.policy_info,
sender=agent.id(),
session_id=session_id,
topic=TopicType.FINISHED,
headers=headers
)
else:
self.swarm.cur_step += 1
logger.info(f"_social_stop_check execute loop {self.swarm.cur_step}.")
yield Message(
category=Constants.AGENT,
payload=Observation(content=action.policy_info),
sender=agent.id(),
session_id=session_id,
receiver=self.swarm.communicate_agent.id(),
headers=message.headers
)
else:
idx = 0
for idx, name in enumerate(self.agent_calls[::-1]):
if name == agent.id():
break
idx = len(self.agent_calls) - idx - 1
if idx:
caller = self.agent_calls[idx - 1]
yield Message(
category=Constants.AGENT,
payload=Observation(content=action.policy_info),
sender=agent.id(),
session_id=session_id,
receiver=caller,
headers=message.headers
)
def is_group_finish(self, event: Message) -> bool:
"""Determine if an event triggers group completion"""
if not isinstance(event, Message) or not event.group_id:
return False
agent_id = event.sender
if not agent_id:
return False
agent = self.swarm.agents.get(agent_id)
if not agent:
return False
return agent._finished and agent.id() == event.headers.get('root_agent_id', '')
async def post_handle(self, input: Message, output: Message) -> Message:
new_context = output.context.deep_copy()
new_context._task = output.context.get_task()
output.context = new_context
if self.is_group_finish(output):
from aworld.runners.state_manager import RuntimeStateManager
state_mng = RuntimeStateManager.instance()
await state_mng.finish_sub_group(output.group_id, output.headers.get('root_message_id'),
[output])
return None
return output
@@ -0,0 +1,102 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
import time
from typing import TypeVar, Generic, AsyncGenerator
from aworld.events.util import send_message
from aworld.core.common import TaskItem
from aworld.core.event.base import Message, Constants, TopicType, CancelMessage
from aworld.logs.util import logger
IN = TypeVar('IN')
OUT = TypeVar('OUT')
class Handler(Generic[IN, OUT]):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
async def handle(self, data: IN) -> AsyncGenerator[OUT, None]:
"""Process the data as the expected result.
Args:
data: Data generated while running the task.
"""
@classmethod
def name(cls):
"""Handler name."""
return cls.__name__
class DefaultHandler(Handler[Message, AsyncGenerator[Message, None]]):
"""Default handler."""
def __init__(self, runner: 'TaskEventRunner'):
self.runner = runner
self.hooks = None
def get_registered_name(self):
"""Get the registered name of the handler.
If the class has a REGISTERED_NAME attribute, return the value of the attribute;
otherwise return None.
"""
return getattr(self.__class__, "REGISTERED_NAME", None)
def is_valid_message(self, message: Message):
"""Validate if the message is valid for this handler.
If the class has a REGISTERED_NAME attribute, check if the message's category matches the registered name;
otherwise return True.
"""
registered_name = self.get_registered_name()
if registered_name is not None:
return message.category == registered_name
return True
async def handle(self, message: Message) -> AsyncGenerator[Message, None]:
if not self.is_valid_message(message):
return
timeout = message.context.get_task().timeout
time_cost = time.time() - self.runner.start_time
if message.topic != TopicType.CANCEL and timeout > 0 and time_cost > timeout:
logger.warn(
f"[{self.name()}] {message.context.get_task().id} task timeout after {time_cost} seconds.")
yield CancelMessage(
payload=TaskItem(msg="task timeout.", data=message, stop=True),
sender=self.name(),
session_id=self.runner.context.session_id,
headers={"context": message.context}
)
return
async for event in self._do_handle(message):
msg = await self.post_handle(input=message, output=event)
if msg:
yield msg
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
yield message
async def post_handle(self, input:Message, output: Message) -> Message:
"""Post handle the message.
Args:
message: Message generated while running the task.
"""
return output
async def run_hooks(self, message: Message, hook_point: str) -> AsyncGenerator[Message, None]:
if not self.hooks:
return
hooks = self.hooks.get(hook_point, [])
for hook in hooks:
try:
msg = await hook.exec(message)
if msg:
yield msg
except:
logger.warning(f"{self.name()}|{hook.point()} {hook.name()} execute fail.")
@@ -0,0 +1,413 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
import copy
import json
from typing import AsyncGenerator, List, Dict, Any, Tuple
from aworld.agents.llm_agent import Agent
from aworld.core.agent.base import is_agent
from aworld.core.common import ActionModel, TaskItem, Observation, ActionResult
from aworld.core.context.base import Context
from aworld.core.event.base import Message, Constants, TopicType, GroupMessage
from aworld.logs.util import logger
from aworld.output.base import StepOutput
from aworld.runners import HandlerFactory
from aworld.runners.handler.base import DefaultHandler
from aworld.runners.handler.tool import DefaultToolHandler
from aworld.runners.state_manager import RuntimeStateManager, RunNodeStatus
from aworld.utils.serialized_util import to_serializable
from aworld.utils.run_util import exec_agent
class GroupHandler(DefaultHandler):
__metaclass__ = abc.ABCMeta
def __init__(self, runner: 'TaskEventRunner'):
super().__init__(runner)
self.runner = runner
self.swarm = runner.swarm
self.endless_threshold = runner.endless_threshold
self.task_id = runner.task.id
@classmethod
def name(cls):
return "_group_handler"
@HandlerFactory.register(name=f'__{Constants.GROUP}__')
class DefaultGroupHandler(GroupHandler):
def is_valid_message(self, message: Message):
if message.category != Constants.GROUP:
return False
return True
async def _do_handle(self, message: GroupMessage) -> AsyncGenerator[Message, None]:
if not self.is_valid_message(message):
return
self.context = message.context
group_id = message.group_id
headers = {'context': self.context}
state_manager = RuntimeStateManager.instance()
if message.topic == TopicType.GROUP_ACTIONS:
# message.payload is List[ActionModel]
node_ids = []
action_messages = []
agents = []
tools = []
agent_actions_map = {}
for action in message.payload:
if not isinstance(action, ActionModel):
# error message, p2p
async for event in self._send_failed_message(message, message.payload, message):
yield event
return
if is_agent(action):
agents.append(action)
agent_name = action.tool_name
if agent_name not in agent_actions_map:
agent_actions_map[agent_name] = []
agent_actions_map[agent_name].append(action)
else:
tools.append(action)
# Process each agent's actions
agent_messages = {}
for agent_name, actions in agent_actions_map.items():
# Get original agent
original_agent = self.swarm.agents.get(agent_name)
if not original_agent:
error_msg = Message(
category=Constants.TASK,
payload=TaskItem(msg=f"Can not find {agent_name} agent in swarm.",
data=actions,
stop=True),
sender=self.name(),
session_id=message.session_id,
topic=TopicType.ERROR,
headers={'context': self.context}
)
yield error_msg
return
# Create agent copies and execute for each action
for action in actions:
msg = await self._build_agent_message(action, message)
if msg.category != Constants.AGENT:
yield msg
return
self._update_headers(msg, message)
agent_copy = self.copy_agent(original_agent)
con = action.policy_info
if action.params and 'content' in action.params:
con = action.params['content']
if agent_name not in agent_messages:
agent_messages[agent_name] = []
agent_messages[agent_name].append((con, agent_copy, msg))
agent_node_ids, agent_tasks = await self._parallel_exec_agents_actions(agent_messages, message)
node_ids.extend(agent_node_ids)
if tools:
tool_mapping = {}
for action in tools:
tool_name = action.tool_name
if tool_name not in tool_mapping:
tool_mapping[tool_name] = []
tool_mapping[tool_name].append(action)
for tool_name, actions in tool_mapping.items():
msg = await self._build_tool_message(actions, message)
self._update_headers(msg, message)
action_messages.append(msg)
node_ids.append(msg.id)
# create group
group_meta_data = message.headers.copy()
group_meta_data["context"] = message.context.deep_copy()
group_meta_data["context"].set_task(message.context.get_task())
await state_manager.create_group(group_id, message.session_id, node_ids,
message.headers.get('parent_group_id'),
group_meta_data)
for _, acts in agent_messages.items():
for act in acts:
self.runner.state_manager.start_message_node(act[2])
for msg in action_messages:
yield msg
await self.process_agent_tasks(agent_tasks, message)
elif message.topic == TopicType.GROUP_RESULTS:
# merge group results
action_results = []
group_results = message.payload
group_sender = None
group_sender_node_id = None
agent_context = self.context.deep_copy()
agent_context._task = self.context.get_task()
receiver_results = {}
for node_id, handle_res_list in group_results.items():
if not handle_res_list:
logger.warn(f"{self.name()} get group result with empty handle_res.")
return
node = state_manager._find_node(node_id)
tool_call_id = node.metadata.get('root_tool_call_id')
is_tool = not tool_call_id and not node.metadata.get('root_agent_id')
if not group_sender:
group_sender = node.metadata.get('group_sender')
if not group_sender_node_id:
group_sender_node_id = node.metadata.get('group_sender_node_id')
node_results = []
for handle_res in handle_res_list:
res_msg = handle_res.result
res_status = handle_res.status
if res_status == RunNodeStatus.FAILED or not res_msg:
logger.warn(f"{self.name()} get group result with failed handle_res: {handle_res}.")
return
receiver = res_msg.receiver
if not receiver:
logger.warn(f"{self.name()} get group result with empty receiver: {res_msg}.")
continue
if receiver != group_sender:
if receiver not in receiver_results:
receiver_results[receiver] = []
receiver_results[receiver].append(res_msg)
else:
if is_tool and isinstance(res_msg.payload, Observation):
action_results.extend(res_msg.payload.action_result)
else:
node_results.append(res_msg.payload)
self._merge_context(agent_context, res_msg.context)
if node_results and tool_call_id:
act_res = ActionResult(
content=json.dumps(to_serializable(node_results), ensure_ascii=False),
tool_call_id=tool_call_id
)
action_results.append(act_res)
if action_results:
group_res_msg = Message(
category=Constants.AGENT,
payload=Observation(content="", action_result=action_results),
caller=message.caller,
sender=self.name(),
session_id=message.session_id,
receiver=group_sender,
headers={'context': agent_context}
)
receiver_results[group_sender] = [group_res_msg]
for receiver, res_msgs in receiver_results.items():
result_message = self._merge_result_messages(res_msgs, message, group_sender_node_id)
group_headers = {}
group_sender_node = state_manager._find_node(group_sender_node_id)
if group_sender_node:
group_headers.update(group_sender_node.metadata.copy())
group_headers['level'] = headers.get('level', 0) + 1
group_headers['context'] = result_message.context or self.context
result_message.headers = group_headers
yield result_message
def copy_agent(self, agent: Agent):
"""Create a copy of the agent
Args:
agent: Original agent object
Returns:
Deep copy of the agent
"""
return agent
async def _parallel_exec_agents_actions(self, agent_messages: Dict[str, List[Tuple[str, Agent, Message]]],
message: Message):
"""Execute multiple agent actions in parallel
Args:
agent_messages: Messages for agent actions
"""
tasks = {}
messages_ids = []
for agent_name, acts in agent_messages.items():
for act in acts:
agent_message = act[2]
messages_ids.append(agent_message.id)
tasks[agent_message.id] = exec_agent(act[0], act[1], self.context, sub_task=True, outputs=self.context.outputs)
return messages_ids, tasks
async def process_agent_tasks(self, agent_tasks, input_message):
"""Process agent async tasks
Args:
agent_tasks: Agent async tasks
"""
root_agent_set = set()
for node_id, task in agent_tasks.items():
res = await task
logger.info(f"{node_id} finished task: {res}")
state_manager = self.runner.state_manager
node = state_manager._find_node(node_id)
if not node:
logger.warn(f"{self.name()} get group result with empty node.")
return
root_agent_id = node.metadata.get('root_agent_id')
root_agent_set.add(root_agent_id)
self.context.merge_sub_context(res.context)
msg = Message(
category=Constants.AGENT,
payload=[ActionModel(policy_info=res.answer, agent_name=root_agent_id)],
sender=root_agent_id,
session_id=node.session_id,
headers={'context': self.context,
'root_agent_id': root_agent_id,
'root_tool_call_id': node.metadata.get('root_tool_call_id')}
)
finish_group_messages = []
async for event in self.runner._inner_handler_process(
results=[msg],
handlers=self.runner.handlers
):
# Only AGENT and TASK messages
if isinstance(event, Message) and (
event.category == Constants.AGENT or event.category == Constants.TASK):
finish_group_messages.append(event)
print(f"======== event context: {event.context},.context.task: {event.context.get_task()}")
await state_manager.finish_sub_group(node.metadata.get('group_id'), node_id, finish_group_messages)
for agent_id in root_agent_set:
agent = self.swarm.agents.get(agent_id)
if agent:
agent._finished = True
def _merge_result_messages(self, res_msgs: List[Message], input_message: Message, group_sender_node_id: str):
"""Merge multiple result messages
Args:
res_msgs: Result messages
"""
if len(res_msgs) == 1:
return res_msgs[0]
input_list = []
new_context = input_message.context.deep_copy()
new_context._task = self.context.get_task()
for message in res_msgs:
map = {}
map[message.sender] = message.payload
input_list.append(map)
new_context.merge_context(message.context)
return Message(
category=Constants.AGENT,
payload=Observation(content=input_list),
sender=self.name(),
receiver=res_msgs[0].receiver,
session_id=res_msgs[0].session_id,
headers={
'context': new_context
}
)
async def _build_agent_message(self, action: ActionModel, message: Message) -> Message:
session_id = message.session_id
headers = {
"context": message.context,
"root_tool_call_id": action.tool_call_id
}
from_agent = self.swarm.agents.get(action.agent_name)
tool_name = action.tool_name
if not tool_name:
logger.warn(f"{self.name()} get agent action with empty tool_name.")
return Message(
category=Constants.TASK,
payload=TaskItem(msg=f"Empty tool_name in group_action: {action}.",
data=action,
stop=True),
sender=self.name(),
session_id=session_id,
topic=TopicType.ERROR,
headers=headers
)
cur_agent = self.swarm.agents.get(tool_name)
if not cur_agent:
return Message(
category=Constants.TASK,
payload=TaskItem(msg=f"Can not find {tool_name} agent in swarm.",
data=action,
stop=True),
sender=self.name(),
session_id=session_id,
topic=TopicType.ERROR,
headers=headers
)
cur_agent._finished = False
con = action.policy_info
if action.params and 'content' in action.params:
con = action.params['content']
observation = Observation(content=con, observer=from_agent.id(), from_agent_name=from_agent.id())
return Message(
category=Constants.AGENT,
payload=observation,
caller=message.caller,
sender=action.agent_name,
session_id=session_id,
receiver=cur_agent.id(),
headers=headers
)
async def _build_tool_message(self, actions: List[ActionModel], message: Message):
session_id = message.session_id
headers = {"context": message.context.deep_copy()}
return Message(
category=Constants.TOOL,
payload=actions,
sender=self.name(),
session_id=session_id,
receiver=DefaultToolHandler.name(),
headers=headers
)
async def _send_failed_message(self, message, data, result_msg):
yield Message(
category=Constants.OUTPUT,
payload=StepOutput.build_failed_output(name=f"{message.caller or self.name()}",
step_num=0,
data=result_msg,
task_id=self.task_id),
sender=self.name(),
session_id=self.context.session_id,
headers=message.headers
)
yield Message(
category=Constants.TASK,
payload=TaskItem(msg=result_msg, data=data, stop=True),
sender=self.name(),
session_id=self.context.session_id,
topic=TopicType.ERROR,
headers=message.headers
)
def _update_headers(self, message: Message, parent_message: Message):
headers = message.headers.copy()
context = message.context.deep_copy()
context.set_task(self.context.get_task())
headers['context'] = context
headers['group_id'] = parent_message.group_id
headers['root_message_id'] = message.id
headers['root_agent_id'] = message.receiver if message.category == Constants.AGENT else ''
headers['level'] = 0
headers['group_sender'] = parent_message.sender
headers['group_sender_node_id'] = parent_message.id
headers['parent_group_id'] = parent_message.headers.get('parent_group_id')
message.headers = headers
def _merge_context(self, context: Context, new_context: Context):
if not new_context:
return
if not context:
context = new_context
return
context.merge_context(new_context)
@@ -0,0 +1,95 @@
# aworld/runners/handler/output.py
import json
from typing import AsyncGenerator
from aworld.core.task import TaskResponse
from aworld.models.model_response import ModelResponse
from aworld.runners import HandlerFactory
from aworld.runners.handler.base import DefaultHandler
from aworld.output.base import StepOutput, MessageOutput, Output
from aworld.core.common import TaskItem
from aworld.core.event.base import Message, Constants, TopicType
from aworld.logs.util import logger
from aworld.runners.hook.hook_factory import HookFactory
from aworld.runners.hook.hooks import HookPoint
@HandlerFactory.register(name=f'__{Constants.OUTPUT}__')
class DefaultOutputHandler(DefaultHandler):
def __init__(self, runner):
super().__init__(runner)
self.runner = runner
self.hooks = {}
if runner.task.hooks:
for k, vals in runner.task.hooks.items():
self.hooks[k] = []
for v in vals:
cls = HookFactory.get_class(v)
if cls:
self.hooks[k].append(cls)
def is_valid_message(self, message: Message):
if message.category != Constants.OUTPUT:
return False
return True
async def _do_handle(self, message):
if not self.is_valid_message(message):
return
# 1. get outputs
outputs = self.runner.task.outputs
if not outputs:
yield Message(
category=Constants.TASK,
payload=TaskItem(msg="Cannot get outputs.",
data=message, stop=True),
sender=self.name(),
session_id=self.runner.context.session_id,
topic=TopicType.ERROR,
headers={"context": message.context}
)
return
# 2. Call OUTPUT_PROCESS hooks to process data in the message
async for event in self.run_hooks(message, HookPoint.OUTPUT_PROCESS):
# If hook returns a processed message, use the processed message
if event and isinstance(event, Message) and event.payload:
message.payload = event.payload
# 3. build Output
payload = message.payload
mark_complete = False
output = None
try:
if isinstance(payload, Output):
output = payload
output.task_id = self.runner.task.id
elif isinstance(payload, TaskResponse):
logger.info(
f"FINISHED|output get task_response with usage: {json.dumps(payload.usage)}")
if message.topic == TopicType.FINISHED or message.topic == TopicType.ERROR:
mark_complete = True
elif isinstance(payload, ModelResponse) or isinstance(payload, AsyncGenerator):
output = MessageOutput(source=payload, task_id=self.runner.task.id)
except Exception as e:
logger.warning(f"Failed to parse output: {e}")
yield Message(
category=Constants.TASK,
payload=TaskItem(msg="Failed to parse output.",
data=payload, stop=True),
sender=self.name(),
session_id=self.runner.context.session_id,
topic=TopicType.ERROR,
headers={"context": message.context}
)
finally:
if output:
if not output.metadata:
output.metadata = {}
output.metadata['sender'] = message.sender
output.metadata['receiver'] = message.receiver
await outputs.add_output(output)
if mark_complete:
logger.info(f"FINISHED|output mark_completed|{self.runner.task.id}")
await outputs.mark_completed()
return
@@ -0,0 +1,138 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
import time
from typing import AsyncGenerator, TYPE_CHECKING
from aworld.core.common import TaskItem
from aworld.core.tool.base import Tool, AsyncTool
from aworld.core.event.base import Message, Constants, TopicType
from aworld.core.task import TaskResponse
from aworld.logs.util import logger
from aworld.output import Output
from aworld.runners import HandlerFactory
from aworld.runners.handler.base import DefaultHandler
from aworld.runners.hook.hook_factory import HookFactory
from aworld.runners.hook.hooks import HookPoint
if TYPE_CHECKING:
from aworld.runners.event_runner import TaskEventRunner
class TaskHandler(DefaultHandler):
__metaclass__ = abc.ABCMeta
def __init__(self, runner: 'TaskEventRunner'):
super().__init__(runner)
self.runner = runner
self.retry_count = runner.task.max_retry_count
self.hooks = {}
if runner.task.hooks:
for k, vals in runner.task.hooks.items():
self.hooks[k] = []
for v in vals:
cls = HookFactory.get_class(v)
if cls:
self.hooks[k].append(cls)
@classmethod
def name(cls):
return "_task_handler"
@HandlerFactory.register(name=f'__{Constants.TASK}__')
class DefaultTaskHandler(TaskHandler):
def is_valid_message(self, message: Message):
if message.category != Constants.TASK:
return False
return True
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
if not self.is_valid_message(message):
return
logger.debug(f"task handler receive message: {message}")
headers = {"context": message.context}
topic = message.topic
task_item: TaskItem = message.payload
if topic == TopicType.SUBSCRIBE_TOOL:
new_tools = message.payload.data
for name, tool in new_tools.items():
if isinstance(tool, Tool) or isinstance(tool, AsyncTool):
await self.runner.event_mng.register(Constants.TOOL, name, tool.step)
logger.info(f"dynamic register {name} tool.")
else:
logger.warning(f"Unknown tool instance: {tool}")
return
elif topic == TopicType.SUBSCRIBE_AGENT:
return
elif topic == TopicType.ERROR:
async for event in self.run_hooks(message, HookPoint.ERROR):
yield event
logger.warning(f"task {self.runner.task.id} stop, cause: {task_item.msg}")
self.runner._task_response = TaskResponse(msg=task_item.msg,
answer='',
context=message.context,
success=False,
id=self.runner.task.id,
time_cost=(time.time() - self.runner.start_time),
usage=self.runner.context.token_usage)
if not self.runner.task.is_sub_task:
logger.info(f"FINISHED|DefaultTaskHandler|outputs|{self.runner.task.id} {self.runner.task.is_sub_task}")
await self.runner.task.outputs.mark_completed()
await self.runner.stop()
elif topic == TopicType.FINISHED:
async for event in self.run_hooks(message, HookPoint.FINISHED):
yield event
self.runner._task_response = TaskResponse(answer=message.payload,
success=True,
context=message.context,
id=self.runner.task.id,
time_cost=(time.time() - self.runner.start_time),
usage=self.runner.context.token_usage)
logger.info(f"FINISHED|task|{self.runner.task.id} finished. {self.runner.task.is_sub_task}")
if not self.runner.task.is_sub_task:
logger.info(f"FINISHED|DefaultTaskHandler|outputs|{self.runner.task.id} {self.runner.task.is_sub_task}")
await self.runner.task.outputs.mark_completed()
await self.runner.stop()
elif topic == TopicType.START:
async for event in self.run_hooks(message, HookPoint.START):
yield event
logger.info(f"task start event: {message}, will send init message.")
if message.payload:
yield message
else:
yield self.runner.init_message
elif topic == TopicType.OUTPUT:
yield message
elif topic == TopicType.HUMAN_CONFIRM:
logger.warn("=============== Get human confirm, pause execution ===============")
if self.runner.task.outputs and message.payload:
await self.runner.task.outputs.add_output(Output(data=message.payload))
self.runner._task_response = TaskResponse(answer=message.payload,
success=True,
context=message.context,
id=self.runner.task.id,
time_cost=(time.time() - self.runner.start_time),
usage=self.runner.context.token_usage)
await self.runner.stop()
elif topic == TopicType.CANCEL:
# Avoid waiting to receive events and send a mock event for quick cancel
yield Message(session_id=self.runner.context.session_id, sender=self.name(), category='mock', headers={"context": message.context})
# mark task response as cancelled
self.runner._task_response = TaskResponse(answer='',
success=False,
context=message.context,
id=self.runner.task.id,
time_cost=(time.time() - self.runner.start_time),
usage=self.runner.context.token_usage,
msg=f'cancellation message received: {task_item.msg}',
status='cancelled')
await self.runner.stop()
@@ -0,0 +1,123 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
from typing import AsyncGenerator
from aworld.config import ConfigDict
from aworld.core.agent.base import is_agent
from aworld.core.common import ActionModel, TaskItem
from aworld.core.event.base import Message, Constants, TopicType
from aworld.core.tool.base import AsyncTool, Tool, ToolFactory
from aworld.logs.util import logger
from aworld.runners import HandlerFactory
from aworld.runners.handler.base import DefaultHandler
class ToolHandler(DefaultHandler):
__metaclass__ = abc.ABCMeta
def __init__(self, runner: 'TaskEventRunner'):
super().__init__(runner)
self.tools = runner.tools
self.tools_conf = runner.tools_conf
@classmethod
def name(cls):
return "_tool_handler"
@HandlerFactory.register(name=f'__{Constants.TOOL}__')
class DefaultToolHandler(ToolHandler):
def is_valid_message(self, message: Message):
if message.category != Constants.TOOL:
return False
return True
async def _do_handle(self, message: Message) -> AsyncGenerator[Message, None]:
if not self.is_valid_message(message):
return
headers = {"context": message.context}
# data is List[ActionModel]
data = message.payload
if not data:
# error message, p2p
yield Message(
category=Constants.TASK,
payload=TaskItem(msg="no data to process.", data=data, stop=True),
sender='agent_handler',
session_id=message.session_id,
topic=TopicType.ERROR,
headers=headers
)
return
for action in data:
if not isinstance(action, ActionModel):
# error message, p2p
yield Message(
category=Constants.TASK,
payload=TaskItem(msg="action not a ActionModel.", data=data, stop=True),
sender=self.name(),
session_id=message.session_id,
topic=TopicType.ERROR,
headers=headers
)
return
new_tools = dict()
tool_mapping = dict()
# Directly use or use tools after creation.
for act in data:
if is_agent(act):
logger.warning(f"somethings wrong, {act} is an agent.")
continue
if not self.tools or (self.tools and act.tool_name not in self.tools):
# dynamic only use default config in module.
conf = self.tools_conf.get(act.tool_name)
if isinstance(conf, dict):
conf = ConfigDict(conf)
tool = ToolFactory(act.tool_name, conf=conf, asyn=conf.use_async if conf else False)
tool.event_driven = True
if isinstance(tool, Tool):
tool.reset()
elif isinstance(tool, AsyncTool):
await tool.reset()
tool_mapping[act.tool_name] = []
self.tools[act.tool_name] = tool
new_tools[act.tool_name] = tool
if act.tool_name not in tool_mapping:
tool_mapping[act.tool_name] = []
tool_mapping[act.tool_name].append(act)
if new_tools:
yield Message(
category=Constants.TASK,
payload=TaskItem(data=new_tools),
sender=self.name(),
session_id=message.session_id,
topic=TopicType.SUBSCRIBE_TOOL,
headers=headers
)
for tool_name, actions in tool_mapping.items():
if not (isinstance(self.tools[tool_name], Tool) or isinstance(self.tools[tool_name], AsyncTool)):
logger.warning(f"Unsupported tool type: {self.tools[tool_name]}")
continue
# send to the tool
yield Message(
category=Constants.TOOL,
payload=actions,
sender=actions[0].agent_name if actions else '',
session_id=message.session_id,
receiver=tool_name,
headers=message.headers
)
async def post_handle(self, input:Message, output: Message) -> Message:
new_context = output.context.deep_copy()
new_context._task = output.context.get_task()
output.context = new_context
return output
@@ -0,0 +1,2 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
@@ -0,0 +1,37 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
from aworld.core.context.base import Context
from aworld.core.event.base import Message
from aworld.runners.hook.hook_factory import HookFactory
from aworld.runners.hook.hooks import PostLLMCallHook, PreLLMCallHook
from aworld.utils.common import convert_to_snake
@HookFactory.register(name="PreLLMCallContextProcessHook",
desc="PreLLMCallContextProcessHook")
class PreLLMCallContextProcessHook(PreLLMCallHook):
"""Process in the hook point of the pre_llm_call."""
__metaclass__ = abc.ABCMeta
def name(self):
return convert_to_snake("PreLLMCallContextProcessHook")
async def exec(self, message: Message, context: Context = None) -> Message:
# and do something
pass
@HookFactory.register(name="PostLLMCallContextProcessHook",
desc="PostLLMCallContextProcessHook")
class PostLLMCallContextProcessHook(PostLLMCallHook):
"""Process in the hook point of the post_llm_call."""
__metaclass__ = abc.ABCMeta
def name(self):
return convert_to_snake("PostLLMCallContextProcessHook")
async def exec(self, message: Message, context: Context = None) -> Message:
# get context
pass
@@ -0,0 +1,44 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import sys
from typing import Dict, List
from aworld.core.factory import Factory
from aworld.logs.util import logger
from aworld.runners.hook.hooks import Hook, StartHook, HookPoint
class HookManager(Factory):
def __init__(self, type_name: str = None):
super(HookManager, self).__init__(type_name)
def __call__(self, name: str, **kwargs):
if name is None:
raise ValueError("hook name is None")
try:
if name in self._cls:
act = self._cls[name](**kwargs)
else:
raise RuntimeError("The hook was not registered.\nPlease confirm the package has been imported.")
except Exception:
err = sys.exc_info()
logger.warning(f"Failed to create hook with name {name}:\n{err[1]}")
act = None
return act
def hooks(self, name: str = None) -> Dict[str, List[Hook]]:
vals = list(filter(lambda s: not s.startswith('__'), dir(HookPoint)))
results = {val.lower(): [] for val in vals}
for k, v in self._cls.items():
hook = v()
if name and hook.point() != name:
continue
results.get(hook.point(), []).append(hook)
return results
HookFactory = HookManager("hook_type")
@@ -0,0 +1,85 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
from aworld.core.context.base import Context
from aworld.core.event.base import Message
from aworld.models.model_response import ModelResponse
class HookPoint:
START = "start"
FINISHED = "finished"
ERROR = "error"
PRE_LLM_CALL = "pre_llm_call"
POST_LLM_CALL = "post_llm_call"
OUTPUT_PROCESS = "output_process"
class Hook:
"""Runner hook."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def point(self):
"""Hook point."""
@abc.abstractmethod
async def exec(self, message: Message, context: Context = None) -> Message:
"""Execute hook function."""
class StartHook(Hook):
"""Process in the hook point of the start."""
__metaclass__ = abc.ABCMeta
def point(self):
return HookPoint.START
class FinishedHook(Hook):
"""Process in the hook point of the finished."""
__metaclass__ = abc.ABCMeta
def point(self):
return HookPoint.FINISHED
class ErrorHook(Hook):
"""Process in the hook point of the error."""
__metaclass__ = abc.ABCMeta
def point(self):
return HookPoint.ERROR
class PreLLMCallHook(Hook):
"""Process in the hook point of the pre_llm_call."""
__metaclass__ = abc.ABCMeta
def point(self):
return HookPoint.PRE_LLM_CALL
class PostLLMCallHook(Hook):
"""Process in the hook point of the post_llm_call."""
__metaclass__ = abc.ABCMeta
def point(self):
return HookPoint.POST_LLM_CALL
class OutputProcessHook(Hook):
"""Output process hook for processing output data for display."""
__metaclass__ = abc.ABCMeta
def point(self):
return HookPoint.OUTPUT_PROCESS
def process_output_content(self, content: str) -> str:
"""process output content
Args:
content: original content
Returns:
processed content
"""
return content
@@ -0,0 +1,111 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import re
import copy
from typing import Dict, Any, AsyncGenerator
from aworld.core.context.base import Context
from aworld.core.event.base import Message
from aworld.logs.util import logger
from aworld.models.model_response import ModelResponse
from aworld.output.base import Output, MessageOutput
from aworld.runners.hook.hook_factory import HookFactory
from aworld.runners.hook.hooks import OutputProcessHook
from aworld.utils.common import convert_to_snake
@HookFactory.register(name="ModelResponseProcessHook",
desc="Process ModelResponse type messages before sending to frontend display")
class ModelResponseProcessHook(OutputProcessHook):
"""Process ModelResponse type messages before sending to frontend display"""
def name(self):
return convert_to_snake("ModelResponseProcessHook")
async def exec(self, message: Message, context: Context = None) -> Message:
"""Process ModelResponse type messages
Args:
message: Message object
context: Context object
Returns:
Processed message object
"""
# Get payload
if not message or not message.payload:
return message
payload = message.payload
# Process different types of payload
if isinstance(payload, ModelResponse):
# Directly process ModelResponse type
processed_payload = self.process_model_response(payload)
message.payload = processed_payload
# Record processing results
self._log_processing_result(payload, processed_payload, context)
elif isinstance(payload, MessageOutput) and hasattr(payload, 'source'):
# Process ModelResponse in MessageOutput
source = payload.source
if isinstance(source, ModelResponse):
processed_source = self.process_model_response(source)
payload.source = processed_source
# Record processing results
self._log_processing_result(source, processed_source, context)
return message
def process_model_response(self, model_response: ModelResponse) -> ModelResponse:
"""Process ModelResponse
Args:
model_response: ModelResponse object
Returns:
Processed ModelResponse object
"""
if not model_response:
return model_response
# Create a new ModelResponse object to avoid modifying the original
processed_response = copy.deepcopy(model_response)
content = self.process_output_content(processed_response.content)
processed_response.content = content
return processed_response
def _log_processing_result(self, original: ModelResponse, processed: ModelResponse, context: Context = None):
"""Record processing results
Args:
original: Original ModelResponse
processed: Processed ModelResponse
context: Context object
"""
# Record content length before and after processing for analysis
original_length = len(original.content) if original and original.content else 0
processed_length = len(processed.content) if processed and processed.content else 0
# Save processing results to context for later retrieval
if context:
if not hasattr(context, 'hook_results'):
context.hook_results = {}
if not hasattr(context.hook_results, 'output_process'):
context.hook_results.output_process = {}
# Save processing results
context.hook_results.output_process = {
'hook_name': self.name(),
'original_length': original_length,
'processed_length': processed_length,
'removed_content': original_length - processed_length,
'processed_at': context.get_current_timestamp() if hasattr(context, 'get_current_timestamp') else None,
'processing_details': {
'removed_html_tags': True,
'removed_think_tags': True
}
}
logger.info(f"ModelResponse processing result: Original length {original_length}, Processed length {processed_length}, Removed content {original_length - processed_length}")
@@ -0,0 +1,41 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
HOOK_TEMPLATE = """
import traceback
from aworld.core.context.base import Context
from aworld.core.event.base import Message, Constants, TopicType
from aworld.runners.hook.hooks import *
from aworld.runners.hook.hook_factory import HookFactory
from aworld.logs.util import logger
from aworld.utils.common import convert_to_snake
@HookFactory.register(name="{name}",
desc="{desc}")
class {name}({point}Hook):
def name(self):
return convert_to_snake("{name}")
async def exec(self, message: Message) -> Message:
{func_import}import {func}
try:
res = {func}(message)
if not res:
raise ValueError(f"{func} no result return.")
return Message(payload=res,
session_id=message.context.session_id,
sender="{name}",
category=Constants.TASK,
topic="{topic}")
except Exception as e:
logger.error(traceback.format_exc())
return Message(payload=str(e),
session_id=message.context.session_id,
sender="{name}",
category=Constants.TASK,
topic=TopicType.ERROR)
"""
@@ -0,0 +1,55 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import importlib
import inspect
import os
from typing import Callable, Any
from aworld.runners.hook.template import HOOK_TEMPLATE
from aworld.utils.common import snake_to_camel
def hook(hook_point: str, name: str = None):
"""Hook decorator.
NOTE: Hooks can be annotated, but they need to comply with the protocol agreement.
The input parameter of the hook function is `Message` type, and the @hook needs to specify `hook_point`.
Examples:
>>> @hook(hook_point=HookPoint.ERROR)
>>> def error_process(message: Message) -> Message | None:
>>> print("process error")
The function `error_process` will be executed when an error message appears in the task,
you can choose return nothing or return a message.
Args:
hook_point: Hook point that wants to process the message.
name: Hook name.
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
# converts python function into a hoop with associated hoop point
func_import = func.__module__
if func_import == '__main__':
path = inspect.getsourcefile(func)
package = path.replace(os.getcwd(), '').replace('.py', '')
if package[0] == '/':
package = package[1:]
func_import = f"from {package} "
else:
func_import = f"from {func_import} "
real_name = name if name else func.__name__
con = HOOK_TEMPLATE.format(func_import=func_import,
func=func.__name__,
point=snake_to_camel(hook_point),
name=real_name,
topic=hook_point,
desc='')
with open(f"{real_name}.py", 'w+') as write:
write.writelines(con)
importlib.import_module(real_name)
return func
return decorator
@@ -0,0 +1,824 @@
import abc
import time
import asyncio
from pydantic import BaseModel
from typing import Optional, List
from aworld.core.event.base import Message
from enum import Enum
from abc import ABC, abstractmethod, ABCMeta
from aworld.core.agent.base import is_agent_by_name
from aworld.core.tool.tool_desc import is_tool_by_name
from aworld.core.singleton import InheritanceSingleton, SingletonMeta
from aworld.core.event.base import Constants
from aworld.logs.util import logger
from aworld.events.util import send_message
class RunNodeBusiType(Enum):
AGENT = 'AGENT'
TOOL = 'TOOL'
TASK = 'TASK'
TOOL_CALLBACK = 'TOOL_CALLBACK'
HUMAN = 'HUMAN'
@staticmethod
def from_message_category(category: str) -> 'RunNodeBusiType':
if category == Constants.AGENT:
return RunNodeBusiType.AGENT
if category == Constants.TOOL:
return RunNodeBusiType.TOOL
if category == Constants.TASK:
return RunNodeBusiType.TASK
if category == Constants.TOOL_CALLBACK:
return RunNodeBusiType.TOOL_CALLBACK
if category == Constants.HUMAN:
return RunNodeBusiType.HUMAN
return None
class RunNodeStatus(Enum):
INIT = 'INIT'
RUNNING = 'RUNNING'
BREAKED = 'BREAKED'
SUCCESS = 'SUCCESS'
FAILED = 'FAILED'
TIMEOUT = 'TIMEOUT'
class HandleResult(BaseModel):
name: str = None
status: RunNodeStatus = None
result_msg: Optional[str] = None
result: Optional[Message] = None
class RunNode(BaseModel):
# {busi_id}_{busi_type}
node_id: Optional[str] = None
task_id: Optional[str] = None
busi_type: str = None
busi_id: Optional[str] = None
session_id: Optional[str] = None
msg_id: Optional[str] = None # input message id
# busi_id of node that send the input message
msg_from: Optional[str] = None
parent_node_id: Optional[str] = None
status: RunNodeStatus = None
result_msg: Optional[str] = None
results: Optional[List[HandleResult]] = None
create_time: Optional[float] = None
execute_time: Optional[float] = None
end_time: Optional[float] = None
group_id: Optional[str] = None
# sub_group_root_id required when group_id is not None
sub_group_root_id: Optional[str] = None
# metadata is used to store the context of the sub task when group_id is not None
metadata: Optional[dict] = None
def has_finished(self):
return self.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.TIMEOUT]
class SubGroup(BaseModel):
'''
SubGroup represents an execution chain pointing to the root node
'''
root_node_id: Optional[str] = None
session_id: Optional[str] = None
group_id: Optional[str] = None
create_time: Optional[float] = None
execute_time: Optional[float] = None
end_time: Optional[float] = None
status: RunNodeStatus = None
result_msg: Optional[str] = None
results: Optional[List[HandleResult]] = None
metadata: Optional[dict] = None
def has_finished(self):
return self.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.TIMEOUT]
class NodeGroup(BaseModel):
'''
Node group, used to manage sub group
'''
group_id: str = None
session_id: str = None
# subtask root node id list
root_node_ids: List[str] = None
finished: Optional[bool] = False
finish_notified: Optional[bool] = False
create_time: Optional[float] = None
execute_time: Optional[float] = None
end_time: Optional[float] = None
status: RunNodeStatus = None
# failed subtask root node id list
failed_root_node_ids: Optional[List[str]] = None
parent_group_id: Optional[str] = None
metadata: Optional[dict] = None
def has_finished(self):
return self.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.TIMEOUT]
class NodeGroupDetail(NodeGroup):
sub_groups: Optional[List[SubGroup]] = None
class StateStorage:
__metaclass__ = abc.ABCMeta
@abstractmethod
def get(self, node_id: str) -> RunNode:
pass
@abstractmethod
def insert(self, node: RunNode):
pass
@abstractmethod
def update(self, node: RunNode):
pass
@abstractmethod
def query(self, session_id: str) -> List[RunNode]:
pass
@abstractmethod
def query_by_task_id(self, task_id: str) -> List[RunNode]:
pass
class NodeGroupStorage:
__metaclass__ = abc.ABCMeta
@abstractmethod
def get(self, group_id: str) -> NodeGroup:
pass
@abstractmethod
def insert(self, node_group: NodeGroup):
pass
@abstractmethod
def update(self, node_group: NodeGroup):
pass
class SubGroupStorage:
__metaclass__ = abc.ABCMeta
@abstractmethod
def get(self, node_id: str) -> SubGroup:
pass
@abstractmethod
def insert(self, sub_group: SubGroup):
pass
@abstractmethod
def update(self, sub_group: SubGroup):
pass
class StateStorageMeta(SingletonMeta, ABCMeta):
pass
class InMemoryStateStorage(StateStorage, InheritanceSingleton, metaclass=StateStorageMeta):
'''
In memory state storage
'''
def __init__(self, max_session=1000):
self._max_session = max_session
self._nodes = {} # {node_id: RunNode}
self._ordered_session_ids = []
self._session_nodes = {} # {session_id: [RunNode, RunNode]}
def get(self, node_id: str) -> RunNode:
return self._nodes.get(node_id)
def insert(self, node: RunNode):
if node.session_id not in self._ordered_session_ids:
self._ordered_session_ids.append(node.session_id)
self._session_nodes.update({node.session_id: []})
if node.node_id not in self._nodes:
self._nodes.update({node.node_id: node})
self._session_nodes[node.session_id].append(node)
if len(self._ordered_session_ids) > self._max_session:
oldest_session_id = self._ordered_session_ids.pop(0)
session_nodes = self._session_nodes.pop(oldest_session_id)
for node in session_nodes:
self._nodes.pop(node.node_id)
# logger.info(f"storage nodes: {self._nodes}")
def update(self, node: RunNode):
self._nodes[node.node_id] = node
def query(self, session_id: str, msg_id: str = None) -> List[RunNode]:
session_nodes = self._session_nodes.get(session_id, [])
if msg_id:
return [node for node in session_nodes if node.msg_id == msg_id]
return session_nodes
def query_by_task_id(self, task_id: str) -> List[RunNode]:
return [node for node in self._nodes.values() if node.task_id == task_id]
class InMemoryNodeGroupStorage(NodeGroupStorage, InheritanceSingleton, metaclass=StateStorageMeta):
'''
In memory node group storage
'''
def __init__(self):
self.node_groups = {}
def get(self, group_id: str) -> NodeGroup:
return self.node_groups.get(group_id)
def insert(self, node_group: NodeGroup):
self.node_groups[node_group.group_id] = node_group
def update(self, node_group: NodeGroup):
self.node_groups[node_group.group_id] = node_group
class InMemorySubGroupStorage(SubGroupStorage, InheritanceSingleton, metaclass=StateStorageMeta):
'''
In memory sub task storage
'''
def __init__(self):
self.sub_groups = {}
def get(self, node_id: str) -> SubGroup:
return self.sub_groups.get(node_id)
def insert(self, sub_group: SubGroup):
self.sub_groups[sub_group.root_node_id] = sub_group
def update(self, sub_group: SubGroup):
self.sub_groups[sub_group.root_node_id] = sub_group
class RuntimeStateManager(InheritanceSingleton):
'''
Runtime state manager
'''
def __init__(self,
storage: StateStorage = InMemoryStateStorage.instance()):
self.storage = storage
self._node_group_manager = None
@property
def node_group_manager(self):
if not self._node_group_manager:
self._node_group_manager = NodeGroupManager(node_state_manager=self)
return self._node_group_manager
def create_node(self,
busi_type: RunNodeBusiType,
busi_id: str,
session_id: str,
node_id: str = None,
task_id: str = None,
parent_node_id: str = None,
msg_id: str = None,
msg_from: str = None,
group_id: str = None,
sub_group_root_id: str = None,
metadata: Optional[dict] = None) -> RunNode:
'''
create node and insert to storage
'''
node_id = node_id or msg_id
node = self._find_node(node_id)
if node:
# raise Exception(f"node already exist, node_id: {node_id}")
return
if parent_node_id:
parent_node = self._find_node(parent_node_id)
if not parent_node:
logger.warning(
f"parent node not exist, parent_node_id: {parent_node_id}")
node = RunNode(node_id=node_id,
busi_type=busi_type.name,
busi_id=busi_id,
session_id=session_id,
task_id=task_id,
msg_id=msg_id,
msg_from=msg_from,
parent_node_id=parent_node_id,
status=RunNodeStatus.INIT,
create_time=time.time(),
group_id=group_id,
sub_group_root_id=sub_group_root_id,
metadata=metadata)
self.storage.insert(node)
# create sub group if node is the root node of sub group
if group_id and sub_group_root_id and node_id == sub_group_root_id:
self.node_group_manager.create_sub_group(group_id, session_id, sub_group_root_id, metadata)
return node
def run_node(self, node_id: str):
'''
set node status to RUNNING and update to storage
'''
logger.debug(f"====== set node {node_id} running =======")
node = self._node_exist(node_id)
node.status = RunNodeStatus.RUNNING
node.execute_time = time.time()
self.storage.update(node)
# update sub group status if node is the root node
if node.group_id and node.sub_group_root_id and node.node_id == node.sub_group_root_id:
self.node_group_manager.run_sub_group(node_id)
def save_result(self,
node_id: str,
result: HandleResult):
'''
save node execute result and update to storage
'''
node = self._node_exist(node_id)
if not node.results:
node.results = []
node.results.append(result)
self.storage.update(node)
def break_node(self, node_id):
'''
set node status to BREAKED and update to storage
'''
node = self._node_exist(node_id)
node.status = RunNodeStatus.BREAKED
self.storage.update(node)
def run_succeed(self,
node_id,
result_msg=None,
results: List[HandleResult] = None):
'''
set node status to SUCCESS and update to storage
'''
node = self._node_exist(node_id)
node.status = RunNodeStatus.SUCCESS
node.result_msg = result_msg
node.end_time = time.time()
if results:
if not node.results:
node.results = []
node.results.extend(results)
logger.debug(f"====== run_succeed set node {node_id} succeed: {node} =======")
self.storage.update(node)
def run_failed(self,
node_id,
result_msg=None,
results: List[HandleResult] = None):
'''
set node status to FAILED and update to storage
'''
node = self._node_exist(node_id)
node.status = RunNodeStatus.FAILED
node.result_msg = result_msg
node.end_time = time.time()
if results:
if not node.results:
node.results = []
node.results.extend(results)
self.storage.update(node)
def run_timeout(self,
node_id,
result_msg=None):
'''
set node status to TIMEOUT and update to storage
'''
node = self._node_exist(node_id)
node.status = RunNodeStatus.TIMEOUT
node.result_msg = result_msg
self.storage.update(node)
def finish_sub_task(self, node_id: str):
'''
finish sub task with node_id as the root node
'''
node = self._node_exist(node_id)
node.sub_task_finished = True
self.storage.update(node)
def get_node(self, node_id: str) -> RunNode:
'''
get node from storage
'''
return self._find_node(node_id)
def get_nodes(self, session_id: str) -> List[RunNode]:
'''
get nodes from storage
'''
return self.storage.query(session_id)
def _node_exist(self, node_id: str):
node = self._find_node(node_id)
if not node:
raise Exception(f"node not found, node_id: {node_id}")
return node
def _find_node(self, node_id: str):
return self.storage.get(node_id)
def _judge_msg_from_busi_type(self, msg_from: str) -> RunNodeBusiType:
'''
judge msg_from busi_type
'''
if is_agent_by_name(msg_from):
return RunNodeBusiType.AGENT
if is_tool_by_name(msg_from):
return RunNodeBusiType.TOOL
return RunNodeBusiType.TASK
async def wait_for_node_completion(self, node_id: str, timeout: float = 600.0, interval: float = 1.0) -> RunNode:
'''Poll for node status until completion or timeout.
Args:
node_id: Node ID
timeout: Timeout threshold in seconds
interval: Polling interval in seconds
Returns:
RunNode: Node object
Raises:
Exception: If node does not exist
TimeoutError: If waiting times out
'''
start_time = time.time()
log_start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
logger.info(f"wait for node completion: {node_id}, start_time:{log_start_time}")
while True:
node = self._find_node(node_id)
if not node:
raise Exception(f"Node not found, node_id: {node_id}")
# Check if node has completed
if node.status in [RunNodeStatus.SUCCESS, RunNodeStatus.FAILED, RunNodeStatus.BREAKED,
RunNodeStatus.TIMEOUT]:
return node
# Check if timed out
if time.time() - start_time > timeout:
self.run_timeout(node_id, result_msg=f"Waiting for node completion timed out after {timeout} seconds")
node = self._find_node(node_id)
return node
# Wait for the specified interval before polling again
await asyncio.sleep(interval)
async def create_group(self, group_id: str,
session_id: str,
root_node_ids: List[str] = None,
parent_group_id: Optional[str] = None,
metadata: Optional[dict] = None) -> NodeGroup:
'''
create node group
'''
return await self.node_group_manager.create_group(group_id, session_id, root_node_ids, parent_group_id, metadata)
async def finish_sub_group(self,
group_id: str,
root_node_id: str,
results: List[Message] = None,
result_msg: str = None):
'''
finish sub group
'''
handle_results = []
for msg in results:
handle_result = HandleResult(
status=RunNodeStatus.FAILED if msg.is_error() else RunNodeStatus.SUCCESS,
result=msg,
name=msg.sender
)
handle_results.append(handle_result)
await self.node_group_manager.finish_sub_group(group_id, root_node_id, handle_results, result_msg)
def get_group(self, group_id: str) -> NodeGroup:
'''
get group basic info
'''
return self.node_group_manager.get_group(group_id)
def query_group_detail(self, group_id: str) -> NodeGroupDetail:
'''
query group detail info with all sub group info
'''
return self.node_group_manager.query_group_detail(group_id)
def query_by_task(self, task_id: str, busi_typ: RunNodeBusiType = None, busi_id: str = None) -> List[RunNode]:
all_task_nodes = self.storage.query_by_task_id(task_id)
if (not busi_typ and busi_id) or (busi_typ and not busi_id):
raise Exception("busi_typ and busi_id must be both None or not None")
if busi_typ and busi_id:
result_nodes = [node for node in all_task_nodes if node.busi_type == busi_typ.name and node.busi_id == busi_id]
else:
result_nodes = all_task_nodes
result_nodes.sort(key=lambda x: x.create_time if x.create_time else 0, reverse=True)
return result_nodes
class NodeGroupManager(InheritanceSingleton):
'''
Node group manager, used to manage node group
'''
def __init__(self,
sub_group_storage: SubGroupStorage = InMemorySubGroupStorage.instance(),
node_group_storage: NodeGroupStorage = InMemoryNodeGroupStorage.instance(),
node_state_manager: RuntimeStateManager = None):
self.sub_group_storage = sub_group_storage
self.node_group_storage = node_group_storage
self.node_state_manager = node_state_manager
async def create_group(self, group_id: str,
session_id: str,
root_node_ids: List[str] = None,
parent_group_id: Optional[str] = None,
metadata: Optional[dict] = None) -> NodeGroup:
'''
create node group
'''
group = self._find_group(group_id)
if group:
raise Exception(f"group already exist, group_id: {group_id}")
node_group = NodeGroup(
session_id=session_id,
group_id=group_id,
root_node_ids=root_node_ids,
parent_group_id=parent_group_id,
metadata=metadata,
create_time=time.time(),
update_time=time.time(),
status=RunNodeStatus.INIT,
)
self.node_group_storage.insert(node_group)
await self._check_subgroup_status(group_id, root_node_ids)
def create_sub_group(self,
group_id: str,
session_id: str,
root_node_id: str,
metadata: Optional[dict] = None) -> SubGroup:
'''
create sub group
'''
subgroup = self._find_subgroup(root_node_id)
if subgroup:
raise Exception(f"subgroup already exist, group_id: {group_id}, root_node_id: {root_node_id}")
run_node = self.node_state_manager.get_node(root_node_id)
if not run_node:
raise Exception(f"run node not found, root_node_id: {root_node_id}")
sub_group = SubGroup(
session_id=session_id,
group_id=group_id,
root_node_id=root_node_id,
metadata=metadata,
create_time=time.time(),
update_time=time.time(),
status=RunNodeStatus.INIT,
)
self.sub_group_storage.insert(sub_group)
return sub_group
def run_sub_group(self,
root_node_id: str):
'''
run sub group
'''
subgroup = self._subgroup_exist(root_node_id)
if not subgroup:
raise Exception(f"subgroup not found, root_node_id: {root_node_id}")
subgroup.execute_time = time.time()
subgroup.status = RunNodeStatus.RUNNING
self.sub_group_storage.update(subgroup)
self.run_group(subgroup.group_id)
def run_group(self, group_id):
group = self.node_group_storage.get(group_id)
if group.status == RunNodeStatus.INIT:
group.status = RunNodeStatus.RUNNING
group.execute_time = time.time()
self.node_group_storage.update(group)
async def finish_sub_group(self,
group_id: str,
root_node_id: str,
results: List[HandleResult] = None,
result_msg: str = None):
'''
finish sub task with node_id as the root node
'''
subgroup = self.sub_group_storage.get(root_node_id)
if not subgroup:
raise Exception(f"subgroup not found, group_id: {group_id}, root_node_id: {root_node_id}")
if subgroup.group_id != group_id:
raise Exception(f"subgroup group_id not match, group_id: {group_id}, root_node_id: {root_node_id}")
group = self._group_exist(group_id)
subgroup.end_time = time.time()
subgroup.results = results
subgroup.result_msg = result_msg
subgroup.status = RunNodeStatus.SUCCESS
for result in results:
if result.status == RunNodeStatus.FAILED:
subgroup.status = RunNodeStatus.FAILED
self.sub_group_storage.update(subgroup)
# check all subgroup status and update group status
await self._check_subgroup_status(group_id, group.root_node_ids)
async def _check_subgroup_status(self, group_id, root_node_ids: List[str]):
'''
check subgroups status and update group status, if group finished, send group finish message
'''
all_subgroups_finished = True
failed_subgroups = []
for root_node_id in root_node_ids:
subgroup = self.sub_group_storage.get(root_node_id)
if not subgroup or not subgroup.has_finished():
all_subgroups_finished = False
break
if subgroup.status == RunNodeStatus.FAILED or subgroup.status == RunNodeStatus.TIMEOUT:
failed_subgroups.append(subgroup)
if all_subgroups_finished:
group = self._group_exist(group_id)
if failed_subgroups:
group.status = RunNodeStatus.FAILED
group.failed_root_node_ids = [subgroup.root_node_id for subgroup in failed_subgroups]
else:
group.status = RunNodeStatus.SUCCESS
group.end_time = time.time()
self.node_group_storage.update(group)
await self._send_group_finish_message(group_id)
async def _send_group_finish_message(self, group_id: str):
'''
Currently, for simple implementation, concurrency control needs to be considered in a distributed environment
'''
group = self._group_exist(group_id)
if group.finish_notified:
logger.warning(f"group finish message already sent, group_id: {group_id}")
return
group_results = {}
metadata = group.metadata
for root_node_id in group.root_node_ids:
subgroup = self.sub_group_storage.get(root_node_id)
group_results[root_node_id] = subgroup.results
if not metadata:
metadata = subgroup.metadata
metadata = metadata or {}
if group.parent_group_id:
metadata.update({
"parent_group_id": group.parent_group_id
})
message = Message(
category="group",
payload=group_results,
sender="node_group_manager",
session_id=group.session_id,
topic="__group_results",
headers=metadata
)
await send_message(message)
group.finish_notified = True
self.node_group_storage.update(group)
def get_group(self, group_id: str) -> NodeGroup:
'''
get group basic info
'''
return self._find_group(group_id)
def query_group_detail(self, group_id: str) -> NodeGroupDetail:
'''
query group detail info with all sub group info
'''
group = self._find_group(group_id)
if not group:
return None
sub_groups = []
for root_node_id in group.root_node_ids:
subgroup = self._find_subgroup(root_node_id)
if subgroup:
sub_groups.append(subgroup)
return NodeGroupDetail(
group_id=group.group_id,
root_node_ids=group.root_node_ids,
parent_group_id=group.parent_group_id,
metadata=group.metadata,
create_time=group.create_time,
execute_time=group.execute_time,
end_time=group.end_time,
status=group.status,
failed_root_node_ids=group.failed_root_node_ids,
sub_groups=sub_groups
)
def _find_subgroup(self, root_node_id: str) -> SubGroup:
return self.sub_group_storage.get(root_node_id)
def _subgroup_exist(self, root_node_id: str) -> SubGroup:
subgroup = self._find_subgroup(root_node_id)
if not subgroup:
raise Exception(f"subgroup not found, root_node_id: {root_node_id}")
return subgroup
def _find_group(self, group_id: str) -> NodeGroup:
return self.node_group_storage.get(group_id)
def _group_exist(self, group_id: str) -> NodeGroup:
group = self._find_group(group_id)
if not group:
raise Exception(f"group not found, group_id: {group_id}")
return group
class EventRuntimeStateManager(RuntimeStateManager):
def __init__(self, storage: StateStorage = InMemoryStateStorage.instance()):
super().__init__(storage)
def start_message_node(self, message: Message):
'''
create and start node while message handle started.
'''
metadata = message.headers
run_node_busi_type = RunNodeBusiType.from_message_category(
message.category)
logger.debug(
f"start message node: {message.receiver}, busi_type={run_node_busi_type}, node_id={message.id}")
if run_node_busi_type:
self.create_node(
node_id=message.id,
busi_type=run_node_busi_type,
busi_id=message.receiver or "",
session_id=message.session_id,
task_id=message.task_id,
msg_id=message.id,
msg_from=message.sender,
group_id=metadata.get("group_id") if metadata else None,
sub_group_root_id=metadata.get("root_message_id") if metadata else None,
metadata=metadata)
self.run_node(message.id)
def save_message_handle_result(self, name: str, message: Message, result: Message = None):
'''
save message handle result
'''
run_node_busi_type = RunNodeBusiType.from_message_category(
message.category)
if run_node_busi_type:
if result and result.is_error():
handle_result = HandleResult(
name=name,
status=RunNodeStatus.FAILED,
result=result)
else:
handle_result = HandleResult(
name=name,
status=self.get_node(message.id).status if self.get_node(message.id) else RunNodeStatus.FAILED,
result=result)
self.save_result(node_id=message.id, result=handle_result)
def end_message_node(self, message: Message):
'''
end node while message handle finished.
'''
run_node_busi_type = RunNodeBusiType.from_message_category(
message.category)
if run_node_busi_type:
node = self._node_exist(node_id=message.id)
status = RunNodeStatus.SUCCESS
if node.results:
for result in node.results:
if result.status == RunNodeStatus.FAILED:
status = RunNodeStatus.FAILED
break
if status == RunNodeStatus.FAILED:
self.run_failed(node_id=message.id)
else:
self.run_succeed(node_id=message.id)
def get_message_node_status(self, message: Message) -> RunNodeStatus:
node = self.get_node(node_id=message.id)
if not node:
return RunNodeStatus.INIT
return node.status
@@ -0,0 +1,160 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
import abc
import os
import time
import uuid
from typing import Callable, Any
from pydantic import BaseModel
import aworld.tools
from aworld.config import ConfigDict
from aworld.config.conf import ToolConfig
from aworld.core.agent.swarm import Swarm
from aworld.core.common import Observation
from aworld.core.context.base import Context
from aworld.core.context.session import Session
from aworld.core.tool.base import Tool, AsyncTool
from aworld.core.task import Task, TaskResponse, Runner
from aworld.logs.util import logger
from aworld import trace, cleanup
from aworld.utils.common import load_module_by_path
class TaskRunner(Runner):
"""Task based runner api class."""
__metaclass__ = abc.ABCMeta
def __init__(self,
task: Task,
*,
agent_oriented: bool = True,
daemon_target: Callable[..., Any] = None):
"""Task runner initialize.
Args:
task: Task entity to be executed.
agent_oriented: Is it an agent oriented task, default is True.
"""
if task.tools is None:
task.tools = []
if task.tool_names is None:
task.tool_names = []
if agent_oriented:
if not task.agent and not task.swarm:
raise ValueError("agent and swarm all is None.")
if task.agent and task.swarm:
logger.warning("agent and swarm all is not None.")
raise ValueError("agent and swarm choose one only.")
if task.agent:
# uniform agent
task.swarm = Swarm(task.agent)
if task.conf is None:
task.conf = dict()
if isinstance(task.conf, BaseModel):
task.conf = task.conf.model_dump()
task.conf = ConfigDict(task.conf)
check_input = task.conf.get("check_input", False)
if check_input and not task.input:
raise ValueError("task no input")
if not task.is_sub_task:
self.context = task.context if task.context else Context()
self.context.set_task(task)
self.task = task
self.agent_oriented = agent_oriented
self.daemon_target = daemon_target
self._use_demon = False if not task.conf else task.conf.get(
'use_demon', False)
self._exception = None
self.start_time = time.time()
self.step_agent_counter = {}
async def pre_run(self):
task = self.task
# copy context from parent_task(if exists)
if task.is_sub_task:
task.context = await task.context.build_sub_context(
task.input, task.id,
agents=task.swarm.agents if task.swarm and task.swarm.agents else None
)
self.context = task.context
self.context.set_task(task)
self.swarm = task.swarm
self.input = task.input
self.outputs = task.outputs
self.name = task.name
self.conf = task.conf if task.conf else ConfigDict()
self.tools = {
tool.name(): tool for tool in task.tools} if task.tools else {}
task.tool_names.extend(self.tools.keys())
# lazy load
self.tool_names = task.tool_names
self.tools_conf = task.tools_conf
if self.tools_conf is None:
self.tools_conf = {}
# mcp performs special process, use async only in the runn
self.tools_conf['mcp'] = ToolConfig(use_async=True, name='mcp')
self.endless_threshold = task.endless_threshold
# build context
if task.session_id:
session = Session(session_id=task.session_id)
else:
session = Session(session_id=uuid.uuid4().hex)
trace_id = uuid.uuid1().hex if trace.get_current_span(
) is None else trace.get_current_span().get_trace_id()
self.context.task_id = self.task.id
self.context.trace_id = trace_id
self.context.session = session
self.context.swarm = self.swarm
# init tool state by reset(), and ignore them observation
observation = None
if self.tools:
for _, tool in self.tools.items():
# use the observation and info of the last one
if isinstance(tool, Tool):
tool.context = self.context
observation, info = tool.reset()
elif isinstance(tool, AsyncTool):
observation, info = await tool.reset()
else:
logger.warning(f"Unsupported tool type: {tool}, will ignored.")
if observation:
if not observation.content:
observation.content = self.input
else:
observation = Observation(content=self.input)
self.observation = observation
if self.swarm:
self.swarm.event_driven = task.event_driven
self.swarm.reset(observation.content,
context=self.context, tools=self.tool_names)
self._load_tool_module()
logger.info(f'{"sub task: " if self.task.is_sub_task else "main task: "}{self.task.id} started...')
def _load_tool_module(self):
# used to distributed running local tools
try:
value = os.environ.get(aworld.tools.LOCAL_TOOLS_ENV_VAR, '')
if value:
for val in value.split(";"):
load_module_by_path(os.path.basename(val).replace("_action", ""),
val.replace("_action.py", ".py"))
load_module_by_path(os.path.basename(val), val)
except:
logger.warning(f"{os.environ.get(aworld.tools.LOCAL_TOOLS_ENV_VAR, '')} tools load fail, can't use them!!")
async def post_run(self):
pass
@abc.abstractmethod
async def do_run(self, context: Context = None) -> TaskResponse:
"""Task do run."""
@@ -0,0 +1,124 @@
# coding: utf-8
# Copyright (c) 2025 inclusionAI.
from typing import List, Dict
from aworld.config import RunConfig, EngineName, ConfigDict
from aworld.core.agent.swarm import GraphBuildType
from aworld.core.task import Task, TaskResponse, Runner
from aworld.logs.util import logger
from aworld.runners.task_runner import TaskRunner
from aworld.utils.common import new_instance, snake_to_camel
async def choose_runners(tasks: List[Task], agent_oriented: bool = True) -> List[Runner]:
"""Choose the correct runner to run the task.
Args:
task: A task that contains agents, tools and datas.
Returns:
Runner instance or exception.
"""
runners = []
for task in tasks:
# user custom runner class
runner_cls = task.runner_cls
if runner_cls:
return new_instance(runner_cls, task)
else:
# user runner class in the framework
if task.swarm:
task.swarm.event_driven = task.event_driven
execute_type = task.swarm.build_type
else:
execute_type = GraphBuildType.WORKFLOW.value
if task.event_driven:
runner = new_instance("aworld.runners.event_runner.TaskEventRunner",
task,
agent_oriented=agent_oriented)
else:
runner = new_instance(
f"aworld.runners.call_driven_runner.{snake_to_camel(execute_type)}Runner",
task
)
runners.append(runner)
return runners
async def execute_runner(runners: List[Runner], run_conf: RunConfig) -> Dict[str, TaskResponse]:
"""Execute runner in the runtime engine.
Args:
runners: The task processing flow.
run_conf: Runtime config, can choose the special computing engine to execute the runner.
"""
if not run_conf:
run_conf = RunConfig()
name = run_conf.engine_name
if run_conf.cls:
runtime_backend = new_instance(run_conf.cls, run_conf)
else:
runtime_backend = new_instance(
f"aworld.core.runtime_engine.{snake_to_camel(name)}Runtime", run_conf)
runtime_engine = runtime_backend.build_engine()
if run_conf.engine_name != EngineName.LOCAL or run_conf.reuse_process == False:
# distributed in AWorld, the `context` can't carry by response
for runner in runners:
if not isinstance(runner, TaskRunner):
logger.info("not task runner in AWorld, skip...")
continue
if runner.task.conf:
runner.task.conf.resp_carry_context = False
else:
runner.task.conf = ConfigDict(resp_carry_context=False)
return await runtime_engine.execute([runner.run for runner in runners])
def endless_detect(records: List[str], endless_threshold: int, root_agent_name: str):
"""A very simple implementation of endless loop detection.
Args:
records: Call sequence of agent.
endless_threshold: Threshold for the number of repetitions.
root_agent_name: Name of the entrance agent.
"""
if not records:
return False
threshold = endless_threshold
last_agent_name = root_agent_name
count = 1
for i in range(len(records) - 2, -1, -1):
if last_agent_name == records[i]:
count += 1
else:
last_agent_name = records[i]
count = 1
if count >= threshold:
logger.warning("detect loop, will exit the loop.")
return True
if len(records) > 6:
last_agent_name = None
# latest
for j in range(1, 3):
for i in range(len(records) - j, 0, -2):
if last_agent_name and last_agent_name == (records[i], records[i - 1]):
count += 1
elif last_agent_name is None:
last_agent_name = (records[i], records[i - 1])
count = 1
else:
last_agent_name = None
break
if count >= threshold:
logger.warning(f"detect loop: {last_agent_name}, will exit the loop.")
return True
return False