mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-29 13:04:51 +02:00
[eric] agents: extract no-SDK mock loop into MockAgentMixin (AgentLaunchMixin now under ceiling)
This commit is contained in:
@@ -53,6 +53,7 @@ from backend.apps.agents.manager.session.SessionLifecycleMixin import SessionLif
|
||||
from backend.apps.agents.manager.MessagingMixin import MessagingMixin
|
||||
from backend.apps.agents.manager.SessionControlMixin import SessionControlMixin
|
||||
from backend.apps.agents.manager.AgentLaunchMixin import AgentLaunchMixin
|
||||
from backend.apps.agents.manager.MockAgentMixin import MockAgentMixin
|
||||
from backend.apps.agents.manager.RunSupportMixin import RunSupportMixin
|
||||
from backend.apps.agents.manager.permissions import gate_hooks
|
||||
from backend.apps.agents.manager.session.workspace_git import ensure_cwd_git_repo
|
||||
@@ -71,7 +72,7 @@ logger = logging.getLogger(__name__)
|
||||
os.environ.setdefault("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "3600000")
|
||||
|
||||
|
||||
class AgentManager(SessionLifecycleMixin, MessagingMixin, SessionControlMixin, AgentLaunchMixin, RunSupportMixin):
|
||||
class AgentManager(SessionLifecycleMixin, MessagingMixin, SessionControlMixin, AgentLaunchMixin, MockAgentMixin, RunSupportMixin):
|
||||
@typechecked
|
||||
def __init__(self):
|
||||
self.sessions: Dict[str, AgentSession] = {}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Agent run entry points for AgentManager: launch a new top-level run, run the no-SDK mock
|
||||
fallback, and the staticmethod invoke_agent helper. Split into a mixin to keep the manager file
|
||||
under the size ceiling; self.p_run_agent_loop / self.p_stream_text / self.sessions resolve across
|
||||
the MRO exactly as before."""
|
||||
"""Agent run entry points for AgentManager: launch a new top-level run and the staticmethod
|
||||
invoke_agent helper (fork-and-send a sub-agent). The no-SDK mock fallback lives in MockAgentMixin.
|
||||
Split into a mixin to keep the manager file under the size ceiling; self.p_run_agent_loop /
|
||||
self.sessions resolve across the MRO exactly as before."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
@@ -14,7 +13,7 @@ from typing import Dict, List, Optional
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import (
|
||||
AgentConfig, AgentSession, ApprovalRequest, Message, MessageBranch,
|
||||
AgentConfig, AgentSession, Message, MessageBranch,
|
||||
)
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
from backend.apps.settings.settings import load_settings
|
||||
@@ -134,101 +133,6 @@ class AgentLaunchMixin:
|
||||
|
||||
return session
|
||||
|
||||
@typechecked
|
||||
async def p_run_mock_agent(self, session_id: str, prompt: str):
|
||||
"""Mock agent loop for development without claude_agent_sdk installed."""
|
||||
session = self.sessions.get(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
request_id = uuid4().hex
|
||||
approval_req = ApprovalRequest(
|
||||
id=request_id,
|
||||
session_id=session_id,
|
||||
tool_name="Bash",
|
||||
tool_input={"command": f"echo 'Processing: {prompt}'", "description": "Echo the user prompt"},
|
||||
)
|
||||
session.pending_approvals.append(approval_req)
|
||||
session.status = "waiting_approval"
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
"status": "waiting_approval",
|
||||
})
|
||||
|
||||
decision = await ws_manager.send_approval_request(
|
||||
session_id, request_id, "Bash",
|
||||
{"command": f"echo 'Processing: {prompt}'", "description": "Echo the user prompt"}
|
||||
)
|
||||
|
||||
session.pending_approvals = [a for a in session.pending_approvals if a.id != request_id]
|
||||
session.status = "running"
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
"status": "running",
|
||||
})
|
||||
|
||||
import json
|
||||
tool_input_content = {"tool": "Bash", "input": {"command": f"echo 'Processing: {prompt}'"}, "approved": decision.get("behavior") == "allow"}
|
||||
tool_msg_id = uuid4().hex
|
||||
await self.p_stream_tool_input(
|
||||
session_id, tool_msg_id, "Bash",
|
||||
json.dumps(tool_input_content["input"], indent=2),
|
||||
)
|
||||
tool_msg = Message(id=tool_msg_id, role="tool_call", content=tool_input_content, branch_id=session.active_branch_id)
|
||||
session.messages.append(tool_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": tool_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if decision.get("behavior") == "allow":
|
||||
tool_result = Message(role="tool_result", content=f"Processing: {prompt}", branch_id=session.active_branch_id)
|
||||
session.messages.append(tool_result)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": tool_result.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
asst_text = (
|
||||
f"I've processed your request: \"{prompt}\"\n\n"
|
||||
"This is a mock response because `claude-agent-sdk` is not installed. "
|
||||
"Install it with `pip install claude-agent-sdk` to use real Claude Code instances.\n\n"
|
||||
f"The agent was configured with:\n- Model: {session.model}\n- Mode: {session.mode}"
|
||||
)
|
||||
asst_msg_id = uuid4().hex
|
||||
await self.p_stream_text(session_id, asst_msg_id, asst_text)
|
||||
|
||||
asst_msg = Message(id=asst_msg_id, role="assistant", content=asst_text, branch_id=session.active_branch_id)
|
||||
session.messages.append(asst_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": asst_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
session.status = "completed"
|
||||
session.closed_at = datetime.now()
|
||||
# Mock branch (claude_agent_sdk missing): leave cost untouched so
|
||||
# it stays at its 0.0 default. A fake nonzero value here would
|
||||
# poison the cost shown in the session header during dev. The
|
||||
# `_mock_run` flag is read by the close path so a mock session
|
||||
# doesn't get reported to the cloud as a real one.
|
||||
setattr(session, "_mock_run", True)
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
"status": "completed",
|
||||
"session": session.model_dump(mode="json"),
|
||||
})
|
||||
await ws_manager.send_to_session(session_id, "agent:cost_update", {
|
||||
"session_id": session_id,
|
||||
"cost_usd": session.cost_usd,
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
@typechecked
|
||||
async def invoke_agent(
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""The no-SDK mock agent loop, split out of AgentLaunchMixin so each file is one concern. This
|
||||
fires only when claude_agent_sdk isn't installed (dev fallback): it fakes one Bash approval +
|
||||
tool-result + assistant reply so the UI is exercisable without a real model. self.p_stream_* /
|
||||
self.sessions resolve across the MRO exactly as when this lived on AgentLaunchMixin."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import ApprovalRequest, Message
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MockAgentMixin:
|
||||
@typechecked
|
||||
async def p_run_mock_agent(self, session_id: str, prompt: str):
|
||||
"""Mock agent loop for development without claude_agent_sdk installed."""
|
||||
session = self.sessions.get(session_id)
|
||||
if not session:
|
||||
return
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
request_id = uuid4().hex
|
||||
approval_req = ApprovalRequest(
|
||||
id=request_id,
|
||||
session_id=session_id,
|
||||
tool_name="Bash",
|
||||
tool_input={"command": f"echo 'Processing: {prompt}'", "description": "Echo the user prompt"},
|
||||
)
|
||||
session.pending_approvals.append(approval_req)
|
||||
session.status = "waiting_approval"
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
"status": "waiting_approval",
|
||||
})
|
||||
|
||||
decision = await ws_manager.send_approval_request(
|
||||
session_id, request_id, "Bash",
|
||||
{"command": f"echo 'Processing: {prompt}'", "description": "Echo the user prompt"}
|
||||
)
|
||||
|
||||
session.pending_approvals = [a for a in session.pending_approvals if a.id != request_id]
|
||||
session.status = "running"
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
"status": "running",
|
||||
})
|
||||
|
||||
tool_input_content = {"tool": "Bash", "input": {"command": f"echo 'Processing: {prompt}'"}, "approved": decision.get("behavior") == "allow"}
|
||||
tool_msg_id = uuid4().hex
|
||||
await self.p_stream_tool_input(
|
||||
session_id, tool_msg_id, "Bash",
|
||||
json.dumps(tool_input_content["input"], indent=2),
|
||||
)
|
||||
tool_msg = Message(id=tool_msg_id, role="tool_call", content=tool_input_content, branch_id=session.active_branch_id)
|
||||
session.messages.append(tool_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": tool_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if decision.get("behavior") == "allow":
|
||||
tool_result = Message(role="tool_result", content=f"Processing: {prompt}", branch_id=session.active_branch_id)
|
||||
session.messages.append(tool_result)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": tool_result.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
asst_text = (
|
||||
f"I've processed your request: \"{prompt}\"\n\n"
|
||||
"This is a mock response because `claude-agent-sdk` is not installed. "
|
||||
"Install it with `pip install claude-agent-sdk` to use real Claude Code instances.\n\n"
|
||||
f"The agent was configured with:\n- Model: {session.model}\n- Mode: {session.mode}"
|
||||
)
|
||||
asst_msg_id = uuid4().hex
|
||||
await self.p_stream_text(session_id, asst_msg_id, asst_text)
|
||||
|
||||
asst_msg = Message(id=asst_msg_id, role="assistant", content=asst_text, branch_id=session.active_branch_id)
|
||||
session.messages.append(asst_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
"session_id": session_id,
|
||||
"message": asst_msg.model_dump(mode="json"),
|
||||
})
|
||||
|
||||
session.status = "completed"
|
||||
session.closed_at = datetime.now()
|
||||
# Mock branch (claude_agent_sdk missing): leave cost untouched so
|
||||
# it stays at its 0.0 default. A fake nonzero value here would
|
||||
# poison the cost shown in the session header during dev. The
|
||||
# `_mock_run` flag is read by the close path so a mock session
|
||||
# doesn't get reported to the cloud as a real one.
|
||||
setattr(session, "_mock_run", True)
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
"status": "completed",
|
||||
"session": session.model_dump(mode="json"),
|
||||
})
|
||||
await ws_manager.send_to_session(session_id, "agent:cost_update", {
|
||||
"session_id": session_id,
|
||||
"cost_usd": session.cost_usd,
|
||||
})
|
||||
Reference in New Issue
Block a user