mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] fix browser agent stop: cancel mid-flight API calls, stop children before parent, guard frontend state
This commit is contained in:
@@ -1353,23 +1353,25 @@ class AgentManager:
|
||||
|
||||
async def stop_agent(self, session_id: str):
|
||||
"""Stop a running agent and all its browser-agent children."""
|
||||
task = self.tasks.get(session_id)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
# 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:
|
||||
await self.stop_agent(child.id)
|
||||
|
||||
session = self.sessions.get(session_id)
|
||||
if session:
|
||||
# Set cancel event BEFORE cancelling the task so in-flight
|
||||
# browser agent loops see it immediately
|
||||
if hasattr(session, '_cancel_event'):
|
||||
session._cancel_event.set()
|
||||
|
||||
for req in list(session.pending_approvals):
|
||||
ws_manager.resolve_approval(req.id, {"behavior": "deny", "message": "Agent stopped"})
|
||||
session.pending_approvals = []
|
||||
|
||||
if hasattr(session, '_cancel_event'):
|
||||
session._cancel_event.set()
|
||||
|
||||
session.status = "stopped"
|
||||
if not session.closed_at:
|
||||
session.closed_at = datetime.now()
|
||||
@@ -1379,12 +1381,13 @@ class AgentManager:
|
||||
"session": session.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
children = [
|
||||
s for s in self.sessions.values()
|
||||
if s.parent_session_id == session_id and s.mode == "browser-agent"
|
||||
]
|
||||
for child in children:
|
||||
await self.stop_agent(child.id)
|
||||
task = self.tasks.get(session_id)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
def handle_approval(self, request_id: str, decision: dict):
|
||||
"""Resolve a pending HITL approval."""
|
||||
|
||||
@@ -337,18 +337,33 @@ async def run_browser_agent(
|
||||
"message": user_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
async def _cancellable(coro):
|
||||
"""Race any awaitable against the cancel event. Returns None if cancelled."""
|
||||
task = asyncio.ensure_future(coro)
|
||||
cancel_wait = asyncio.ensure_future(cancel_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
[task, cancel_wait], return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for p in pending:
|
||||
p.cancel()
|
||||
if cancel_event.is_set():
|
||||
return None
|
||||
return task.result()
|
||||
|
||||
try:
|
||||
for turn in range(MAX_TURNS):
|
||||
if cancel_event.is_set():
|
||||
break
|
||||
|
||||
response = await client.messages.create(
|
||||
response = await _cancellable(client.messages.create(
|
||||
model=api_model,
|
||||
max_tokens=4096,
|
||||
system=SYSTEM_PROMPT,
|
||||
tools=BROWSER_TOOLS_SCHEMA,
|
||||
messages=messages,
|
||||
)
|
||||
))
|
||||
if response is None:
|
||||
break
|
||||
|
||||
assistant_content = []
|
||||
text_parts = []
|
||||
@@ -444,9 +459,12 @@ async def run_browser_agent(
|
||||
continue
|
||||
|
||||
start = time.time()
|
||||
result = await execute_browser_tool(
|
||||
result = await _cancellable(execute_browser_tool(
|
||||
tu.name, tu.input, browser_id, tab_id,
|
||||
)
|
||||
))
|
||||
if result is None:
|
||||
cancelled = True
|
||||
break
|
||||
elapsed_ms = int((time.time() - start) * 1000)
|
||||
|
||||
action_log.append({
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openswarm",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openswarm",
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.3.0",
|
||||
|
||||
@@ -536,6 +536,11 @@ const agentsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
const existing = state.sessions[action.payload.id];
|
||||
// Don't let a stale "running" message overwrite a terminal status
|
||||
const terminal = ['stopped', 'error'] as const;
|
||||
if (existing && terminal.includes(existing.status as any) && action.payload.status === 'running') {
|
||||
return;
|
||||
}
|
||||
// Preserve local pending_approvals if the server payload has none but
|
||||
// the frontend has some (avoids race where backend clears approvals
|
||||
// before the frontend processes the removal).
|
||||
@@ -559,6 +564,10 @@ const agentsSlice = createSlice({
|
||||
) {
|
||||
const session = state.sessions[action.payload.sessionId];
|
||||
if (session) {
|
||||
const terminal = ['stopped', 'error'] as const;
|
||||
if (terminal.includes(session.status as any) && action.payload.status === 'running') {
|
||||
return;
|
||||
}
|
||||
session.status = action.payload.status;
|
||||
}
|
||||
if (action.payload.status === 'running' && !state.trackedNotificationIds.includes(action.payload.sessionId)) {
|
||||
|
||||
Reference in New Issue
Block a user