Вы отделили логику анализа поступающей информации от логики ее буферизации, это хорошо. Буферизация и в самом деле может быть реализована более эффективно, так что может пригодиться возможность ее изменить. К тому же, тривиальные протоколы, работающие на уровне байтов, можно было бы реализовать без использования дополнительной памяти и работы по ее выделению.

Насчет того, что "Protocol.ReceiveData" звучит гораздо понятнее, чем "Engine.Do", то я с вами на 100% согласен. Но здесь есть и концептуальное различие. Дело в том, что я предусмотрел возможность сначала получить данные, а потом их обработать. Это с прицелом на те задачи, в которых обработка данных может занять больше времени, чем требуется на их получение. Здесь мы рискуем получить переполнение системного буфера, если не будем успевать считывать из него данные, пытаясь их сразу обработать. Поэтому Engine.Do вызывается постоянно, а не только когда получены новые данные (DataReceived). Это дает возможность вычерпывать данные из буфера в очень ленивом режиме, не боясь помешать другим задачам.

По-другому это, впрочем, можно реализовать, переложив ответственность за буферизацию на протокол. Тогда протокол нижнего уровня будет складывать данные в некий буфер, а совсем другой Services.Action будет из этого буфера брать данные и лениво обрабатывать. Тогда эту схему с использованием дополнительного Services.Action можно реализовывать только в тех случаях, когда она действительно нужна.

Еще Engine.Do периодически вызывается для того, чтобы определять таймаут приема. Например, нет смысла бесконечно ждать ответа, если устройство не подключено к последовательному порту. Но и это можно реализовать в виде отдельного Action.

В целом мне ваш вариант нравится больше, чем мой. По приведенному вами коду есть только пара замечаний по процедуре LineOnlyReceiver.ReceiveData (на всякий случай, если вы будете ее где-то применять):
1. вы используете параметр len в значении "индекс байта, следующего за последним". Чтобы она этому определению соответствовала, нужно перед LOOP сделать INC(len, beg), иначе ваш код будет корректно работать только для beg = 0;
2. ваш код без сбоев будет работать только при LEN(delimiter) = 1 или 2. Если delimiter = NIL (по умолчанию), будет trap при вызове LEN(delimiter). Чтобы процедура работала корректно для более длинных разделителей (3 символа и более), необходимо в конструкцию "IF k = dlen THEN" добавить следующее:
## Код:
```
ELSE
  DEC(i, k-1)
END
```

Представьте, что delimiter = "aab" и вы приняли строку "aaab". Без поправки вы не заметите здесь разделителя, хотя он есть. Дело в том, что при несовпадении символов во втором WHILE i будет равно 2, а чтобы найти разделитель нужно вернуться к i = 1.

Мне определенно нравится то, как вы структурировали программу. Переделаю у себя тоже. Заодно снимется зависимость CommAction от CommBuffer.

PS: ИМХО, строчные буквы i, j и l в некоторых шрифтах довольно трудно различить между собой, а l еще бывает похожа на 1. Я обычно использую i, c, n (i - index, c - count, n - number).