mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
230 lines
9.4 KiB
Python
230 lines
9.4 KiB
Python
"""Complex agent-manager operations extracted for the 250-line limit.
|
|
|
|
Each function is a standalone async operation that receives the sessions
|
|
dict (and other dependencies) explicitly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime
|
|
from uuid import uuid4
|
|
|
|
from backend.apps.agents.models import (
|
|
AgentSession, Message, MessageBranch,
|
|
)
|
|
from backend.apps.agents.manager.ws_manager import ws_manager
|
|
from backend.apps.agents.manager.session_store import (
|
|
save_session, load_session_data, delete_session_file,
|
|
build_search_text, copy_session_messages,
|
|
)
|
|
from backend.apps.analytics.collector import record as _analytics
|
|
from backend.apps.agents.execution.agent_loop import run_agent_loop
|
|
|
|
from backend.apps.agents.execution.agent_loop import run_agent_loop
|
|
|
|
from backend.apps.agents.execution.agent_mock import fire_session_completed
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def edit_message_op(
|
|
sessions: dict, tasks: dict,
|
|
session_id: str, message_id: str, new_content: str,
|
|
):
|
|
session = sessions.get(session_id)
|
|
if not session:
|
|
raise ValueError(f"Session {session_id} not found")
|
|
existing = tasks.get(session_id)
|
|
if existing and not existing.done():
|
|
existing.cancel()
|
|
try:
|
|
await existing
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
target_msg = next((m for m in session.messages if m.id == message_id), None)
|
|
if not target_msg or target_msg.role != "user":
|
|
raise ValueError("Can only edit user messages")
|
|
|
|
fork_point_id = message_id
|
|
fork_parent_branch = target_msg.branch_id
|
|
msg_branch = session.branches.get(target_msg.branch_id)
|
|
if msg_branch and msg_branch.fork_point_message_id:
|
|
branch_user_msgs = [m for m in session.messages if m.branch_id == target_msg.branch_id and m.role == "user"]
|
|
if branch_user_msgs and branch_user_msgs[0].id == message_id:
|
|
fork_point_id = msg_branch.fork_point_message_id
|
|
fork_parent_branch = msg_branch.parent_branch_id or "main"
|
|
|
|
new_branch_id = uuid4().hex
|
|
new_branch = MessageBranch(id=new_branch_id, parent_branch_id=fork_parent_branch, fork_point_message_id=fork_point_id)
|
|
session.branches[new_branch_id] = new_branch
|
|
session.active_branch_id = new_branch_id
|
|
_analytics("feature.used", {
|
|
"feature": "message.branched",
|
|
"branch_depth": len([b for b in session.branches.values() if b.parent_branch_id]),
|
|
"total_branches_in_session": len(session.branches),
|
|
"messages_before_fork": len([m for m in session.messages if m.branch_id == fork_parent_branch]),
|
|
}, session_id=session_id, dashboard_id=session.dashboard_id)
|
|
|
|
edited_msg = Message(
|
|
role="user", content=new_content, branch_id=new_branch_id,
|
|
parent_id=target_msg.parent_id, images=target_msg.images,
|
|
context_paths=target_msg.context_paths, forced_tools=target_msg.forced_tools,
|
|
attached_skills=target_msg.attached_skills,
|
|
)
|
|
session.messages.append(edited_msg)
|
|
await ws_manager.emit_message(session_id, edited_msg)
|
|
await ws_manager.emit_branch_created(session_id, new_branch, new_branch_id)
|
|
session.sdk_session_id = None
|
|
session.status = "running"
|
|
await ws_manager.emit_status(session_id, "running", session)
|
|
task = asyncio.create_task(run_agent_loop(
|
|
sessions, session_id, new_content,
|
|
images=target_msg.images, context_paths=target_msg.context_paths,
|
|
forced_tools=target_msg.forced_tools, attached_skills=target_msg.attached_skills,
|
|
))
|
|
tasks[session_id] = task
|
|
|
|
|
|
async def close_session_op(
|
|
sessions: dict, tasks: dict,
|
|
session_id: str,
|
|
):
|
|
children = [s for s in sessions.values() if s.parent_session_id == session_id and s.mode == "browser-agent"]
|
|
from backend.apps.agents.manager.agent_manager import agent_manager
|
|
# NOTE: this is a circular dependency, must be fixed soon by fixing the ai slop
|
|
for child in children:
|
|
await agent_manager.stop_agent(child.id)
|
|
task = tasks.get(session_id)
|
|
if task and not task.done():
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
session = sessions.get(session_id)
|
|
if not session:
|
|
raise ValueError(f"Session {session_id} not found")
|
|
if session.status in ("running", "waiting_approval"):
|
|
session.status = "stopped"
|
|
session.closed_at = datetime.now()
|
|
for req in list(session.pending_approvals):
|
|
ws_manager.resolve_approval(req.id, {"behavior": "deny", "message": "Session closed"})
|
|
session.pending_approvals = []
|
|
if hasattr(session, '_cancel_event'):
|
|
session._cancel_event.set()
|
|
fire_session_completed(session, sessions)
|
|
doc_data = session.model_dump(mode="json")
|
|
doc_data["search_text"] = build_search_text(session)
|
|
save_session(session_id, doc_data)
|
|
await ws_manager.emit_closed(session_id, session)
|
|
sessions.pop(session_id, None)
|
|
tasks.pop(session_id, None)
|
|
logger.info(f"Session {session_id} closed and persisted")
|
|
|
|
|
|
async def resume_session_op(sessions: dict, session_id: str) -> AgentSession:
|
|
if session_id in sessions:
|
|
return sessions[session_id]
|
|
data = load_session_data(session_id)
|
|
if data is None:
|
|
raise ValueError(f"Session {session_id} not found in history")
|
|
session = AgentSession(**data)
|
|
hours_since = 0
|
|
if data.get("closed_at"):
|
|
try:
|
|
closed = datetime.fromisoformat(data["closed_at"][:19])
|
|
hours_since = round((datetime.now() - closed).total_seconds() / 3600, 1)
|
|
except Exception:
|
|
pass
|
|
_analytics("session.resumed", {
|
|
"hours_since_closed": hours_since,
|
|
"original_message_count": len(data.get("messages", [])),
|
|
"original_cost_usd": data.get("cost_usd", 0), "model": session.model,
|
|
}, session_id=session_id, dashboard_id=session.dashboard_id)
|
|
session.closed_at = None
|
|
sessions[session_id] = session
|
|
delete_session_file(session_id)
|
|
await ws_manager.emit_status(session_id, session.status, session)
|
|
logger.info(f"Session {session_id} resumed from history")
|
|
return session
|
|
|
|
|
|
async def duplicate_session_op(
|
|
sessions: dict, session_id: str,
|
|
dashboard_id: str | None = None, up_to_message_id: str | None = None,
|
|
) -> AgentSession:
|
|
source = sessions.get(session_id)
|
|
if not source:
|
|
data = load_session_data(session_id)
|
|
if data is None:
|
|
raise ValueError(f"Session {session_id} not found")
|
|
source = AgentSession(**data)
|
|
new_messages, new_branches, _ = copy_session_messages(source, up_to_message_id)
|
|
new_session = AgentSession(
|
|
id=uuid4().hex, name=f"{source.name} (copy)", status="stopped",
|
|
model=source.model, mode=source.mode, system_prompt=source.system_prompt,
|
|
allowed_tools=list(source.allowed_tools), max_turns=source.max_turns,
|
|
cwd=source.cwd, created_at=datetime.now(), messages=new_messages,
|
|
branches=new_branches, active_branch_id=source.active_branch_id,
|
|
tool_group_meta=dict(source.tool_group_meta),
|
|
dashboard_id=dashboard_id or source.dashboard_id,
|
|
)
|
|
sessions[new_session.id] = new_session
|
|
await ws_manager.emit_status(new_session.id, new_session.status, new_session)
|
|
return new_session
|
|
|
|
|
|
async def invoke_agent_op(
|
|
sessions: dict, source_session_id: str, message: str,
|
|
parent_session_id: str | None = None, dashboard_id: str | None = None,
|
|
) -> dict:
|
|
source = sessions.get(source_session_id)
|
|
if not source:
|
|
data = load_session_data(source_session_id)
|
|
if data is None:
|
|
raise ValueError(f"Session {source_session_id} not found")
|
|
source = AgentSession(**data)
|
|
source_name = source.name
|
|
new_messages, new_branches, _ = copy_session_messages(source)
|
|
fork = AgentSession(
|
|
id=uuid4().hex, name=f"{source_name} (invoked)", status="running",
|
|
model=source.model, mode="invoked-agent", sdk_session_id=source.sdk_session_id,
|
|
system_prompt=source.system_prompt, allowed_tools=list(source.allowed_tools),
|
|
max_turns=source.max_turns or 25, cwd=source.cwd, created_at=datetime.now(),
|
|
messages=new_messages, branches=new_branches,
|
|
active_branch_id=source.active_branch_id,
|
|
tool_group_meta=dict(source.tool_group_meta),
|
|
dashboard_id=dashboard_id or source.dashboard_id,
|
|
parent_session_id=parent_session_id,
|
|
)
|
|
sessions[fork.id] = fork
|
|
await ws_manager.broadcast_global("agent:status", {
|
|
"session_id": fork.id, "status": fork.status,
|
|
"session": fork.model_dump(mode="json"),
|
|
})
|
|
user_msg = Message(role="user", content=message, branch_id=fork.active_branch_id)
|
|
fork.messages.append(user_msg)
|
|
await ws_manager.emit_message(fork.id, user_msg)
|
|
await run_agent_loop(sessions, fork.id, message, fork_session=True)
|
|
last_assistant = None
|
|
for msg in reversed(fork.messages):
|
|
if msg.role == "assistant":
|
|
content = msg.content
|
|
if isinstance(content, str):
|
|
last_assistant = content
|
|
elif isinstance(content, list):
|
|
texts = [b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"]
|
|
last_assistant = "\n".join(texts)
|
|
else:
|
|
last_assistant = str(content)
|
|
break
|
|
return {
|
|
"forked_session_id": fork.id, "source_name": source_name,
|
|
"response": last_assistant or "No response from invoked agent.",
|
|
"cost_usd": fork.cost_usd,
|
|
} |