The tasks build the agent loop on M2a's client: channel messages and the usage record in proto, four config tables, the tool port and registry with find_tool and call_tool, the baseline and replay, the session store, the turn loop with its limits and the append-only property test, the channel server, loopd serve, bxctl chat, and the device checks including a four-turn conversation with a restart. Checked against a private reference implementation: the gate passes after every task in order, the new suites pass under CPU load, and the reference passes make verify-device on straylight with no cache loss. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.6 KiB
M2b task 04: the baseline
Branch: m2b (run git switch m2b; git status --short must be empty, otherwise stop)
Commit subject: Add the baseline and the log replay function
Goal
The baseline is the fixed prefix of every request in an epoch: the system prompt, memory/core.md
if it exists, and the core tool schemas. This task assembles it, writes and reads it as JSON, hashes
it, and turns a session log back into messages.
Context
From the brief's inference contract: "The request for turn N+1 is a strict extension of the request
for turn N. Nothing volatile goes anywhere but the newest message." The baseline is snapshotted per
session so that an edit to system.md changes nothing for a running session. The M0 measurements
show why: any change to an earlier byte re-reads the whole prompt.
Files
- Copy:
crates/loopd/tests/baseline.rs,crates/loopd/tests/support/mod.rs(replaces the old one; it gains aHomehelper and a scripted tool port) - Create:
crates/loopd/src/baseline.rs - Modify:
crates/loopd/src/lib.rs,docs/implementer-log.md
Interfaces
// our format: deny_unknown_fields; derives Debug, Clone, PartialEq, Serialize, Deserialize
pub struct Baseline { pub system: String, pub tools: Vec<ToolSchema> }
pub enum BaselineError { Read(PathBuf, std::io::Error), Parse(PathBuf, serde_json::Error), Hash } // Display names the file; std::error::Error
impl Baseline {
pub fn assemble(cfg: &Config, registry: &Registry) -> Result<Baseline, BaselineError>;
pub fn to_json(&self) -> Result<String, serde_json::Error>;
pub fn from_json(text: &str) -> Result<Baseline, serde_json::Error>;
pub fn load(path: &Path) -> Result<Baseline, BaselineError>;
pub fn hash(&self) -> Result<Hash32, BaselineError>; // proto::sha256 of to_json()
}
/// The message array for a request: the system message, then the log replayed.
pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec<ChatMessage>;
Rules:
assemble: readcfg.baseline.system, trim trailing whitespace. If<cfg.paths.home>/memory/core.mdis a file whose trimmed content is not empty, append a blank line and its trimmed content.toolsisregistry.core_schemas().messages:UsergivesChatMessage::User;AssistantgivesChatMessage::Assistantwith its three fields unchanged;ToolResultgivesChatMessage::Tool { tool_call_id, content };SessionStart,Usage,CacheLossandEpochEndgive nothing. Write the match with every variant named, so that a new variant is a compile error and not a silent skip.
Steps
- 1. Copy.
git switch m2b
cp docs/plans/M2b/files/crates/loopd/tests/baseline.rs crates/loopd/tests/
cp docs/plans/M2b/files/crates/loopd/tests/support/mod.rs crates/loopd/tests/support/
- 2. See the test fail.
cargo test -p loopd --test baseline. Expected: it does not compile. - 3. Write
baseline.rsand addpub mod baseline;tolib.rs. Runcargo fmt --all. - 4. See the tests pass.
cargo test -p loopd. Expected:baseline6 passed, and every earlier file still passes with the newsupport/mod.rs. - 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 baselinereports 6 passed;make gateprintsgate: ok.cmp crates/loopd/tests/support/mod.rs docs/plans/M2b/files/crates/loopd/tests/support/mod.rsprints nothing.
Stop and report if
replay_of_a_prefix_is_a_prefixfails:messagesis reordering or rewriting something.