fix(agent): release completed task groups

This commit is contained in:
yu-xin-c
2026-09-01 17:42:41 +08:00
committed by chengyongru
parent dda67286f9
commit 9ecdc4533f
2 changed files with 76 additions and 6 deletions
+17 -6
View File
@@ -852,6 +852,22 @@ class AgentLoop:
metadata={**metadata, "render_as": "text"}, metadata={**metadata, "render_as": "text"},
) )
def _track_active_task(self, key: str, task: asyncio.Task[Any]) -> None:
"""Track active session work until its task group becomes empty."""
tasks = self._active_tasks.setdefault(key, set())
tasks.add(task)
task.add_done_callback(partial(self._active_task_done, key, tasks))
def _active_task_done(
self,
key: str,
tasks: set[asyncio.Task[Any]],
task: asyncio.Task[Any],
) -> None:
tasks.discard(task)
if not tasks and self._active_tasks.get(key) is tasks:
self._active_tasks.pop(key, None)
async def _cancel_active_tasks(self, key: str) -> int: async def _cancel_active_tasks(self, key: str) -> int:
"""Cancel and await all active work for *key*. """Cancel and await all active work for *key*.
@@ -1350,12 +1366,7 @@ class AgentLoop:
# Compute the effective session key before dispatching # Compute the effective session key before dispatching
# This ensures /stop command can find tasks correctly when unified session is enabled # This ensures /stop command can find tasks correctly when unified session is enabled
task = asyncio.create_task(self._dispatch(msg)) task = asyncio.create_task(self._dispatch(msg))
active_tasks: set[asyncio.Task[Any]] = self._active_tasks.setdefault( self._track_active_task(effective_key, task)
effective_key,
set(),
)
active_tasks.add(task)
task.add_done_callback(active_tasks.discard)
finally: finally:
await self.aclose() await self.aclose()
+59
View File
@@ -42,6 +42,65 @@ def _make_loop(*, tools_config=None):
return loop, bus return loop, bus
class TestActiveTaskTracking:
@pytest.mark.asyncio
async def test_completed_task_removes_empty_session_group(self):
loop, _bus = _make_loop()
release = asyncio.Event()
task = asyncio.create_task(release.wait())
loop._track_active_task("test:c1", task)
release.set()
await task
await asyncio.sleep(0)
assert "test:c1" not in loop._active_tasks
@pytest.mark.asyncio
async def test_session_group_remains_until_last_task_completes(self):
loop, _bus = _make_loop()
releases = [asyncio.Event(), asyncio.Event()]
tasks = [asyncio.create_task(release.wait()) for release in releases]
for task in tasks:
loop._track_active_task("test:c1", task)
releases[0].set()
await tasks[0]
await asyncio.sleep(0)
assert loop._active_tasks["test:c1"] == {tasks[1]}
releases[1].set()
await tasks[1]
await asyncio.sleep(0)
assert "test:c1" not in loop._active_tasks
@pytest.mark.asyncio
async def test_old_callback_preserves_replacement_session_group(self):
loop, _bus = _make_loop()
old_release = asyncio.Event()
new_release = asyncio.Event()
old_task = asyncio.create_task(old_release.wait())
new_task = asyncio.create_task(new_release.wait())
loop._track_active_task("test:c1", old_task)
loop._active_tasks.pop("test:c1")
loop._track_active_task("test:c1", new_task)
old_release.set()
await old_task
await asyncio.sleep(0)
assert loop._active_tasks["test:c1"] == {new_task}
new_release.set()
await new_task
await asyncio.sleep(0)
assert "test:c1" not in loop._active_tasks
class TestHandleStop: class TestHandleStop:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_stop_no_active_task(self): async def test_stop_no_active_task(self):