The tasks build the inference path: emsha-backed SHA-256, inferproxy, config, a hand-written HTTP and SSE client, request building, delta assembly, the chat state machine, the thinking cap, the slot gate with retry, the startup self-test and on-device verification. Everything the tasks copy in was checked against a private reference implementation: the gate passes after each task in order, the timing tests pass repeatedly under CPU load, and the reference passes the self-test and all four device checks on straylight. Expected results for the recorded streams were derived by a separate script. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.2 KiB
3.2 KiB
M2a task 05: the SSE reader
Branch: m2a (run git switch m2a; git status --short must be empty, otherwise stop)
Commit subject: Add a server-sent-events reader to loopd
Goal
Turn a streamed body into a sequence of data: payloads. llama-server streams a completion as
server-sent events: one JSON object per data: line, and data: [DONE] at the end.
Context
data: {"choices":[…]}\n
\n
: a comment line\n
data: {"choices":[…]}\r\n
\r\n
data: [DONE]\n
\n
Lines end in \n or \r\n. Blank lines, comment lines (starting with :) and any field other
than data (event:, id:, …) carry nothing for us and are skipped. After data: one leading
space is dropped if present, and only one.
Files
- Copy:
crates/loopd/tests/sse.rs - Create:
crates/loopd/src/sse.rs - Modify:
crates/loopd/src/lib.rs,docs/implementer-log.md
Interfaces
Consumes: loopd::http::is_timeout.
Produces, in crates/loopd/src/sse.rs:
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SseItem { Data(String), Done } // Data: the text after "data:", without the line ending
#[derive(Debug)]
pub enum SseError { Io(std::io::Error), Timeout, Truncated, LineTooLong, NotUtf8 } // Display, Error
pub struct Events<R> { /* private */ }
impl<R: std::io::Read> Events<R> {
pub fn new(reader: R, max_line: usize) -> Self;
/// The next item, or None when the stream has ended cleanly between lines.
pub fn next_item(&mut self) -> Result<Option<SseItem>, SseError>;
}
Rules the tests check:
- The result must not depend on how the bytes arrive. A reader that gives one byte per call must produce the same items, including for text with multi-byte characters: decode a line as UTF-8 only once the whole line is there.
- A line longer than
max_linebytes isLineTooLong. This applies both when the line is still arriving with no newline in sight and when it arrives whole in a single read. Check the length in both places. - When the reader ends (
readreturns 0): with nothing buffered, returnOk(None), and keep returning it if asked again. With a partial line buffered:Truncated. - Reader errors:
is_timeoutgivesTimeout;UnexpectedEofgivesTruncated(the HTTP body reader reports a cut stream that way);Interruptedis retried; anything else isIo. - A line that is not valid UTF-8 is
NotUtf8.
Steps
- 1. Copy.
git switch m2a, thencp docs/plans/M2a/files/crates/loopd/tests/sse.rs crates/loopd/tests/ - 2. See the test fail.
cargo test -p loopd --test sse. Expected: it does not compile. - 3. Write
sse.rs, and addpub mod sse;tolib.rs. Runcargo fmt --all. - 4. See the test pass.
cargo test -p loopd --test sse. Expected:7 passed. The last test reads four recordings through the HTTP client from task 04. - 5. Run the gate.
make gate. Expected last line:gate: ok. - 6. Log and commit.
git add crates/loopd docs/implementer-log.md && git commit
Done when
cargo test -p loopd --test ssereports 7 passed;make gateprintsgate: ok.
Stop and report if
recorded_streams_have_the_expected_shapefails althoughcargo test -p loopd --test httppasses and the other six tests here pass.