Render parallel code execution as EXE

Use the code_exe log type for code_execution_tool children launched through parallel so the WebUI reuses the existing code execution renderer. Add a focused regression test and document the parallel child-log contract.

Alessandro committed Jul 7, 2026 at 13:56 UTC 9bb3cb9469c7b9fbc6a1243f4b991c68deb0d91c
3 files changed +60
helpers/parallel_tools.py
+13
@@ -600,6 +600,19 @@ def _log_parallel_child_started(agent: "Agent", job: ParallelJob) -> None:
600 )
601 return
602
603 + if job.tool_name == "code_execution_tool":
604 + runtime = job.tool_args.get("runtime", "unknown")
605 + session = job.tool_args.get("session", None)
606 + session_text = f"[{session}] " if session or session == 0 else ""
607 + job.log_item = agent.context.log.log(
608 + type="code_exe",
609 + heading=f"icon://terminal {session_text}code_execution_tool - {runtime}",
610 + content="",
611 + kvps=job.tool_args,
612 + id=job.log_id,
613 + )
614 + return
615 +
616 heading = f"icon://construction {agent.agent_name}: Using tool '{job.tool_name}'"
617 job.log_item = agent.context.log.log(
618 type="tool",
helpers/parallel_tools.py.dox.md
+1
@@ -31,6 +31,7 @@
31 - Direct tool background context cleanup removes both the in-memory context and any transient chat folder left on disk.
32 - 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.
33 - 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.
34 +- Wrapped `code_execution_tool` child logs use the normal `code_exe` WebUI message type and terminal heading so they render like direct code execution calls.
35 - Job IDs are stable handles for later await, collect, or cancel operations.
36 - Prompt extras must stay bounded and expose only job IDs, tool names, status, and compact result/error summaries.
37
tests/test_parallel_tool.py
+46
@@ -427,6 +427,52 @@ async def test_parallel_direct_tool_jobs_log_normal_tool_metadata(monkeypatch) -
427 assert agent.context.log.items[0].kvps == {"seconds": 1, "_tool_name": "wait"}
428
429
430 +@pytest.mark.asyncio
431 +async def test_parallel_code_execution_child_uses_code_exe_log_type(monkeypatch) -> None:
432 + class FakeDeferredTask:
433 + def __init__(self, thread_name=None) -> None:
434 + self.thread_name = thread_name
435 +
436 + def start_task(self, func, *args):
437 + return self
438 +
439 + def is_ready(self):
440 + return False
441 +
442 + def is_alive(self):
443 + return True
444 +
445 + def kill(self):
446 + pass
447 +
448 + monkeypatch.setattr(parallel_tools, "DeferredTask", FakeDeferredTask)
449 + agent = _FakeAgent()
450 +
451 + jobs = await parallel_tools.start_parallel_jobs(
452 + agent, # type: ignore[arg-type]
453 + [
454 + parallel_tools.NormalizedToolCall(
455 + index=0,
456 + tool_name="code_execution_tool",
457 + tool_args={
458 + "runtime": "terminal",
459 + "session": 0,
460 + "code": "pwd",
461 + },
462 + )
463 + ],
464 + )
465 +
466 + assert jobs[0].kind == "tool"
467 + assert agent.context.log.items[0].type == "code_exe"
468 + assert agent.context.log.items[0].heading == "icon://terminal [0] code_execution_tool - terminal"
469 + assert agent.context.log.items[0].kvps == {
470 + "runtime": "terminal",
471 + "session": 0,
472 + "code": "pwd",
473 + }
474 +
475 +
476 @pytest.mark.asyncio
477 async def test_parallel_tool_keeps_wrapper_out_of_visible_log() -> None:
478 from tools.parallel import ParallelTool