mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Reuse native parallel child logs
Pass each parent-visible child log into direct parallel worker execution so tool before_execution reuses it instead of creating a second generic tool log. This keeps native badge types such as wait/progress (HDL) intact through execution, updates the parallel helper DOX contract, and adds a regression test for log reuse.
This commit is contained in:
parent
8d79f556c6
commit
fa05710da7
3 changed files with 82 additions and 2 deletions
|
|
@ -487,13 +487,24 @@ async def _run_direct_tool_job(parent_context_id: str, job: ParallelJob) -> str:
|
||||||
|
|
||||||
worker_agent = worker_context.agent0
|
worker_agent = worker_context.agent0
|
||||||
worker_agent.loop_data = LoopData()
|
worker_agent.loop_data = LoopData()
|
||||||
return await execute_tool_call(worker_agent, job.tool_name, job.tool_args)
|
return await execute_tool_call(
|
||||||
|
worker_agent,
|
||||||
|
job.tool_name,
|
||||||
|
job.tool_args,
|
||||||
|
log_item=job.log_item,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
if worker_context:
|
if worker_context:
|
||||||
await _remove_context(worker_context.id)
|
await _remove_context(worker_context.id)
|
||||||
|
|
||||||
|
|
||||||
async def execute_tool_call(agent: "Agent", tool_name: str, tool_args: dict[str, Any]) -> str:
|
async def execute_tool_call(
|
||||||
|
agent: "Agent",
|
||||||
|
tool_name: str,
|
||||||
|
tool_args: dict[str, Any],
|
||||||
|
*,
|
||||||
|
log_item: "LogItem | None" = None,
|
||||||
|
) -> str:
|
||||||
if tool_name == "parallel":
|
if tool_name == "parallel":
|
||||||
raise ValueError("`parallel` cannot be nested inside a parallel worker.")
|
raise ValueError("`parallel` cannot be nested inside a parallel worker.")
|
||||||
|
|
||||||
|
|
@ -501,6 +512,11 @@ async def execute_tool_call(agent: "Agent", tool_name: str, tool_args: dict[str,
|
||||||
if not tool:
|
if not tool:
|
||||||
raise ValueError(f"Tool '{tool_name}' not found or could not be initialized.")
|
raise ValueError(f"Tool '{tool_name}' not found or could not be initialized.")
|
||||||
|
|
||||||
|
original_get_log_object = None
|
||||||
|
if log_item is not None:
|
||||||
|
original_get_log_object = tool.get_log_object
|
||||||
|
tool.get_log_object = lambda: log_item
|
||||||
|
|
||||||
agent.loop_data.current_tool = tool
|
agent.loop_data.current_tool = tool
|
||||||
try:
|
try:
|
||||||
await agent.handle_intervention()
|
await agent.handle_intervention()
|
||||||
|
|
@ -524,6 +540,8 @@ async def execute_tool_call(agent: "Agent", tool_name: str, tool_args: dict[str,
|
||||||
await agent.handle_intervention()
|
await agent.handle_intervention()
|
||||||
return response.message
|
return response.message
|
||||||
finally:
|
finally:
|
||||||
|
if original_get_log_object is not None:
|
||||||
|
tool.get_log_object = original_get_log_object
|
||||||
agent.loop_data.current_tool = None
|
agent.loop_data.current_tool = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
- Parent-visible child log items are created for each wrapped call so the WebUI can inspect concurrent children separately while the wrapper result remains model-history-only.
|
- Parent-visible child log items are created for each wrapped call so the WebUI can inspect concurrent children separately while the wrapper result remains model-history-only.
|
||||||
- Child tool logs mirror normal tool-call visible args; job ids remain available through wrapper results and prompt extras rather than visible process-step args.
|
- Child tool logs mirror normal tool-call visible args; job ids remain available through wrapper results and prompt extras rather than visible process-step args.
|
||||||
- Wrapped tool child logs use each tool's native `get_log_object()` output when available, preserving special log rendering (for example: `code_execution_tool` uses `code_exe`, `wait` uses `progress`, MCP tools use `mcp`, and regular tools use `tool`).
|
- Wrapped tool child logs use each tool's native `get_log_object()` output when available, preserving special log rendering (for example: `code_execution_tool` uses `code_exe`, `wait` uses `progress`, MCP tools use `mcp`, and regular tools use `tool`).
|
||||||
|
- Direct parallel worker execution reuses the parent-visible child log item so tool `before_execution()` cannot create a second generic worker log or lose the native badge type.
|
||||||
- Job IDs are stable handles for later await, collect, or cancel operations.
|
- Job IDs are stable handles for later await, collect, or cancel operations.
|
||||||
- Prompt extras must stay bounded and expose only job IDs, tool names, status, and compact result/error summaries.
|
- Prompt extras must stay bounded and expose only job IDs, tool names, status, and compact result/error summaries.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -551,6 +551,67 @@ async def test_parallel_wait_child_uses_wait_log_type(monkeypatch) -> None:
|
||||||
assert agent.context.log.items[0].kvps == {"seconds": 1}
|
assert agent.context.log.items[0].kvps == {"seconds": 1}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parallel_execute_reuses_child_log_object(monkeypatch) -> None:
|
||||||
|
class FakeTool:
|
||||||
|
def __init__(self, agent, args):
|
||||||
|
self.agent = agent
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def get_log_object(self):
|
||||||
|
return self.agent.context.log.log(
|
||||||
|
type="tool",
|
||||||
|
heading="generic tool log",
|
||||||
|
content="",
|
||||||
|
kvps=self.args,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def before_execution(self, **kwargs):
|
||||||
|
self.log = self.get_log_object()
|
||||||
|
|
||||||
|
async def execute(self, **kwargs):
|
||||||
|
return Response(message="done", break_loop=False)
|
||||||
|
|
||||||
|
async def after_execution(self, response):
|
||||||
|
self.log.update(content=response.message)
|
||||||
|
|
||||||
|
class FakeWorkerAgent(_FakeAgent):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.loop_data = SimpleNamespace(current_tool=None)
|
||||||
|
|
||||||
|
def get_tool(self, **kwargs):
|
||||||
|
return FakeTool(self, kwargs["args"])
|
||||||
|
|
||||||
|
async def handle_intervention(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def noop_extensions(*_args, **_kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setattr(parallel_tools, "call_extensions_async", noop_extensions)
|
||||||
|
|
||||||
|
agent = FakeWorkerAgent()
|
||||||
|
child_log = agent.context.log.log(
|
||||||
|
type="progress",
|
||||||
|
heading="icon://timer Wait: Waiting...",
|
||||||
|
content="",
|
||||||
|
kvps={"seconds": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await parallel_tools.execute_tool_call(
|
||||||
|
agent, # type: ignore[arg-type]
|
||||||
|
"wait",
|
||||||
|
{"seconds": 1},
|
||||||
|
log_item=child_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == "done"
|
||||||
|
assert agent.context.log.items == [child_log]
|
||||||
|
assert child_log.type == "progress"
|
||||||
|
assert child_log.content == "done"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_parallel_tool_keeps_wrapper_out_of_visible_log() -> None:
|
async def test_parallel_tool_keeps_wrapper_out_of_visible_log() -> None:
|
||||||
from tools.parallel import ParallelTool
|
from tools.parallel import ParallelTool
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue