#pragma once // Step 59: WebSocket agent endpoint // // WebSocketAgentServer: manages agent sessions and routes JSON-RPC messages. // Agents connect via WebSocket and exchange JSON-RPC over the connection. // // Architecture: // WebSocketTransport (abstract) — handles raw WebSocket I/O // WebSocketAgentServer — session management + JSON-RPC dispatch // MockWebSocketTransport — for testing without real sockets // // The server runs alongside the existing stdin/stdout JSON-RPC in // orchestrator_main.cpp. Both share the same processRequest() handler // via the setRequestHandler() callback. #include #include #include #include #include #include #include #include #include #include "PrimitivesRegistry.h" #include "ast/Module.h" using json = nlohmann::json; // --------------------------------------------------------------------------- // AgentSession — represents one connected agent // --------------------------------------------------------------------------- struct AgentSession { std::string sessionId; std::string agentName; // optional, set via setAgentName RPC bool connected = false; uint64_t connectedAtMs = 0; uint64_t lastMessageAtMs = 0; int messageCount = 0; }; // Callback types using JsonRpcHandler = std::function; using SessionEventCallback = std::function; using RequestLogCallback = std::function; using LibraryContextProvider = std::function; // --------------------------------------------------------------------------- // WebSocketTransport — abstract transport interface // --------------------------------------------------------------------------- class WebSocketTransport { public: virtual ~WebSocketTransport() = default; virtual bool start(int port) = 0; virtual void stop() = 0; virtual bool isRunning() const = 0; // Send a text message to a connected session virtual bool sendMessage(const std::string& sessionId, const std::string& message) = 0; // --- handler registration (called by WebSocketAgentServer) --- void setMessageHandler( std::function handler) { messageHandler_ = std::move(handler); } void setConnectHandler( std::function handler) { connectHandler_ = std::move(handler); } void setDisconnectHandler( std::function handler) { disconnectHandler_ = std::move(handler); } protected: std::function messageHandler_; std::function connectHandler_; std::function disconnectHandler_; }; // --------------------------------------------------------------------------- // WebSocketAgentServer // --------------------------------------------------------------------------- class WebSocketAgentServer { public: struct LibraryContext { json data; static LibraryContext fromRegistry(PrimitivesRegistry& registry, Module* root, const std::string& language) { LibraryContext ctx; registry.setRoot(root); registry.setLanguage(language); json funcs = json::array(); json types = json::array(); json consts = json::array(); auto addSymbol = [](json& arr, const PrimitiveSymbol& sym) { arr.push_back({ {"name", sym.name}, {"kind", sym.kind}, {"source", sym.source}, {"library", sym.library} }); }; for (const auto& sym : registry.getAvailableFunctions()) addSymbol(funcs, sym); for (const auto& sym : registry.getAvailableTypes()) addSymbol(types, sym); for (const auto& sym : registry.getAvailableConstants()) addSymbol(consts, sym); ctx.data = { {"language", language}, {"functions", funcs}, {"types", types}, {"constants", consts} }; return ctx; } }; explicit WebSocketAgentServer(std::unique_ptr transport) : transport_(std::move(transport)) { transport_->setMessageHandler( [this](const std::string& sid, const std::string& msg) { onMessage(sid, msg); }); transport_->setConnectHandler( [this](const std::string& sid) { onConnect(sid); }); transport_->setDisconnectHandler( [this](const std::string& sid) { onDisconnect(sid); }); } bool start(int port) { port_ = port; return transport_->start(port); } void stop() { transport_->stop(); } bool isRunning() const { return transport_->isRunning(); } int getPort() const { return port_; } // Register a fallback JSON-RPC handler for methods the server doesn't // handle itself (e.g. the orchestrator's processRequest). void setRequestHandler(JsonRpcHandler handler) { requestHandler_ = std::move(handler); } // Register a callback for session lifecycle events ("connected" / // "disconnected"). void setSessionEventCallback(SessionEventCallback cb) { sessionEventCallback_ = std::move(cb); } void setRequestLogCallback(RequestLogCallback cb) { requestLogCallback_ = std::move(cb); } void setLibraryContextProvider(LibraryContextProvider cb) { libraryContextProvider_ = std::move(cb); } // --- session queries --- const AgentSession* getSession(const std::string& sessionId) const { auto it = sessions_.find(sessionId); return it != sessions_.end() ? &it->second : nullptr; } std::vector getActiveSessions() const { std::vector result; for (const auto& [id, s] : sessions_) { if (s.connected) result.push_back(s); } return result; } size_t getActiveSessionCount() const { size_t n = 0; for (const auto& [id, s] : sessions_) { if (s.connected) ++n; } return n; } private: // --- transport callbacks --- void onConnect(const std::string& sessionId) { AgentSession s; s.sessionId = sessionId; s.connected = true; s.connectedAtMs = nowMs(); s.lastMessageAtMs = s.connectedAtMs; sessions_[sessionId] = s; if (sessionEventCallback_) sessionEventCallback_(s, "connected"); if (libraryContextProvider_) { json payload = libraryContextProvider_(sessionId); json message = { {"jsonrpc", "2.0"}, {"method", "libraryContext"}, {"params", payload} }; transport_->sendMessage(sessionId, message.dump()); } } void onDisconnect(const std::string& sessionId) { auto it = sessions_.find(sessionId); if (it != sessions_.end()) { it->second.connected = false; if (sessionEventCallback_) sessionEventCallback_(it->second, "disconnected"); } } void onMessage(const std::string& sessionId, const std::string& message) { // Update session stats auto it = sessions_.find(sessionId); if (it != sessions_.end()) { it->second.lastMessageAtMs = nowMs(); it->second.messageCount++; } // Parse + dispatch json response; try { json request = json::parse(message); response = processRequest(request, sessionId); } catch (const std::exception& e) { response = { {"jsonrpc", "2.0"}, {"id", nullptr}, {"error", {{"code", -32700}, {"message", std::string("Parse error: ") + e.what()}}} }; } if (requestLogCallback_) { try { requestLogCallback_(sessionId, json::parse(message), response); } catch (...) { // Ignore logging parse errors } } transport_->sendMessage(sessionId, response.dump()); } // Route a JSON-RPC request. Built-in methods are handled first; // everything else is forwarded to the registered handler. json processRequest(const json& request, const std::string& sessionId) { json response; response["jsonrpc"] = "2.0"; response["id"] = request.contains("id") ? request["id"] : json(nullptr); try { std::string method = request.at("method").get(); if (method == "ping") { response["result"] = json{ {"message", "pong"}, {"sessionId", sessionId} }; } else if (method == "getSessionInfo") { auto* sess = getSession(sessionId); if (sess) { response["result"] = { {"sessionId", sess->sessionId}, {"agentName", sess->agentName}, {"connected", sess->connected}, {"messageCount", sess->messageCount} }; } else { response["error"] = { {"code", -32603}, {"message", "Session not found"} }; } } else if (method == "setAgentName") { std::string name = request.at("params").at("name").get(); auto sit = sessions_.find(sessionId); if (sit != sessions_.end()) { sit->second.agentName = name; response["result"] = true; } else { response["error"] = { {"code", -32603}, {"message", "Session not found"} }; } } else if (method == "listSessions") { json arr = json::array(); for (const auto& [id, s] : sessions_) { if (s.connected) { arr.push_back({ {"sessionId", s.sessionId}, {"agentName", s.agentName}, {"messageCount", s.messageCount} }); } } response["result"] = arr; } else if (requestHandler_) { // Delegate to the orchestrator's processRequest response = requestHandler_(request, sessionId); // Ensure jsonrpc and id are set response["jsonrpc"] = "2.0"; if (!response.contains("id")) response["id"] = request.contains("id") ? request["id"] : json(nullptr); } else { response["error"] = { {"code", -32601}, {"message", "Method not found: " + method} }; } } catch (const std::exception& e) { response["error"] = { {"code", -32600}, {"message", std::string("Invalid request: ") + e.what()} }; } return response; } static uint64_t nowMs() { using namespace std::chrono; return static_cast( duration_cast( steady_clock::now().time_since_epoch()) .count()); } std::unique_ptr transport_; std::map sessions_; JsonRpcHandler requestHandler_; SessionEventCallback sessionEventCallback_; RequestLogCallback requestLogCallback_; LibraryContextProvider libraryContextProvider_; int port_ = 0; }; // --------------------------------------------------------------------------- // MockWebSocketTransport — for testing without real sockets // --------------------------------------------------------------------------- class MockWebSocketTransport : public WebSocketTransport { public: bool start(int port) override { port_ = port; running_ = true; return true; } void stop() override { running_ = false; } bool isRunning() const override { return running_; } bool sendMessage(const std::string& sessionId, const std::string& message) override { sentMessages_[sessionId].push_back(message); return true; } // --- test helpers: simulate agent behaviour --- // Simulate an agent opening a WebSocket connection std::string simulateConnect() { std::string sessionId = "agent_" + std::to_string(nextId_++); if (connectHandler_) connectHandler_(sessionId); return sessionId; } // Simulate an agent closing its connection void simulateDisconnect(const std::string& sessionId) { if (disconnectHandler_) disconnectHandler_(sessionId); } // Simulate an agent sending a text message void simulateMessage(const std::string& sessionId, const std::string& message) { if (messageHandler_) messageHandler_(sessionId, message); } // Get all messages the server sent back to a session std::vector getSentMessages( const std::string& sessionId) const { auto it = sentMessages_.find(sessionId); return it != sentMessages_.end() ? it->second : std::vector{}; } // Get the most recent message sent to a session std::string getLastSentMessage(const std::string& sessionId) const { auto it = sentMessages_.find(sessionId); if (it != sentMessages_.end() && !it->second.empty()) return it->second.back(); return ""; } // Clear recorded messages (useful between test cases) void clearSentMessages() { sentMessages_.clear(); } private: bool running_ = false; int port_ = 0; int nextId_ = 1; std::map> sentMessages_; };