feat: Add agent workspace system and runtime management

- Add agent core modules (agent_core, factory, registry, skill_loader)
- Add runtime system for agent execution management
- Add REST API for agents, workspaces, and runtime control
- Add process supervisor for agent lifecycle management
- Add workspace template system with agent profiles
- Add frontend RuntimeView and runtime API integration
- Add per-agent skill workspaces for smoke_fullstack run
- Refactor skill system with active/installed separation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 16:43:29 +08:00
parent 2daf5717ba
commit 59b44545d0
121 changed files with 8384 additions and 358 deletions

View File

@@ -19,6 +19,8 @@ from backend.utils.settlement import SettlementCoordinator
from backend.utils.terminal_dashboard import get_dashboard
from backend.core.state_sync import StateSync
from backend.utils.trade_executor import PortfolioTradeExecutor
from backend.runtime.manager import TradingRuntimeManager
from backend.runtime.session import TradingSessionKey
logger = logging.getLogger(__name__)
@@ -46,6 +48,8 @@ class TradingPipeline:
6. Reflection phase: broadcast closing P&L, agents record to long-term memory
Real-time updates via StateSync after each agent completes.
Supports both legacy agent lists and new workspace-based agent loading.
"""
def __init__(
@@ -56,6 +60,9 @@ class TradingPipeline:
state_sync: Optional["StateSync"] = None,
settlement_coordinator: Optional[SettlementCoordinator] = None,
max_comm_cycles: Optional[int] = None,
workspace_id: Optional[str] = None,
agent_factory: Optional[Any] = None,
runtime_manager: Optional[TradingRuntimeManager] = None,
):
self.analysts = analysts
self.risk_manager = risk_manager
@@ -66,6 +73,10 @@ class TradingPipeline:
os.getenv("MAX_COMM_CYCLES", "2"),
)
self.conference_summary = None # Store latest conference summary
self.workspace_id = workspace_id
self.agent_factory = agent_factory
self.runtime_manager = runtime_manager
self._session_key: Optional[str] = None
async def run_cycle(
self,
@@ -80,6 +91,7 @@ class TradingPipeline:
get_close_prices_fn: Optional[
Callable[[], Awaitable[Dict[str, float]]]
] = None,
execute_decisions: bool = True,
) -> Dict[str, Any]:
"""
Run one complete trading cycle
@@ -101,6 +113,12 @@ class TradingPipeline:
Each agent's result is broadcast immediately via StateSync.
"""
_log(f"Starting cycle {date} - {len(tickers)} tickers")
session_key = TradingSessionKey(date=date).key()
self._session_key = session_key
if self.runtime_manager:
self.runtime_manager.set_session_key(session_key)
self._runtime_log_event("cycle:start", {"tickers": tickers, "date": date})
self._runtime_batch_status(self.analysts, "analysis_in_progress")
# Phase 0: Clear short-term memory to avoid cross-day context pollution
_log("Phase 0: Clearing memory")
@@ -123,6 +141,7 @@ class TradingPipeline:
# Phase 1.2: Risk Manager
_log("Phase 1.2: Risk assessment")
self._runtime_update_status(self.risk_manager, "risk_assessment")
risk_assessment = await self._run_risk_manager_with_sync(
tickers,
date,
@@ -161,6 +180,7 @@ class TradingPipeline:
# Phase 3: PM makes decisions
_log("Phase 3.1: PM makes decisions")
self._runtime_update_status(self.pm, "decision_phase")
pm_result = await self._run_pm_with_sync(
tickers,
date,
@@ -169,10 +189,17 @@ class TradingPipeline:
risk_assessment,
)
# Phase 4: Execute decisions
_log("Phase 4: Executing trades")
decisions = pm_result.get("decisions", {})
execution_result = self._execute_decisions(decisions, prices, date)
execution_result = {
"executed_trades": [],
"portfolio": self.pm.get_portfolio_state(),
}
if execute_decisions:
_log("Phase 4: Executing trades")
self._runtime_update_status(self.pm, "executing")
execution_result = self._execute_decisions(decisions, prices, date)
else:
_log("Phase 4: Skipping trade execution")
# Live mode: wait for market close before settlement
if get_close_prices_fn:
@@ -184,6 +211,10 @@ class TradingPipeline:
settlement_result = None
if close_prices and self.settlement_coordinator:
_log("Phase 5: Daily review and generate memories")
self._runtime_batch_status(
[self.risk_manager] + self.analysts + [self.pm],
"settlement",
)
agent_trajectories = await self._capture_agent_trajectories()
@@ -214,8 +245,17 @@ class TradingPipeline:
settlement_result=settlement_result,
conference_summary=self.conference_summary,
)
self._runtime_batch_status(
[self.risk_manager] + self.analysts + [self.pm],
"reflection",
)
_log(f"Cycle complete: {date}")
self._runtime_batch_status(
self.analysts + [self.risk_manager, self.pm],
"idle",
)
self._runtime_log_event("cycle:end", {"tickers": tickers, "date": date})
return {
"analyst_results": analyst_results,
@@ -1306,3 +1346,122 @@ class TradingPipeline:
if decision_texts:
return "Decisions: " + "; ".join(decision_texts)
return "Portfolio analysis completed. No trades recommended."
def load_agents_from_workspace(
self,
workspace_id: str,
agent_factory: Optional[Any] = None,
) -> Dict[str, Any]:
"""
Load agents from workspace using AgentFactory.
This method supports the new EvoAgent architecture by loading
agents from a workspace instead of using hardcoded agents.
Args:
workspace_id: Workspace identifier
agent_factory: Optional AgentFactory instance (uses self.agent_factory if None)
Returns:
Dictionary with loaded agents:
{
"analysts": List[EvoAgent],
"risk_manager": EvoAgent,
"portfolio_manager": EvoAgent,
}
Raises:
ValueError: If workspace doesn't exist or no agents found
"""
factory = agent_factory or self.agent_factory
if factory is None:
from backend.agents import AgentFactory
factory = AgentFactory()
# Check workspace exists
if not factory.workspaces_root.exists():
raise ValueError(f"Workspaces root does not exist: {factory.workspaces_root}")
workspace_dir = factory.workspaces_root / workspace_id
if not workspace_dir.exists():
raise ValueError(f"Workspace '{workspace_id}' does not exist")
# Load agents from workspace
agents_data = factory.list_agents(workspace_id=workspace_id)
if not agents_data:
raise ValueError(f"No agents found in workspace '{workspace_id}'")
# Categorize agents by type
analysts = []
risk_manager = None
portfolio_manager = None
for agent_data in agents_data:
agent_type = agent_data.get("agent_type", "unknown")
agent_id = agent_data.get("agent_id")
# Load full agent configuration
config_path = Path(agent_data.get("config_path", ""))
if config_path.exists():
agent = factory.load_agent(agent_id, workspace_id)
if agent_type.endswith("_analyst"):
analysts.append(agent)
elif agent_type == "risk_manager":
risk_manager = agent
elif agent_type == "portfolio_manager":
portfolio_manager = agent
if not analysts:
raise ValueError(f"No analysts found in workspace '{workspace_id}'")
if risk_manager is None:
raise ValueError(f"No risk_manager found in workspace '{workspace_id}'")
if portfolio_manager is None:
raise ValueError(f"No portfolio_manager found in workspace '{workspace_id}'")
return {
"analysts": analysts,
"risk_manager": risk_manager,
"portfolio_manager": portfolio_manager,
}
def reload_agents_from_workspace(self, workspace_id: Optional[str] = None) -> None:
"""
Reload all agents from workspace.
This updates self.analysts, self.risk_manager, and self.pm
with agents loaded from the specified workspace.
Args:
workspace_id: Workspace ID (uses self.workspace_id if None)
"""
ws_id = workspace_id or self.workspace_id
if not ws_id:
raise ValueError("No workspace_id specified")
loaded = self.load_agents_from_workspace(ws_id)
self.analysts = loaded["analysts"]
self.risk_manager = loaded["risk_manager"]
self.pm = loaded["portfolio_manager"]
self.workspace_id = ws_id
logger.info(f"Reloaded {len(self.analysts)} analysts from workspace '{ws_id}'")
def _runtime_update_status(self, agent: Any, status: str) -> None:
if not self.runtime_manager:
return
agent_id = getattr(agent, "agent_id", None) or getattr(agent, "name", None)
if not agent_id:
return
self.runtime_manager.update_agent_status(agent_id, status, self._session_key)
def _runtime_batch_status(self, agents: List[Any], status: str) -> None:
for agent in agents:
self._runtime_update_status(agent, status)
def _runtime_log_event(self, event: str, details: Optional[Dict[str, Any]] = None) -> None:
if not self.runtime_manager:
return
self.runtime_manager.log_event(event, details)