base_url = untrailingslashit($settings["memanto_url"] ?? ""); } // ========================================================================= // Configuration & Health // ========================================================================= /** * Whether MEMANTO is configured (URL + Moorcheh key set). * * @return bool */ public function is_configured() { return !empty($this->base_url) && !empty($this->get_moorcheh_key()); } /** * Whether MEMANTO is enabled in settings. * * @return bool */ public function is_enabled() { $settings = get_option("wp_agentic_writer_settings", []); return !empty($settings["memanto_enabled"]); } /** * Whether MEMANTO is enabled, configured, and reachable. * * @return bool */ public function is_active() { return $this->is_enabled() && $this->is_configured() && $this->is_healthy(); } /** * Check MEMANTO health endpoint. Result cached for 5 minutes. * * @return bool True if healthy. */ public function is_healthy() { if (!$this->is_configured()) { return false; } // Use cached result if fresh (5 minutes). if (null !== $this->health_cache) { if (time() - $this->health_cache["checked_at"] < 300) { return $this->health_cache["healthy"]; } } // Also check transient for cross-request caching. $cached = get_transient("wpaw_memanto_health"); if ( false !== $cached && isset($cached["checked_at"]) && time() - $cached["checked_at"] < 300 ) { $this->health_cache = $cached; return $cached["healthy"]; } $response = $this->get("/health"); if (is_wp_error($response)) { $this->health_cache = ["healthy" => false, "checked_at" => time()]; set_transient("wpaw_memanto_health", $this->health_cache, 300); return false; } $healthy = !empty($response["status"]) && "healthy" === $response["status"]; $this->health_cache = ["healthy" => $healthy, "checked_at" => time()]; set_transient("wpaw_memanto_health", $this->health_cache, 300); return $healthy; } /** * Force-refresh the health check (used by Test Connection button). * * @return array { healthy: bool, details: array|null } */ public function check_health_fresh() { if (!$this->is_configured()) { return ["healthy" => false, "details" => null]; } delete_transient("wpaw_memanto_health"); $this->health_cache = null; $response = $this->get("/health"); if (is_wp_error($response)) { return [ "healthy" => false, "details" => ["error" => $response->get_error_message()], ]; } $healthy = !empty($response["status"]) && "healthy" === $response["status"]; $this->health_cache = ["healthy" => $healthy, "checked_at" => time()]; set_transient("wpaw_memanto_health", $this->health_cache, 300); return ["healthy" => $healthy, "details" => $response]; } // ========================================================================= // Agent Management // ========================================================================= /** * Ensure an agent exists. Creates if not found. * * @param string $agent_id Agent identifier (e.g. "wp-user-1" or "wp-post-42"). * @return bool True on success. */ public function ensure_agent($agent_id) { if (!$this->is_configured()) { return false; } // Check if agent already exists. $agent = $this->get("/api/v2/agents/{$agent_id}"); if (!is_wp_error($agent) && !empty($agent["agent_id"])) { return true; } // Create agent. $result = $this->post("/api/v2/agents", [ "agent_id" => $agent_id, "pattern" => "support", "description" => "WP Agentic Writer agent", ]); return !is_wp_error($result); } // ========================================================================= // Session Management // ========================================================================= /** * Activate a session for an agent. Returns cached token if still valid. * * @param string $agent_id Agent identifier. * @return string|false Session token or false on failure. */ public function activate_session($agent_id) { if (!$this->is_configured()) { return false; } $transient_key = "wpaw_memanto_token_" . md5($agent_id); $cached_token = get_transient($transient_key); if (!empty($cached_token)) { return $cached_token; } $response = $this->post( "/api/v2/agents/{$agent_id}/activate", [], $agent_id, ); if (is_wp_error($response) || empty($response["session_token"])) { wpaw_debug_log("MEMANTO activate_session failed", [ "agent_id" => $agent_id, ]); return false; } $token = $response["session_token"]; $expires_at = strtotime($response["expires_at"] ?? "+6 hours"); $ttl = max(60, $expires_at - time() - 300); // Expire 5 min before actual. set_transient($transient_key, $token, $ttl); return $token; } /** * Deactivate a session for an agent. * * Clears the cached token and sends the deactivate request * without injecting a session token (avoids re-activation loop). * * @param string $agent_id Agent identifier. * @return bool True on success. */ public function deactivate_session($agent_id) { $transient_key = "wpaw_memanto_token_" . md5($agent_id); delete_transient($transient_key); if (!$this->is_configured()) { return false; } $url = $this->base_url . "/api/v2/agents/{$agent_id}/deactivate"; $headers = $this->get_headers(); $response = wp_remote_post($url, [ "headers" => $headers, "body" => wp_json_encode([]), "timeout" => 10, ]); if (is_wp_error($response)) { wpaw_debug_log("MEMANTO deactivate_session failed", [ "agent_id" => $agent_id, ]); return false; } return true; } // ========================================================================= // Memory Operations // ========================================================================= /** * Store a memory. * * @param string $agent_id Agent identifier. * @param string $content Memory content (max 10000 chars). * @param string $type Memory type (fact, preference, goal, decision, artifact, learning, event, instruction, relationship, context, observation, commitment, error). * @param array $tags Optional tags. * @param string $title Optional title (max 100 chars). * @return bool True on success. */ public function remember( $agent_id, $content, $type = "context", $tags = [], $title = "", ) { if (!$this->is_active()) { return false; } $body = [ "content" => mb_substr($content, 0, 10000), "type" => $type, "tags" => $tags, "source" => "wp-agentic-writer", ]; if (!empty($title)) { $body["title"] = mb_substr($title, 0, 100); } $response = $this->post( "/api/v2/agents/{$agent_id}/remember", $body, $agent_id, ); if (is_wp_error($response)) { wpaw_debug_log("MEMANTO remember failed", [ "agent_id" => $agent_id, "type" => $type, "error" => $response->get_error_message(), ]); return false; } return true; } /** * Store multiple memories in batch (max 100). * * @param string $agent_id Agent identifier. * @param array $memories Array of memory items. Each item: { content, type, tags, title }. * @return bool True on success. */ public function batch_remember($agent_id, $memories) { if (!$this->is_active() || empty($memories)) { return false; } $batch = []; foreach (array_slice($memories, 0, 100) as $item) { $entry = [ "content" => mb_substr($item["content"] ?? "", 0, 10000), "type" => $item["type"] ?? "context", "source" => "wp-agentic-writer", ]; if (!empty($item["title"])) { $entry["title"] = mb_substr($item["title"], 0, 100); } if (!empty($item["tags"])) { $entry["tags"] = $item["tags"]; } $batch[] = $entry; } $response = $this->post( "/api/v2/agents/{$agent_id}/batch-remember", ["memories" => $batch], $agent_id, ); if (is_wp_error($response)) { wpaw_debug_log("MEMANTO batch_remember failed", [ "agent_id" => $agent_id, "count" => count($batch), "error" => $response->get_error_message(), ]); return false; } return true; } /** * Semantic search / recall memories. * * @param string $agent_id Agent identifier. * @param string $query Search query. * @param array $type_filter Optional memory type filter. * @param int $limit Max results (default 10). * @param float $min_similarity Minimum similarity score 0-1. * @return array Recalled memories (empty on failure). */ public function recall( $agent_id, $query, $type_filter = [], $limit = 10, $min_similarity = 0.3, ) { if (!$this->is_active()) { return []; } $body = [ "query" => $query, "limit" => $limit, "min_similarity" => $min_similarity, ]; if (!empty($type_filter)) { $body["type"] = $type_filter; } $response = $this->post( "/api/v2/agents/{$agent_id}/recall", $body, $agent_id, ); if (is_wp_error($response)) { wpaw_debug_log("MEMANTO recall failed", [ "agent_id" => $agent_id, "query" => substr($query, 0, 100), "error" => $response->get_error_message(), ]); return []; } return is_array($response) ? $response : []; } /** * Recall most recent memories. * * @param string $agent_id Agent identifier. * @param int $limit Max results. * @param array $type_filter Optional memory type filter. * @return array Recent memories (empty on failure). */ public function recall_recent($agent_id, $limit = 10, $type_filter = []) { if (!$this->is_active()) { return []; } $body = ["limit" => $limit]; if (!empty($type_filter)) { $body["type"] = $type_filter; } $response = $this->post( "/api/v2/agents/{$agent_id}/recall/recent", $body, $agent_id, ); if (is_wp_error($response)) { return []; } return is_array($response) ? $response : []; } // ========================================================================= // Agent ID Builders // ========================================================================= /** * Build user-level agent ID. * * @param int $user_id WordPress user ID. * @return string Agent ID like "wp-user-1". */ public function get_user_agent_id($user_id) { return "wp-user-" . absint($user_id); } /** * Build post-level agent ID. * * @param int $post_id WordPress post ID. * @return string Agent ID like "wp-post-42". */ public function get_post_agent_id($post_id) { return "wp-post-" . absint($post_id); } // ========================================================================= // HTTP Helpers (private) // ========================================================================= /** * Get the Moorcheh API key from settings. * * @return string */ private function get_moorcheh_key() { $settings = get_option("wp_agentic_writer_settings", []); return $settings["memanto_moorcheh_key"] ?? ""; } /** * GET request to MEMANTO API. * * @param string $path API path (e.g. "/health", "/api/v2/agents/{id}"). * @return array|WP_Error Decoded response or error. */ private function get($path) { $url = $this->base_url . $path; $response = wp_remote_get($url, [ "headers" => $this->get_headers(), "timeout" => 10, ]); return $this->parse_response($response); } /** * POST request to MEMANTO API. * * @param string $path API path. * @param array $body Request body. * @param string $agent_id Optional agent ID for session token injection. * @return array|WP_Error Decoded response or error. */ private function post($path, $body = [], $agent_id = "") { $url = $this->base_url . $path; $headers = $this->get_headers(); // Inject session token if we have an agent_id. if (!empty($agent_id)) { $token = $this->activate_session($agent_id); if ($token) { $headers["X-Session-Token"] = $token; } } $response = wp_remote_post($url, [ "headers" => $headers, "body" => wp_json_encode($body), "timeout" => 10, ]); // Handle expired token (401) — re-activate and retry once. if (!is_wp_error($response)) { $code = wp_remote_retrieve_response_code($response); if (401 === $code && !empty($agent_id)) { // Clear cached token and retry. delete_transient("wpaw_memanto_token_" . md5($agent_id)); $token = $this->activate_session($agent_id); if ($token) { $headers["X-Session-Token"] = $token; $response = wp_remote_post($url, [ "headers" => $headers, "body" => wp_json_encode($body), "timeout" => 10, ]); } } } return $this->parse_response($response); } /** * Build common headers for MEMANTO API requests. * * @return array Headers. */ private function get_headers() { return [ "Content-Type" => "application/json", "Accept" => "application/json", "X-API-Key" => $this->get_moorcheh_key(), ]; } /** * Parse HTTP response from MEMANTO API. * * @param array|WP_Error $response wp_remote response. * @return array|WP_Error Decoded body or error. */ private function parse_response($response) { if (is_wp_error($response)) { return new WP_Error( "memanto_connection_error", $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); if (401 === $code) { // Let caller handle re-auth. return new WP_Error( "memanto_unauthorized", "Session token expired", ); } if ($code >= 400) { wpaw_debug_log("MEMANTO API error ({$code})", $body); return new WP_Error( "memanto_api_error", sprintf( "MEMANTO API error (%d): %s", $code, substr($body, 0, 200), ), ); } $decoded = json_decode($body, true); return is_array($decoded) ? $decoded : []; } }