mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
[eric] sessions: stopping, closing or deleting a chat stops every session it spawned, not only its browser agents; a SpawnAgent tree outlived a killed parent (Haik, exp.9)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e4b16bbe07
commit
dc9a6391e2
@@ -21,13 +21,9 @@ from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtoco
|
||||
class SessionControl(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def stop_agent(self, session_id: str):
|
||||
"""Stop a running agent and all its browser-agent children."""
|
||||
# Stop children first so browser agents get cancelled before parent
|
||||
children = [
|
||||
s for s in self.sessions.values()
|
||||
if s.parent_session_id == session_id and s.mode == "browser-agent"
|
||||
]
|
||||
for child in children:
|
||||
"""Stop a running agent and every session it spawned, leaves first."""
|
||||
from backend.apps.agents.manager.session.descendants import children_of
|
||||
for child in children_of(self.sessions, session_id):
|
||||
await self.stop_agent(child.id)
|
||||
|
||||
session = self.sessions.get(session_id)
|
||||
|
||||
@@ -39,12 +39,9 @@ class SessionLifecycle(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def close_session(self, session_id: str) -> None:
|
||||
"""Close a session: pause the agent if running, persist to JSON file,
|
||||
and remove from in-memory state. Also stops browser-agent children."""
|
||||
children = [
|
||||
s for s in self.sessions.values()
|
||||
if s.parent_session_id == session_id and s.mode == "browser-agent"
|
||||
]
|
||||
for child in children:
|
||||
and remove from in-memory state. Also stops every session it spawned."""
|
||||
from backend.apps.agents.manager.session.descendants import children_of
|
||||
for child in children_of(self.sessions, session_id):
|
||||
await self.stop_agent(child.id)
|
||||
|
||||
task = self.tasks.get(session_id)
|
||||
@@ -112,14 +109,11 @@ class SessionLifecycle(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def delete_session(self, session_id: str) -> None:
|
||||
"""Permanently delete a session: remove from memory and JSON file.
|
||||
Also stops browser-agent children first."""
|
||||
Also stops every session it spawned first."""
|
||||
from backend.apps.agents.core.flight_recorder import drop_session
|
||||
drop_session(session_id)
|
||||
children = [
|
||||
s for s in self.sessions.values()
|
||||
if s.parent_session_id == session_id and s.mode == "browser-agent"
|
||||
]
|
||||
for child in children:
|
||||
from backend.apps.agents.manager.session.descendants import children_of
|
||||
for child in children_of(self.sessions, session_id):
|
||||
await self.stop_agent(child.id)
|
||||
|
||||
task = self.tasks.get(session_id)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Every session born from a parent, whatever kind it is. Stop, close and delete used to walk only
|
||||
browser-agent children, so a SpawnAgent tree outlived the parent that was killed (Haik, exp.9)."""
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
|
||||
|
||||
@typechecked
|
||||
def children_of(sessions: Dict[str, AgentSession], parent_id: str) -> List[AgentSession]:
|
||||
return [s for s in sessions.values() if s.parent_session_id == parent_id]
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Haik, exp.9: killing a parent left its SpawnAgent children running. Stop, close and delete walked
|
||||
only browser-agent children, so a sub-agent tree (and anything under it) outlived the parent."""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
from backend.apps.agents.agent_manager import agent_manager
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
from backend.apps.agents.manager import SessionControl
|
||||
from backend.apps.agents.manager.session import SessionLifecycle, descendants
|
||||
|
||||
|
||||
def p_tree() -> tuple[AgentSession, AgentSession, AgentSession, AgentSession]:
|
||||
parent = AgentSession(name="parent", model="opus-4-8", status="running")
|
||||
child = AgentSession(name="child", model="opus-4-8", status="running", mode="sub-agent", parent_session_id=parent.id)
|
||||
grandchild = AgentSession(name="grandchild", model="opus-4-8", status="running", mode="browser-agent", parent_session_id=child.id)
|
||||
stranger = AgentSession(name="stranger", model="opus-4-8", status="running", mode="sub-agent")
|
||||
for s in (parent, child, grandchild, stranger):
|
||||
agent_manager.sessions[s.id] = s
|
||||
return parent, child, grandchild, stranger
|
||||
|
||||
|
||||
def p_park(session_id: str) -> asyncio.Task:
|
||||
async def forever() -> None:
|
||||
await asyncio.sleep(3600)
|
||||
task = asyncio.get_running_loop().create_task(forever())
|
||||
agent_manager.tasks[session_id] = task
|
||||
return task
|
||||
|
||||
|
||||
def test_stopping_the_parent_stops_the_whole_tree_and_nobody_else():
|
||||
parent, child, grandchild, stranger = p_tree()
|
||||
|
||||
async def run() -> None:
|
||||
tasks = [p_park(s.id) for s in (parent, child, grandchild, stranger)]
|
||||
await agent_manager.stop_agent(parent.id)
|
||||
await asyncio.sleep(0)
|
||||
# Checked inside the loop: closing it cancels every task that is left, which would fake the stranger's cancel.
|
||||
assert [s.status for s in (parent, child, grandchild)] == ["stopped", "stopped", "stopped"]
|
||||
assert stranger.status == "running", "an unrelated running chat is not the tree"
|
||||
assert all(t.cancelled() or t.done() for t in tasks[:3])
|
||||
assert not tasks[3].cancelled() and not tasks[3].done()
|
||||
for s in (parent, child, grandchild):
|
||||
assert s.id not in agent_manager.tasks
|
||||
tasks[3].cancel()
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_children_of_walks_every_kind_not_just_browser_agents():
|
||||
parent, child, grandchild, stranger = p_tree()
|
||||
assert [s.id for s in descendants.children_of(agent_manager.sessions, parent.id)] == [child.id]
|
||||
assert [s.id for s in descendants.children_of(agent_manager.sessions, child.id)] == [grandchild.id]
|
||||
assert descendants.children_of(agent_manager.sessions, stranger.id) == []
|
||||
|
||||
|
||||
def test_close_and_delete_take_the_same_walk():
|
||||
"""Three doors, one walk: a mode filter in any of them is the bug coming back."""
|
||||
for fn in (SessionControl.SessionControl.stop_agent, SessionLifecycle.SessionLifecycle.close_session, SessionLifecycle.SessionLifecycle.delete_session):
|
||||
src = inspect.getsource(fn)
|
||||
assert "children_of(self.sessions, session_id)" in src, fn.__name__
|
||||
assert 'mode == "browser-agent"' not in src, fn.__name__
|
||||
Reference in New Issue
Block a user