4.4 KiB
M1.4 — Gate-response parser
| Field | Value |
|---|---|
| Phase | M1 — Gated loop at L1 |
| Size | M — 1–3 days |
| Status | ⬜ Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | M1.3 |
Goal
Turn the model's tagged output into (U_t, M̂_t, E_t), strictly — because a
lenient parser silently fabricates gate decisions.
Facts (inlined — no spec read needed)
Expected response shape:
<think>...</think>
<check>yes|no</check>
<update>candidate memory, or the previous memory verbatim</update>
<next>continue|end</next>
Semantics, from the paper:
| tag | value | meaning |
|---|---|---|
<check> |
yes |
U_t = true — memory becomes M̂_t |
<check> |
no |
U_t = false — memory stays M_{t-1}, chunk discarded |
<next> |
continue |
E_t = false |
<next> |
end |
E_t = true |
Strict parsing is the design, matching the paper's r_format: it awards 1
only when every turn in the trajectory parses, 0 otherwise, "because we can not
infer whether the incorrect format is caused by the previous erroneous parsing".
So: exactly one of each tag, properly closed, <check> content exactly yes or
no after trimming, <next> exactly continue or end. Anything else is a
ParseError naming which tag failed and carrying the raw text.
A parse failure must not default. Defaulting U_t to false silently drops
evidence; defaulting to true pollutes memory. The loop (M1.5) decides the
retry policy; the parser only reports.
Reasoning models emit <think> natively, which can nest or repeat. Extract by
locating the last </think> before the first <check>, not by regex over the
whole body.
Steps
parse_gate_response(&str) -> Result<GateResponse, ParseError>inmem-core.GateResponse { think: String, update_gate: bool, candidate: String, exit_gate: bool }.- Reject duplicates of any tag, a missing tag, an unclosed tag, and any
<check>/<next>value outside the allowed set. ParseErrorvariants name the tag and include the raw response, truncated.- Trim surrounding whitespace inside tags; do not otherwise normalise — memory text is preserved verbatim.
- When
U_t = false, still capturecandidateso the log can show what the model would have written. The loop ignores it; the record is diagnostic.
Acceptance
- All four tags parse from a well-formed response.
- Every malformed shape errors, naming the failing tag.
- No input produces a default
U_torE_t.
Verify
Harness: table-driven over recorded real responses plus hand-built malformed
cases. Capture real ones with MEM_LLM_RECORD from M1.1.
Integration test — tests/it_gate_parser.rs:
a1_wellformed_yes_continue—U=true,E=false, candidate matches.a2_wellformed_no_end—U=false,E=true.a3_missing_check_errors— error namescheck.a4_duplicate_update_errors— two<update>blocks error.a5_bad_check_value_errors—<check>maybe</check>errors, message shows the value.a6_unclosed_tag_errors—<update>never closed.a7_nested_think— a response with<think>inside<think>still finds the right boundary.a8_no_defaults— property test over 1000 random mutations of a valid response: every result is either an exact parse or an error, never a silently-defaultedGateResponse.a9_real_responses— every recorded real response parses.
Command: cargo test -p mem-core gate_parser
False pass:
- A regex that finds the first
<check>and stops. It passes 1–2 and silently accepts duplicates, which is assertion 4's job. - Testing only hand-written responses. Real 3B output has whitespace, markdown fences and stray prose the author would not think to write — assertion 9 is the only one that sees it.
- Omitting assertion 8. A parser with
unwrap_or(false)anywhere passes every positive test and quietly halves the update rate.
Traps
- Defaulting on parse failure. The one thing that must not happen: a fabricated gate decision is indistinguishable from a real one in the log, and it poisons the M5 training data at the source.
- Normalising the candidate memory (collapsing whitespace, stripping markdown). Memory text is content; the hash and every downstream projection depend on it being verbatim.
Background: DESIGN.md — Architecture · paper §3.2.1 r_format