feat(agent): complete EvoAgent integration for all 6 agent roles
Migrate all agent roles from Legacy to EvoAgent architecture: - fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst - risk_manager, portfolio_manager Key changes: - EvoAgent now supports Portfolio Manager compatibility methods (_make_decision, get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio) - Add UnifiedAgentFactory for centralized agent creation - ToolGuard with batch approval API and WebSocket broadcast - Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent) - Remove backend/agents/compat.py migration shim - Add run_id alongside workspace_id for semantic clarity - Complete integration test coverage (13 tests) - All smoke tests passing for 6 agent roles Constraint: Must maintain backward compatibility with existing run configs Constraint: Memory support must work with EvoAgent (no fallback to Legacy) Rejected: Separate PM implementation for EvoAgent | unified approach cleaner Confidence: high Scope-risk: broad Directive: EVO_AGENT_IDS env var still respected but defaults to all roles Not-tested: Kubernetes sandbox mode for skill execution
This commit is contained in:
@@ -242,7 +242,6 @@ def test_runtime_cleanup_endpoint_prunes_old_runs(monkeypatch, tmp_path):
|
||||
def test_runtime_history_lists_recent_runs(monkeypatch, tmp_path):
|
||||
run_dir = tmp_path / "runs" / "20260324_120000"
|
||||
(run_dir / "state").mkdir(parents=True)
|
||||
(run_dir / "team_dashboard").mkdir(parents=True)
|
||||
(run_dir / "state" / "runtime_state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -256,8 +255,13 @@ def test_runtime_history_lists_recent_runs(monkeypatch, tmp_path):
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(run_dir / "team_dashboard" / "summary.json").write_text(
|
||||
json.dumps({"totalTrades": 3, "totalAssetValue": 123456.0}),
|
||||
(run_dir / "state" / "server_state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"portfolio": {"total_value": 123456.0},
|
||||
"trades": [{}, {}, {}],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
@@ -270,6 +274,7 @@ def test_runtime_history_lists_recent_runs(monkeypatch, tmp_path):
|
||||
payload = response.json()
|
||||
assert payload["runs"][0]["run_id"] == "20260324_120000"
|
||||
assert payload["runs"][0]["total_trades"] == 3
|
||||
assert payload["runs"][0]["total_asset_value"] == 123456.0
|
||||
|
||||
|
||||
def test_restore_run_assets_copies_state(monkeypatch, tmp_path):
|
||||
@@ -278,6 +283,7 @@ def test_restore_run_assets_copies_state(monkeypatch, tmp_path):
|
||||
(source_run / "state").mkdir(parents=True)
|
||||
(source_run / "agents").mkdir(parents=True)
|
||||
(source_run / "team_dashboard" / "_internal_state.json").write_text("{}", encoding="utf-8")
|
||||
(source_run / "team_dashboard" / "summary.json").write_text("{}", encoding="utf-8")
|
||||
(source_run / "state" / "server_state.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
target_run = tmp_path / "runs" / "20260324_130000"
|
||||
@@ -288,6 +294,237 @@ def test_restore_run_assets_copies_state(monkeypatch, tmp_path):
|
||||
|
||||
assert (target_run / "team_dashboard" / "_internal_state.json").exists()
|
||||
assert (target_run / "state" / "server_state.json").exists()
|
||||
assert not (target_run / "team_dashboard" / "summary.json").exists()
|
||||
|
||||
|
||||
def test_runtime_service_routes_contract_stability():
|
||||
"""Verify runtime API routes maintain contract stability."""
|
||||
app = create_app()
|
||||
routes = {route.path: route for route in app.routes if hasattr(route, "methods")}
|
||||
|
||||
# Core runtime lifecycle endpoints
|
||||
assert "/api/runtime/start" in routes
|
||||
assert "/api/runtime/stop" in routes
|
||||
assert "/api/runtime/restart" in routes
|
||||
assert "/api/runtime/current" in routes
|
||||
|
||||
# Configuration endpoints
|
||||
assert "/api/runtime/config" in routes
|
||||
|
||||
# Query endpoints
|
||||
assert "/api/runtime/agents" in routes
|
||||
assert "/api/runtime/events" in routes
|
||||
assert "/api/runtime/history" in routes
|
||||
assert "/api/runtime/context" in routes
|
||||
assert "/api/runtime/logs" in routes
|
||||
|
||||
# Gateway endpoints
|
||||
assert "/api/runtime/gateway/status" in routes
|
||||
assert "/api/runtime/gateway/port" in routes
|
||||
|
||||
# Maintenance endpoints
|
||||
assert "/api/runtime/cleanup" in routes
|
||||
|
||||
|
||||
def test_runtime_service_start_stop_lifecycle_contract(monkeypatch, tmp_path):
|
||||
"""Test the start/stop lifecycle maintains expected contract."""
|
||||
run_dir = tmp_path / "runs" / "test_run"
|
||||
state_dir = run_dir / "state"
|
||||
state_dir.mkdir(parents=True)
|
||||
# Create runtime_state.json so /api/runtime/current can find the context after stop
|
||||
(state_dir / "runtime_state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"context": {
|
||||
"config_name": "test_run",
|
||||
"run_dir": str(run_dir),
|
||||
"bootstrap_values": {"tickers": ["AAPL", "MSFT"]},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
class _DummyManager:
|
||||
def __init__(self, config_name, run_dir, bootstrap):
|
||||
self.config_name = config_name
|
||||
self.run_dir = Path(run_dir)
|
||||
self.bootstrap = bootstrap
|
||||
self.context = None
|
||||
|
||||
def prepare_run(self):
|
||||
self.context = type(
|
||||
"Ctx",
|
||||
(),
|
||||
{
|
||||
"config_name": self.config_name,
|
||||
"run_dir": self.run_dir,
|
||||
"bootstrap_values": self.bootstrap,
|
||||
},
|
||||
)()
|
||||
return self.context
|
||||
|
||||
class _DummyProcess:
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(runtime_module, "PROJECT_ROOT", tmp_path)
|
||||
monkeypatch.setattr(runtime_module, "_find_available_port", lambda start_port=8765, max_port=9000: 8765)
|
||||
monkeypatch.setattr(runtime_module, "_start_gateway_process", lambda **kwargs: _DummyProcess())
|
||||
monkeypatch.setattr(runtime_module, "_stop_gateway", lambda: True)
|
||||
monkeypatch.setattr("backend.runtime.manager.TradingRuntimeManager", _DummyManager)
|
||||
runtime_state = runtime_module.get_runtime_state()
|
||||
runtime_state.gateway_process = None
|
||||
|
||||
with TestClient(create_app()) as client:
|
||||
# Start runtime
|
||||
start_response = client.post(
|
||||
"/api/runtime/start",
|
||||
json={
|
||||
"launch_mode": "fresh",
|
||||
"tickers": ["AAPL", "MSFT"],
|
||||
"schedule_mode": "daily",
|
||||
"interval_minutes": 60,
|
||||
"trigger_time": "09:30",
|
||||
"max_comm_cycles": 2,
|
||||
"initial_cash": 100000.0,
|
||||
"margin_requirement": 0.0,
|
||||
"enable_memory": False,
|
||||
"mode": "live",
|
||||
"poll_interval": 10,
|
||||
},
|
||||
)
|
||||
|
||||
assert start_response.status_code == 200
|
||||
start_payload = start_response.json()
|
||||
assert "run_id" in start_payload
|
||||
assert "status" in start_payload
|
||||
assert "run_dir" in start_payload
|
||||
assert "gateway_port" in start_payload
|
||||
assert "message" in start_payload
|
||||
assert start_payload["status"] == "started"
|
||||
|
||||
# Get current runtime while running
|
||||
current_response = client.get("/api/runtime/current")
|
||||
assert current_response.status_code == 200
|
||||
current_payload = current_response.json()
|
||||
assert "run_id" in current_payload
|
||||
assert "run_dir" in current_payload
|
||||
assert "is_running" in current_payload
|
||||
assert "gateway_port" in current_payload
|
||||
assert "bootstrap" in current_payload
|
||||
|
||||
# Stop runtime
|
||||
stop_response = client.post("/api/runtime/stop?force=true")
|
||||
assert stop_response.status_code == 200
|
||||
stop_payload = stop_response.json()
|
||||
assert "status" in stop_payload
|
||||
assert "message" in stop_payload
|
||||
assert stop_payload["status"] == "stopped"
|
||||
|
||||
|
||||
def test_runtime_service_agents_events_contract(monkeypatch, tmp_path):
|
||||
"""Test agents and events endpoints maintain contract."""
|
||||
run_dir = tmp_path / "runs" / "demo"
|
||||
state_dir = run_dir / "state"
|
||||
state_dir.mkdir(parents=True)
|
||||
(state_dir / "runtime_state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"context": {
|
||||
"config_name": "demo",
|
||||
"run_dir": str(run_dir),
|
||||
"bootstrap_values": {"tickers": ["AAPL"]},
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "fundamentals_analyst",
|
||||
"status": "idle",
|
||||
"last_session": "2026-03-30",
|
||||
"last_updated": "2026-03-30T10:00:00",
|
||||
},
|
||||
{
|
||||
"agent_id": "technical_analyst",
|
||||
"status": "analyzing",
|
||||
"last_session": None,
|
||||
"last_updated": "2026-03-30T10:05:00",
|
||||
},
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"timestamp": "2026-03-30T10:00:00",
|
||||
"event": "agent_registered",
|
||||
"details": {"agent_id": "fundamentals_analyst"},
|
||||
"session": "2026-03-30",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(runtime_module, "PROJECT_ROOT", tmp_path)
|
||||
monkeypatch.setattr(runtime_module, "_is_gateway_running", lambda: True)
|
||||
runtime_module.get_runtime_state().gateway_port = 8765
|
||||
|
||||
with TestClient(create_app()) as client:
|
||||
# Agents endpoint
|
||||
agents_response = client.get("/api/runtime/agents")
|
||||
assert agents_response.status_code == 200
|
||||
agents_payload = agents_response.json()
|
||||
assert "agents" in agents_payload
|
||||
assert len(agents_payload["agents"]) == 2
|
||||
agent = agents_payload["agents"][0]
|
||||
assert "agent_id" in agent
|
||||
assert "status" in agent
|
||||
assert "last_session" in agent
|
||||
assert "last_updated" in agent
|
||||
|
||||
# Events endpoint
|
||||
events_response = client.get("/api/runtime/events")
|
||||
assert events_response.status_code == 200
|
||||
events_payload = events_response.json()
|
||||
assert "events" in events_payload
|
||||
assert len(events_payload["events"]) == 1
|
||||
event = events_payload["events"][0]
|
||||
assert "timestamp" in event
|
||||
assert "event" in event
|
||||
assert "details" in event
|
||||
assert "session" in event
|
||||
|
||||
|
||||
def test_runtime_service_gateway_status_contract(monkeypatch, tmp_path):
|
||||
"""Test gateway status endpoint maintains contract."""
|
||||
run_dir = tmp_path / "runs" / "demo"
|
||||
state_dir = run_dir / "state"
|
||||
state_dir.mkdir(parents=True)
|
||||
(state_dir / "runtime_state.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"context": {
|
||||
"config_name": "demo",
|
||||
"run_dir": str(run_dir),
|
||||
"bootstrap_values": {},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(runtime_module, "PROJECT_ROOT", tmp_path)
|
||||
monkeypatch.setattr(runtime_module, "_is_gateway_running", lambda: True)
|
||||
runtime_module.get_runtime_state().gateway_port = 8765
|
||||
|
||||
with TestClient(create_app()) as client:
|
||||
response = client.get("/api/runtime/gateway/status")
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert "is_running" in payload
|
||||
assert "port" in payload
|
||||
assert "run_id" in payload
|
||||
assert payload["is_running"] is True
|
||||
assert payload["port"] == 8765
|
||||
assert payload["run_id"] == "demo"
|
||||
|
||||
|
||||
def test_start_runtime_restore_reuses_historical_run_id(monkeypatch, tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user