Add M2a plan: thirteen tasks, tests, fake server and recordings
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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# 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`:
|
||||
|
||||
```rust
|
||||
#[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:
|
||||
|
||||
1. 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.
|
||||
2. A line longer than `max_line` bytes is `LineTooLong`. 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.
|
||||
3. When the reader ends (`read` returns 0): with nothing buffered, return `Ok(None)`, and keep
|
||||
returning it if asked again. With a partial line buffered: `Truncated`.
|
||||
4. Reader errors: `is_timeout` gives `Timeout`; `UnexpectedEof` gives `Truncated` (the HTTP body
|
||||
reader reports a cut stream that way); `Interrupted` is retried; anything else is `Io`.
|
||||
5. A line that is not valid UTF-8 is `NotUtf8`.
|
||||
|
||||
## Steps
|
||||
|
||||
- [ ] **1. Copy.** `git switch m2a`, then
|
||||
`cp 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 add `pub mod sse;` to `lib.rs`. Run `cargo 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 sse` reports 7 passed; `make gate` prints `gate: ok`.
|
||||
|
||||
## Stop and report if
|
||||
|
||||
- `recorded_streams_have_the_expected_shape` fails although `cargo test -p loopd --test http`
|
||||
passes and the other six tests here pass.
|
||||
Reference in New Issue
Block a user