ouroboros/tests/test_swarm_parallel_spawn.py
Ouroboros 75d763f5fa feat(subagents): parallel schedule_subagent emission + parent-side emit lock (6.22.2)
WS1: a burst of schedule_subagent calls in one tool-call round now runs in the
existing tool ThreadPool instead of sequentially. tool_capabilities.PARALLEL_SAFE_ENQUEUE_TOOLS
marks schedule_subagent as a fire-and-forget enqueue tool; loop_tool_execution.
tool_calls_can_run_parallel parallelizes any round whose calls are all
read-only-parallel or enqueue-safe.

WS4: a process-local _SCHEDULE_EMIT_LOCK in tools/control.py serializes the
parent-side _last_scheduled_subagents read-modify-write (_record_scheduled_subagent)
and the pending_events fallback, so concurrent emission cannot lose records. The
supervisor still drains its event queue serially, so cap/dedup/enqueue stay
single-threaded. No tool-schema or gateway-contract change.

Tests: tests/test_swarm_parallel_spawn.py (gate semantics + concurrency no-lost-update).
2026-06-08 06:49:56 +03:00

76 lines
2.3 KiB
Python

"""WS1+WS4: parallel schedule_subagent emission gate + parent-side thread safety."""
from __future__ import annotations
import threading
import types
from ouroboros.loop_tool_execution import tool_calls_can_run_parallel
from ouroboros.tool_capabilities import (
PARALLEL_SAFE_ENQUEUE_TOOLS,
READ_ONLY_PARALLEL_TOOLS,
)
from ouroboros.tools.control import _emit_control_event, _record_scheduled_subagent
def _call(name):
return {"function": {"name": name}}
# --- WS1: parallel gate ---
def test_schedule_subagent_parallel_safe_not_readonly():
assert "schedule_subagent" in PARALLEL_SAFE_ENQUEUE_TOOLS
assert "schedule_subagent" not in READ_ONLY_PARALLEL_TOOLS
def test_burst_of_spawns_runs_parallel():
assert tool_calls_can_run_parallel([_call("schedule_subagent")] * 4) is True
def test_mixed_reads_and_spawns_runs_parallel():
assert tool_calls_can_run_parallel(
[_call("read_file"), _call("schedule_subagent"), _call("search_code")]
) is True
def test_any_non_parallel_tool_forces_sequential():
assert tool_calls_can_run_parallel(
[_call("schedule_subagent"), _call("write_file")]
) is False
def test_single_call_is_not_parallel():
assert tool_calls_can_run_parallel([_call("schedule_subagent")]) is False
# --- WS4: parent-side thread safety (no lost updates under concurrency) ---
def _fake_ctx():
# event_queue=None forces the pending_events fallback in _emit_control_event.
return types.SimpleNamespace(event_queue=None, pending_events=[], _last_scheduled_subagents=[])
def _run_concurrently(fn, n=200):
start = threading.Event()
def worker(i):
start.wait()
fn(i)
threads = [threading.Thread(target=worker, args=(i,)) for i in range(n)]
for t in threads:
t.start()
start.set()
for t in threads:
t.join()
def test_concurrent_record_scheduled_subagent_no_lost_updates():
ctx = _fake_ctx()
_run_concurrently(lambda i: _record_scheduled_subagent(ctx, {"task_ids": [f"t{i}"]}))
assert len(ctx._last_scheduled_subagents) == 200
assert {r["task_ids"][0] for r in ctx._last_scheduled_subagents} == {f"t{i}" for i in range(200)}
def test_concurrent_emit_control_event_fallback_no_lost_updates():
ctx = _fake_ctx()
_run_concurrently(lambda i: _emit_control_event(ctx, {"type": "x", "i": i}))
assert len(ctx.pending_events) == 200