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