agent-zero/tests/test_parallel_tool.py
Alessandro a304c7665f Unify subordinate lifecycle across parallel calls
Route direct and parallel call_subordinate execution through one reusable, persisted A1/A2 child-context lifecycle.

Keep failed children resumable with reset=false and cover sibling reuse, recursive numbering, persistence, and nested task ownership.
2026-08-19 09:30:50 +02:00

1073 lines
35 KiB
Python

from __future__ import annotations
import json
import time
import sys
from types import SimpleNamespace
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from helpers import parallel_tools
from helpers.tool import Response
class _FakeLogItem:
def __init__(self, type_, heading="", content="", kvps=None, id_=None, **kwargs) -> None:
self.type = type_
self.heading = heading
self.content = content
self.kvps = dict(kvps or {})
self.kvps.update(kwargs)
self.id = id_
def update(self, content=None, kvps=None, **kwargs):
if content is not None:
self.content = content
if kvps:
self.kvps.update(kvps)
self.kvps.update(kwargs)
class _FakeLog:
def __init__(self) -> None:
self.items = []
def log(self, type, heading="", content="", kvps=None, id=None, **kwargs):
item = _FakeLogItem(type, heading, content, kvps, id, **kwargs)
self.items.append(item)
return item
class _FakeContext:
def __init__(self) -> None:
self.id = "ctx"
self.data = {}
self.log = _FakeLog()
self.task = None
def get_data(self, key: str, recursive: bool = True):
return self.data.get(key)
def set_data(self, key: str, value, recursive: bool = True):
self.data[key] = value
class _FakeAgent:
def __init__(self) -> None:
self.context = _FakeContext()
self.agent_name = "A0"
self.number = 0
class _FakeDeferredTask:
def __init__(
self,
*,
ready: bool = False,
alive: bool = True,
result=None,
thread_name=None,
) -> None:
self.ready = ready
self.alive = alive
self._result = result
self.killed = 0
self.thread_name = thread_name
self.started = None
self.children = []
def start_task(self, func, *args):
self.started = (func, args)
return self
def is_ready(self):
return self.ready
def is_alive(self):
return self.alive
async def result(self):
return self._result
def kill(self):
self.killed += 1
self.alive = False
for child in self.children:
child.kill()
self.children = []
def add_child_task(self, task, terminate_thread=False):
self.children.append(task)
def test_normalize_parallel_tool_calls_accepts_normal_tool_request_shapes() -> None:
calls = parallel_tools.normalize_parallel_tool_calls(
[
{
"tool_name": "text_editor:read",
"tool_args": {"path": "README.md"},
},
{
"tool": "scheduler",
"args": {"method": "list_tasks"},
},
]
)
assert calls[0].tool_name == "text_editor"
assert calls[0].tool_args == {"path": "README.md", "action": "read"}
assert calls[1].tool_name == "scheduler"
assert calls[1].tool_args == {"method": "list_tasks", "action": "list_tasks"}
def test_normalize_parallel_tool_calls_accepts_json_string_array() -> None:
calls = parallel_tools.normalize_parallel_tool_calls(
json.dumps(
[
{
"tool_name": "call_subordinate",
"tool_args": {
"profile": "researcher",
"reset": True,
"message": "Research nuclear fusion news in French.",
},
"headline": "Researching nuclear fusion news in French",
},
{
"tool_name": "call_subordinate",
"tool_args": {
"profile": "researcher",
"reset": True,
"message": "Research nuclear fusion news in Italian.",
},
"headline": "Researching nuclear fusion news in Italian",
},
]
)
)
assert [call.tool_name for call in calls] == [
"call_subordinate",
"call_subordinate",
]
assert calls[0].tool_args["profile"] == "researcher"
assert calls[0].tool_args["reset"] is True
assert calls[1].tool_args["message"] == "Research nuclear fusion news in Italian."
def test_subordinate_prompts_share_reusable_tree_contract() -> None:
call_prompt = (PROJECT_ROOT / "prompts/agent.system.tool.call_sub.md").read_text(
encoding="utf-8"
)
parallel_prompt = (PROJECT_ROOT / "prompts/agent.system.tool.parallel.md").read_text(
encoding="utf-8"
)
assert "A0 creates A1 children, A1 creates A2 children" in call_prompt
assert "stable child ID" in call_prompt
assert "same child lifecycle here as it does top-level" in parallel_prompt
assert "each job's `context_id`" in parallel_prompt
def test_normalize_parallel_tool_calls_rejects_nested_parallel() -> None:
with pytest.raises(ValueError, match="cannot be nested"):
parallel_tools.normalize_parallel_tool_calls(
[{"tool_name": "parallel", "tool_args": {"tool_calls": []}}]
)
@pytest.mark.parametrize("tool_name", ["document_query", "response"])
def test_normalize_parallel_tool_calls_rejects_disallowed_tools(tool_name: str) -> None:
with pytest.raises(ValueError, match=rf"{tool_name}.*parallel"):
parallel_tools.normalize_parallel_tool_calls(
[
{
"tool_name": tool_name,
"tool_args": {},
}
]
)
@pytest.mark.asyncio
async def test_parallel_jobs_extras_lists_running_and_ready_jobs() -> None:
agent = _FakeAgent()
running = parallel_tools.ParallelJob(
id="search-1234abcd",
parent_context_id="ctx",
index=0,
tool_name="search_engine",
tool_args={"query": "Agent Zero"},
kind="tool",
state="running",
started_at=time.time() - 2,
)
ready = parallel_tools.ParallelJob(
id="callsubordin-5678efgh",
parent_context_id="ctx",
index=1,
tool_name="call_subordinate",
tool_args={"message": "Summarize"},
kind="subordinate",
state="success",
started_at=time.time() - 4,
completed_at=time.time() - 1,
result="done",
)
agent.context.set_data(
parallel_tools.PARALLEL_JOBS_KEY,
{running.id: running, ready.id: ready},
)
extras = await parallel_tools.build_parallel_jobs_extras(agent) # type: ignore[arg-type]
assert "search-1234abcd" in extras
assert "callsubordin-5678efgh" in extras
assert "ready to collect" in extras
@pytest.mark.asyncio
async def test_parallel_await_timeout_keeps_running_jobs_awaitable(monkeypatch) -> None:
agent = _FakeAgent()
task = _FakeDeferredTask(alive=True)
job = parallel_tools.ParallelJob(
id="wait-1234abcd",
parent_context_id="ctx",
index=0,
tool_name="wait",
tool_args={"seconds": 60},
kind="tool",
state="running",
created_at=99.0,
started_at=99.0,
deferred_task=task, # type: ignore[arg-type]
)
agent.context.set_data(parallel_tools.PARALLEL_JOBS_KEY, {job.id: job})
times = iter([100.0, 102.0])
monkeypatch.setattr(
parallel_tools.time,
"time",
lambda: next(times, 102.0),
)
results = await parallel_tools.await_parallel_jobs( # type: ignore[arg-type]
agent,
[job.id],
timeout=1,
collect=True,
wait=True,
)
payload = json.loads(parallel_tools.format_parallel_results(results))
assert results[0]["state"] == "running"
assert results[0]["wait_timed_out"] is True
assert payload["status"] == "waiting"
assert payload["wait_timeout"] is True
assert "await" in payload["instruction"]
assert task.killed == 0
assert agent.context.get_data(parallel_tools.PARALLEL_JOBS_KEY)[job.id] is job
@pytest.mark.asyncio
async def test_parallel_collect_returns_running_jobs_without_waiting_or_canceling() -> None:
from tools.parallel import ParallelTool
agent = _FakeAgent()
task = _FakeDeferredTask(alive=True)
job = parallel_tools.ParallelJob(
id="wait-collect",
parent_context_id="ctx",
index=0,
tool_name="wait",
tool_args={"seconds": 60},
kind="tool",
state="running",
deferred_task=task, # type: ignore[arg-type]
)
agent.context.set_data(parallel_tools.PARALLEL_JOBS_KEY, {job.id: job})
tool = ParallelTool(
agent, # type: ignore[arg-type]
"parallel",
None,
{"action": "collect", "job_ids": [job.id]},
"",
None,
)
response = await tool.execute(**tool.args)
payload = json.loads(response.message)
assert payload["status"] == "running"
assert payload["jobs"][0]["job_id"] == job.id
assert "wait_timeout" not in payload
assert task.killed == 0
assert agent.context.get_data(parallel_tools.PARALLEL_JOBS_KEY)[job.id] is job
@pytest.mark.asyncio
async def test_parallel_cancel_still_stops_and_removes_running_jobs() -> None:
agent = _FakeAgent()
task = _FakeDeferredTask(alive=True)
job = parallel_tools.ParallelJob(
id="wait-cancel",
parent_context_id="ctx",
index=0,
tool_name="wait",
tool_args={"seconds": 60},
kind="tool",
state="running",
deferred_task=task, # type: ignore[arg-type]
)
agent.context.set_data(parallel_tools.PARALLEL_JOBS_KEY, {job.id: job})
results = await parallel_tools.cancel_parallel_jobs(agent, [job.id]) # type: ignore[arg-type]
payload = json.loads(parallel_tools.format_parallel_results(results))
assert results[0]["state"] == "cancelled"
assert payload["status"] == "cancelled"
assert task.killed == 1
assert job.id not in agent.context.get_data(parallel_tools.PARALLEL_JOBS_KEY)
@pytest.mark.asyncio
async def test_parallel_remove_context_deletes_persisted_worker_chat(monkeypatch) -> None:
removed = []
from helpers import persist_chat
monkeypatch.setattr(persist_chat, "remove_chat", removed.append)
await parallel_tools._remove_context("missing-worker")
assert removed == ["missing-worker"]
@pytest.mark.asyncio
async def test_parallel_recursion_guard_allows_subordinate_children_but_blocks_tool_workers() -> None:
from extensions.python.tool_execute_before._20_block_parallel_recursion import (
BlockParallelRecursion,
)
from helpers.errors import RepairableException
agent = _FakeAgent()
agent.context.set_data(parallel_tools.PARALLEL_WORKER_JOB_KEY, "legacy-job")
assert parallel_tools.is_parallel_worker(agent) is True # type: ignore[arg-type]
agent.context.set_data(parallel_tools.PARALLEL_WORKER_KIND_KEY, "subordinate")
assert parallel_tools.is_parallel_worker(agent) is False # type: ignore[arg-type]
await BlockParallelRecursion(agent=agent).execute(tool_name="parallel") # type: ignore[arg-type]
agent.context.set_data(parallel_tools.PARALLEL_WORKER_KIND_KEY, "tool")
assert parallel_tools.is_parallel_worker(agent) is True # type: ignore[arg-type]
with pytest.raises(RepairableException, match="cannot be used inside a parallel worker"):
await BlockParallelRecursion(agent=agent).execute(tool_name="parallel") # type: ignore[arg-type]
@pytest.mark.asyncio
async def test_parallel_subordinate_jobs_are_visible_child_logs_not_scheduler_tasks(monkeypatch) -> None:
class FakeDeferredTask:
def __init__(self, thread_name=None) -> None:
self.thread_name = thread_name
self.started = None
def start_task(self, func, *args):
self.started = (func, args)
return self
def is_ready(self):
return False
def is_alive(self):
return True
def kill(self):
pass
monkeypatch.setattr(parallel_tools, "DeferredTask", FakeDeferredTask)
agent = _FakeAgent()
jobs = await parallel_tools.start_parallel_jobs(
agent, # type: ignore[arg-type]
[
parallel_tools.NormalizedToolCall(
index=0,
tool_name="call_subordinate",
tool_args={
"profile": "developer",
"message": "Return ALPHA=1",
"reset": True,
},
)
],
)
assert jobs[0].kind == "subordinate"
snapshot = parallel_tools._job_snapshot(jobs[0], include_result=False)
assert "scheduler_task_uuid" not in snapshot
assert agent.context.log.items[0].type == "subagent"
assert agent.context.log.items[0].kvps == {
"profile": "developer",
"message": "Return ALPHA=1",
"reset": True,
}
assert "id" not in agent.context.log.items[0].kvps
assert "tool_name" not in agent.context.log.items[0].kvps
assert "parallel_child" not in agent.context.log.items[0].kvps
@pytest.mark.asyncio
async def test_parallel_subordinate_enforces_parent_delegation_policy(
monkeypatch,
) -> None:
from agent import AgentContext
from helpers import tool_policy
from helpers.errors import RepairableException
parent_agent = SimpleNamespace(
config=SimpleNamespace(profile="restricted"),
context=_FakeContext(),
)
parent_context = SimpleNamespace(agent0=parent_agent)
monkeypatch.setattr(
AgentContext,
"get",
staticmethod(lambda _context_id: parent_context),
)
monkeypatch.setattr(tool_policy.subagents, "get_paths", lambda *args, **kwargs: [])
monkeypatch.setattr(
tool_policy,
"get_policy",
lambda agent: {
"mode": "custom",
"default": "allow",
"allowed": [],
"blocked": ["local:call_subordinate"],
},
)
job = parallel_tools.ParallelJob(
id="callsubordin-blocked",
parent_context_id="ctx",
index=0,
tool_name="call_subordinate",
tool_args={"profile": "developer", "message": "Work"},
kind="subordinate",
)
with pytest.raises(
RepairableException,
match='Tool "call_subordinate" is blocked for agent profile "restricted"',
):
await parallel_tools._run_subordinate_context_job("ctx", job)
@pytest.mark.asyncio
async def test_parallel_subordinate_reuses_profile_validation(monkeypatch) -> None:
from agent import AgentContext
from helpers import tool_policy
from helpers.errors import RepairableException
from tools import call_subordinate
parent_agent = SimpleNamespace(
config=SimpleNamespace(profile="agent0"),
context=_FakeContext(),
)
parent_context = SimpleNamespace(agent0=parent_agent)
monkeypatch.setattr(
AgentContext,
"get",
staticmethod(lambda _context_id: parent_context),
)
monkeypatch.setattr(tool_policy.subagents, "get_paths", lambda *args, **kwargs: [])
monkeypatch.setattr(
tool_policy,
"get_policy",
lambda agent: {
"mode": "inherit",
"default": "allow",
"allowed": [],
"blocked": [],
},
)
monkeypatch.setattr(
call_subordinate.subagents,
"get_available_agents_dict",
lambda project_name: {"developer": SimpleNamespace(title="Developer")},
)
job = parallel_tools.ParallelJob(
id="callsubordin-invalid",
parent_context_id="ctx",
index=0,
tool_name="call_subordinate",
tool_args={"profile": "ghost", "message": "Work"},
kind="subordinate",
)
with pytest.raises(RepairableException, match="Agent profile 'ghost' not found"):
await parallel_tools._run_subordinate_context_job("ctx", job)
@pytest.mark.asyncio
async def test_parallel_subordinates_are_distinct_reusable_a1_children(monkeypatch) -> None:
from agent import Agent, AgentConfig, AgentContext
from helpers import message_queue, persist_chat, tool_policy
parent_id = "ctx-parallel-a1-tree"
AgentContext.remove(parent_id)
parent = AgentContext(
AgentConfig(mcp_servers="", profile="agent0"),
id=parent_id,
set_current=False,
)
async def fake_monologue(agent):
return agent.agent_name
monkeypatch.setattr(Agent, "monologue", fake_monologue)
monkeypatch.setattr(tool_policy, "ensure_tool_allowed", lambda *_args, **_kwargs: None)
monkeypatch.setattr(message_queue, "log_user_message", lambda *_args, **_kwargs: None)
monkeypatch.setattr(persist_chat, "save_tmp_chat", lambda _context: None)
child_ids = []
try:
jobs = await parallel_tools.start_parallel_jobs(
parent.agent0,
[
parallel_tools.NormalizedToolCall(
index=0,
tool_name="call_subordinate",
tool_args={"message": "left branch", "reset": True},
),
parallel_tools.NormalizedToolCall(
index=1,
tool_name="call_subordinate",
tool_args={"message": "right branch", "reset": True},
),
],
)
results = await parallel_tools.await_parallel_jobs(
parent.agent0,
[job.id for job in jobs],
timeout=10,
)
child_ids = [result["context_id"] for result in results]
assert [result["state"] for result in results] == ["success", "success"]
assert [result["result"] for result in results] == ["A1", "A1"]
assert len(set(child_ids)) == 2
assert set(parent.agent0.get_data("_subordinates")) == set(child_ids)
for child_id in child_ids:
child = AgentContext.get(child_id)
assert child is not None
assert child.agent0.number == 1
assert child.get_output_data("parent_context_id") == parent.id
assert child.get_output_data("parent_agent_number") == 0
assert child.get_output_data("parent_context_kind") == "subordinate"
finally:
for child_id in child_ids:
AgentContext.remove(child_id)
AgentContext.remove(parent_id)
@pytest.mark.asyncio
async def test_failed_parallel_subordinate_continues_directly_or_in_parallel(
monkeypatch,
) -> None:
from agent import Agent, AgentConfig, AgentContext
from helpers import message_queue, persist_chat, tool_policy
from tools.call_subordinate import Delegation
parent_id = "ctx-parallel-resume-tree"
AgentContext.remove(parent_id)
parent = AgentContext(
AgentConfig(mcp_servers="", profile="agent0"),
id=parent_id,
set_current=False,
)
calls = {}
async def flaky_monologue(agent):
count = calls.get(agent.context.id, 0) + 1
calls[agent.context.id] = count
if count == 1:
raise RuntimeError("simulated API failure")
return f"{agent.agent_name} continuation {count}"
monkeypatch.setattr(Agent, "monologue", flaky_monologue)
monkeypatch.setattr(tool_policy, "ensure_tool_allowed", lambda *_args, **_kwargs: None)
monkeypatch.setattr(message_queue, "log_user_message", lambda *_args, **_kwargs: None)
monkeypatch.setattr(persist_chat, "save_tmp_chat", lambda _context: None)
child_id = ""
try:
failed = parallel_tools.ParallelJob(
id="callsubordin-failed",
parent_context_id=parent.id,
index=0,
tool_name="call_subordinate",
tool_args={"message": "remember ALPHA", "reset": True},
kind="subordinate",
parent_agent=parent.agent0,
)
parallel_tools._jobs_for_context(parent)[failed.id] = failed
await parallel_tools._run_parallel_job(parent.id, failed.id)
child_id = failed.worker_context_id or ""
assert failed.state == "error"
assert failed.error == "simulated API failure"
assert child_id
assert AgentContext.get(child_id).agent0.number == 1 # type: ignore[union-attr]
direct = Delegation(
parent.agent0,
"call_subordinate",
None,
{},
"",
None,
)
direct_result = await direct.execute(
message="continue after the API failure",
context_id=child_id,
reset=False,
)
assert direct_result.message == "A1 continuation 2"
assert direct_result.additional == {"context_id": child_id}
continued = parallel_tools.ParallelJob(
id="callsubordin-continued",
parent_context_id=parent.id,
index=0,
tool_name="call_subordinate",
tool_args={
"message": "continue once more",
"context_id": child_id,
"reset": False,
},
kind="subordinate",
parent_agent=parent.agent0,
)
parallel_tools._jobs_for_context(parent)[continued.id] = continued
await parallel_tools._run_parallel_job(parent.id, continued.id)
assert continued.state == "success"
assert continued.worker_context_id == child_id
assert continued.result == "A1 continuation 3"
assert calls == {child_id: 3}
finally:
if child_id:
AgentContext.remove(child_id)
AgentContext.remove(parent_id)
@pytest.mark.asyncio
async def test_parallel_a1_spawns_a2_with_same_lifecycle(monkeypatch) -> None:
from agent import Agent, AgentConfig, AgentContext
from helpers import message_queue, persist_chat, tool_policy
parent_id = "ctx-parallel-a2-tree"
AgentContext.remove(parent_id)
parent = AgentContext(
AgentConfig(mcp_servers="", profile="agent0"),
id=parent_id,
set_current=False,
)
async def fake_monologue(agent):
return agent.agent_name
monkeypatch.setattr(Agent, "monologue", fake_monologue)
monkeypatch.setattr(tool_policy, "ensure_tool_allowed", lambda *_args, **_kwargs: None)
monkeypatch.setattr(message_queue, "log_user_message", lambda *_args, **_kwargs: None)
monkeypatch.setattr(persist_chat, "save_tmp_chat", lambda _context: None)
child_ids = []
try:
a1_job = parallel_tools.ParallelJob(
id="callsubordin-a1",
parent_context_id=parent.id,
index=0,
tool_name="call_subordinate",
tool_args={"message": "be A1", "reset": True},
kind="subordinate",
parent_agent=parent.agent0,
)
a1_result = await parallel_tools._run_subordinate_context_job(parent.id, a1_job)
a1 = AgentContext.get(a1_job.worker_context_id or "").agent0 # type: ignore[union-attr]
child_ids.append(a1.context.id)
a2_job = parallel_tools.ParallelJob(
id="callsubordin-a2",
parent_context_id=a1.context.id,
index=0,
tool_name="call_subordinate",
tool_args={"message": "be A2", "reset": True},
kind="subordinate",
parent_agent=a1,
)
a2_result = await parallel_tools._run_subordinate_context_job(
a1.context.id, a2_job
)
a2_context = AgentContext.get(a2_job.worker_context_id or "")
child_ids.append(a2_context.id) # type: ignore[union-attr]
assert a1_result == "A1"
assert a1.number == 1
assert a2_result == "A2"
assert a2_context.agent0.number == 2 # type: ignore[union-attr]
assert a2_context.get_output_data("parent_context_id") == a1.context.id # type: ignore[union-attr]
assert a2_context.get_output_data("parent_agent_number") == 1 # type: ignore[union-attr]
finally:
for child_id in reversed(child_ids):
AgentContext.remove(child_id)
AgentContext.remove(parent_id)
@pytest.mark.asyncio
async def test_parallel_subordinate_owns_nested_parallel_tasks(monkeypatch) -> None:
monkeypatch.setattr(parallel_tools, "DeferredTask", _FakeDeferredTask)
agent = _FakeAgent()
parent_task = _FakeDeferredTask()
agent.context.task = parent_task
agent.context.set_data(parallel_tools.PARALLEL_WORKER_KIND_KEY, "subordinate")
jobs = await parallel_tools.start_parallel_jobs(
agent, # type: ignore[arg-type]
[
parallel_tools.NormalizedToolCall(
index=0,
tool_name="call_subordinate",
tool_args={"message": "nested", "reset": True},
)
],
)
assert parent_task.children == [jobs[0].deferred_task]
parent_task.kill()
assert jobs[0].deferred_task.killed == 1 # type: ignore[union-attr]
@pytest.mark.asyncio
async def test_parallel_direct_tool_jobs_fallback_to_generic_tool_log_type(monkeypatch) -> None:
class FakeDeferredTask:
def __init__(self, thread_name=None) -> None:
self.thread_name = thread_name
self.started = None
def start_task(self, func, *args):
self.started = (func, args)
return self
def is_ready(self):
return False
def is_alive(self):
return True
def kill(self):
pass
monkeypatch.setattr(parallel_tools, "DeferredTask", FakeDeferredTask)
monkeypatch.setattr(parallel_tools, "_resolve_parallel_tool", lambda *_args, **_kwargs: None)
agent = _FakeAgent()
jobs = await parallel_tools.start_parallel_jobs(
agent, # type: ignore[arg-type]
[
parallel_tools.NormalizedToolCall(
index=0,
tool_name="wait",
tool_args={"seconds": 1},
)
],
)
assert jobs[0].kind == "tool"
assert agent.context.log.items[0].type == "tool"
assert agent.context.log.items[0].kvps == {"seconds": 1, "_tool_name": "wait"}
parallel_tools._finish_job(jobs[0], "success", result="done")
assert agent.context.log.items[0].content == "done"
assert agent.context.log.items[0].kvps == {"seconds": 1, "_tool_name": "wait"}
@pytest.mark.asyncio
async def test_parallel_code_execution_child_uses_code_exe_log_type(monkeypatch) -> None:
class FakeDeferredTask:
def __init__(self, thread_name=None) -> None:
self.thread_name = thread_name
def start_task(self, func, *args):
return self
def is_ready(self):
return False
def is_alive(self):
return True
def kill(self):
pass
class FakeCodeExecutionTool:
def __init__(self, agent, args):
self.agent = agent
self.args = args
def get_log_object(self):
runtime = self.args.get("runtime", "unknown")
session = self.args.get("session", None)
session_text = f"[{session}] " if session or session == 0 else ""
return self.agent.context.log.log(
type="code_exe",
heading=f"icon://terminal {session_text}code_execution_tool - {runtime}",
content="",
kvps=self.args,
)
monkeypatch.setattr(parallel_tools, "DeferredTask", FakeDeferredTask)
monkeypatch.setattr(
parallel_tools,
"_resolve_parallel_tool",
lambda _agent, _tool_name, args: FakeCodeExecutionTool(_agent, args),
)
agent = _FakeAgent()
jobs = await parallel_tools.start_parallel_jobs(
agent, # type: ignore[arg-type]
[
parallel_tools.NormalizedToolCall(
index=0,
tool_name="code_execution_tool",
tool_args={
"runtime": "terminal",
"session": 0,
"code": "pwd",
},
)
],
)
assert jobs[0].kind == "tool"
assert agent.context.log.items[0].type == "code_exe"
assert agent.context.log.items[0].heading == "icon://terminal [0] code_execution_tool - terminal"
assert agent.context.log.items[0].kvps == {
"runtime": "terminal",
"session": 0,
"code": "pwd",
}
@pytest.mark.asyncio
async def test_parallel_wait_child_uses_wait_log_type(monkeypatch) -> None:
class FakeDeferredTask:
def __init__(self, thread_name=None) -> None:
self.thread_name = thread_name
def start_task(self, func, *args):
return self
def is_ready(self):
return False
def is_alive(self):
return True
def kill(self):
pass
class FakeWaitTool:
def __init__(self, agent, args):
self.agent = agent
self.args = args
def get_log_object(self):
return self.agent.context.log.log(
type="progress",
heading="icon://timer Wait: Waiting...",
content="",
kvps=self.args,
)
monkeypatch.setattr(parallel_tools, "DeferredTask", FakeDeferredTask)
monkeypatch.setattr(
parallel_tools,
"_resolve_parallel_tool",
lambda _agent, _tool_name, args: FakeWaitTool(_agent, args),
)
agent = _FakeAgent()
jobs = await parallel_tools.start_parallel_jobs(
agent, # type: ignore[arg-type]
[
parallel_tools.NormalizedToolCall(
index=0,
tool_name="wait",
tool_args={"seconds": 1},
)
],
)
assert jobs[0].kind == "tool"
assert agent.context.log.items[0].type == "progress"
assert agent.context.log.items[0].heading == "icon://timer Wait: Waiting..."
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
async def test_parallel_tool_keeps_wrapper_out_of_visible_log() -> None:
from tools.parallel import ParallelTool
class HistoryAgent(_FakeAgent):
def __init__(self) -> None:
super().__init__()
self.tool_results = []
def hist_add_tool_result(self, tool_name, tool_result, **kwargs):
self.tool_results.append((tool_name, tool_result, kwargs))
agent = HistoryAgent()
tool = ParallelTool(agent, "parallel", None, {}, "", None) # type: ignore[arg-type]
await tool.before_execution()
await tool.after_execution(Response(message="done", break_loop=False, additional={"extra": "value"}))
assert agent.context.log.items == []
assert agent.tool_results == [("parallel", "done", {"extra": "value"})]
@pytest.mark.asyncio
async def test_parallel_child_contexts_are_chats_not_tasks(monkeypatch) -> None:
from agent import AgentContext
from initialize import initialize_agent
from helpers import state_snapshot
class NoTaskScheduler:
def get_task_by_uuid(self, _task_id):
return None
monkeypatch.setattr(
state_snapshot,
"TaskScheduler",
SimpleNamespace(get=lambda: NoTaskScheduler()),
)
parent_id = "ctx-par-parent"
child_id = "ctx-par-child"
parent = AgentContext(config=initialize_agent(), id=parent_id, name="Parent", set_current=False)
child = AgentContext(config=initialize_agent(), id=child_id, name="Child", set_current=False)
try:
child.set_output_data(parallel_tools.CHILD_PARENT_CONTEXT_ID_KEY, parent.id)
child.set_output_data(parallel_tools.CHILD_PARENT_CONTEXT_KIND_KEY, "parallel")
child.set_output_data(parallel_tools.CHILD_PARENT_CONTEXT_LABEL_KEY, "Child task")
child.set_output_data(parallel_tools.CHILD_PARALLEL_JOB_ID_KEY, "job-123")
payload = await state_snapshot.build_snapshot(
context=parent.id,
log_from=0,
notifications_from=0,
timezone="UTC",
)
contexts_by_id = {ctx["id"]: ctx for ctx in payload["contexts"]}
task_ids = {task["id"] for task in payload["tasks"]}
assert parent_id in contexts_by_id
assert child_id in contexts_by_id
assert contexts_by_id[child_id]["parent_context_id"] == parent_id
assert child_id not in task_ids
finally:
AgentContext.remove(parent_id)
AgentContext.remove(child_id)
def test_chats_sidebar_projects_parallel_children_as_indented_accordion() -> None:
store = (PROJECT_ROOT / "webui/components/sidebar/chats/chats-store.js").read_text(
encoding="utf-8"
)
html = (PROJECT_ROOT / "webui/components/sidebar/chats/chats-list.html").read_text(
encoding="utf-8"
)
assert "parent_context_id" in store
assert "const nextExpandedParents = { ...this.expandedParents };" in store
assert "nextExpandedParents[selectedId] === undefined" in store
assert "nextExpandedParents[selectedId] = true;" in store
assert "topLevelContexts()" in html
assert "childContexts(context.id)" in html
assert "chat-child-container" in html
assert "keyboard_arrow_up" in html
assert "keyboard_arrow_down" in html
assert ".chats-config-list .chat-tree-item" in html
assert ".chats-config-list .chat-child-list > li" in html
assert 'x-show="$store.chats.hasChildren(context.id)"' in html
assert "'chat-has-children': $store.chats.hasChildren(context.id)" in html
assert ".chat-container.chat-has-children .chat-list-button" in html
assert "left: 2px" in html
assert "padding-left: 24px" in html
assert "color: var(--color-text-muted)" in html