You would typically write your main loop as a recursive function, which evaluates to a thread, then pass that thread once to Lwt_main.run. Here is a small example:
let () =
let rec echo_loop () =
let%lwt line = Lwt_io.(read_line stdin) in
if line = "exit" then
Lwt.return_unit
else
let%lwt () = Lwt_io.(write_line stdout line) in
echo_loop ()
in
Lwt_main.run (echo_loop ())
This can be compiled and run with:
ocamlfind opt -linkpkg -package lwt.unix -package lwt.ppx code.ml && ./a.out
In rough terms, this is what happens in the above code:
echo_loop () is applied in the argument of Lwt_main.run. This immediately begins evaluating Lwt_io.(read_line stdin), but the rest of the code (starting with the if expression) is put into a closure to be run once the read_line completes. echo_loop () then evaluates to this combination of an ongoing read_line operation followed by the closure.
Lwt_main.run forces your process to wait until all that completes. However, once the read_line completes, if the line is not exit, the closure triggers a write_line operation, followed by another closure, which calls echo_loop () recursively, which starts another read_line, and this can go on indefinitely.