Add the baseline and the log replay function
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
//! The baseline: the fixed prefix of every request in an epoch — the system prompt, core memory
|
||||||
|
//! and core tool schemas. It is snapshotted per session so an edit to `system.md` changes nothing
|
||||||
|
//! for a running session, and `messages` turns a session log back into the request's messages.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::llama::{ChatMessage, ToolSchema};
|
||||||
|
use crate::tools::Registry;
|
||||||
|
use proto::{Hash32, LogRecord, sha256};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// The fixed prefix of a request, and the errors assembling or reading it.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Baseline {
|
||||||
|
pub system: String,
|
||||||
|
pub tools: Vec<ToolSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BaselineError {
|
||||||
|
Read(PathBuf, std::io::Error),
|
||||||
|
Parse(PathBuf, serde_json::Error),
|
||||||
|
Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BaselineError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
BaselineError::Read(path, err) => write!(f, "{}: {err}", path.display()),
|
||||||
|
BaselineError::Parse(path, err) => write!(f, "{}: {err}", path.display()),
|
||||||
|
BaselineError::Hash => write!(f, "hashing the baseline failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for BaselineError {}
|
||||||
|
|
||||||
|
impl Baseline {
|
||||||
|
/// The system prompt trimmed, plus core memory if it has content; the core tool schemas.
|
||||||
|
pub fn assemble(cfg: &Config, registry: &Registry) -> Result<Baseline, BaselineError> {
|
||||||
|
let system = std::fs::read_to_string(&cfg.baseline.system)
|
||||||
|
.map_err(|e| BaselineError::Read(cfg.baseline.system.clone(), e))?;
|
||||||
|
let mut system = system.trim_end().to_string();
|
||||||
|
|
||||||
|
let core = cfg.paths.home.join("memory/core.md");
|
||||||
|
if let Ok(text) = std::fs::read_to_string(&core) {
|
||||||
|
let trimmed = text.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
system.push_str("\n\n");
|
||||||
|
system.push_str(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Baseline {
|
||||||
|
system,
|
||||||
|
tools: registry.core_schemas(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The canonical JSON: key order is fixed, so the hash is stable.
|
||||||
|
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
||||||
|
serde_json::to_string(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_json(text: &str) -> Result<Baseline, serde_json::Error> {
|
||||||
|
serde_json::from_str(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(path: &Path) -> Result<Baseline, BaselineError> {
|
||||||
|
let text = std::fs::read_to_string(path)
|
||||||
|
.map_err(|e| BaselineError::Read(path.to_path_buf(), e))?;
|
||||||
|
serde_json::from_str(&text).map_err(|e| BaselineError::Parse(path.to_path_buf(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sha256 of the baseline's own JSON, as written.
|
||||||
|
pub fn hash(&self) -> Result<Hash32, BaselineError> {
|
||||||
|
let json = self.to_json().map_err(|_| BaselineError::Hash)?;
|
||||||
|
sha256(json.as_bytes()).map_err(|_| BaselineError::Hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system message, then each log record replayed in order. Every record variant is named, so a
|
||||||
|
/// new one is a compile error rather than a silent skip.
|
||||||
|
pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec<ChatMessage> {
|
||||||
|
let mut out = vec![ChatMessage::System {
|
||||||
|
content: baseline.system.clone(),
|
||||||
|
}];
|
||||||
|
for record in records {
|
||||||
|
match record {
|
||||||
|
LogRecord::SessionStart { .. } => {}
|
||||||
|
LogRecord::User { content, .. } => out.push(ChatMessage::User {
|
||||||
|
content: content.clone(),
|
||||||
|
}),
|
||||||
|
LogRecord::Assistant {
|
||||||
|
content,
|
||||||
|
reasoning_content,
|
||||||
|
tool_calls,
|
||||||
|
..
|
||||||
|
} => out.push(ChatMessage::Assistant {
|
||||||
|
content: content.clone(),
|
||||||
|
reasoning_content: reasoning_content.clone(),
|
||||||
|
tool_calls: tool_calls.clone(),
|
||||||
|
}),
|
||||||
|
LogRecord::ToolResult {
|
||||||
|
tool_call_id,
|
||||||
|
content,
|
||||||
|
..
|
||||||
|
} => out.push(ChatMessage::Tool {
|
||||||
|
tool_call_id: tool_call_id.clone(),
|
||||||
|
content: content.clone(),
|
||||||
|
}),
|
||||||
|
LogRecord::Usage { .. } => {}
|
||||||
|
LogRecord::CacheLoss { .. } => {}
|
||||||
|
LogRecord::EpochEnd { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! The agent loop: sessions, prompt assembly and memory. It holds no authority.
|
//! The agent loop: sessions, prompt assembly and memory. It holds no authority.
|
||||||
|
|
||||||
|
pub mod baseline;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod llama;
|
pub mod llama;
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
//! Tests for the baseline and the replay function. Do not edit.
|
||||||
|
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
use loopd::baseline::{Baseline, messages};
|
||||||
|
use loopd::llama::ChatMessage;
|
||||||
|
use loopd::tools::Registry;
|
||||||
|
use proto::{CallId, DataClass, Epoch, Hash32, LogRecord, SessionId, Timestamp, ToolCall};
|
||||||
|
use std::path::Path;
|
||||||
|
use support::Home;
|
||||||
|
|
||||||
|
fn ts() -> Timestamp {
|
||||||
|
Timestamp::parse("2026-09-18T08:00:00.000Z").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assembles_system_prompt_and_core_schemas() {
|
||||||
|
let home = Home::new();
|
||||||
|
let cfg = home.config(Path::new("/tmp/unused.sock"));
|
||||||
|
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
b.system, "You are Boxmaker, a test agent.",
|
||||||
|
"trailing newline trimmed"
|
||||||
|
);
|
||||||
|
let names: Vec<&str> = b.tools.iter().map(|t| t.name.as_str()).collect();
|
||||||
|
assert_eq!(
|
||||||
|
names,
|
||||||
|
["clock", "find_tool", "call_tool"],
|
||||||
|
"core tools, then the two meta-tools; not echo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn core_memory_is_appended_when_present_and_only_then() {
|
||||||
|
let home = Home::new();
|
||||||
|
let cfg = home.config(Path::new("/tmp/unused.sock"));
|
||||||
|
home.write("memory/core.md", "The owner likes cork.\n\n");
|
||||||
|
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
b.system,
|
||||||
|
"You are Boxmaker, a test agent.\n\nThe owner likes cork."
|
||||||
|
);
|
||||||
|
home.write("memory/core.md", " \n");
|
||||||
|
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
b.system, "You are Boxmaker, a test agent.",
|
||||||
|
"an empty core file adds nothing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_missing_system_prompt_is_an_error_naming_the_file() {
|
||||||
|
let home = Home::new();
|
||||||
|
let mut cfg = home.config(Path::new("/tmp/unused.sock"));
|
||||||
|
cfg.baseline.system = home.dir.join("nope.md");
|
||||||
|
let e = Baseline::assemble(&cfg, &Registry::m2b()).unwrap_err();
|
||||||
|
assert!(e.to_string().contains("nope.md"), "{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_round_trip_and_hash() {
|
||||||
|
let home = Home::new();
|
||||||
|
let cfg = home.config(Path::new("/tmp/unused.sock"));
|
||||||
|
let b = Baseline::assemble(&cfg, &Registry::m2b()).unwrap();
|
||||||
|
let text = b.to_json().unwrap();
|
||||||
|
assert_eq!(Baseline::from_json(&text).unwrap(), b);
|
||||||
|
assert_eq!(
|
||||||
|
b.hash().unwrap(),
|
||||||
|
proto::sha256(text.as_bytes()).unwrap(),
|
||||||
|
"the hash is of the JSON as written"
|
||||||
|
);
|
||||||
|
assert_ne!(b.hash().unwrap(), Hash32::ZERO);
|
||||||
|
let mut other = b.clone();
|
||||||
|
other.system.push('!');
|
||||||
|
assert_ne!(other.hash().unwrap(), b.hash().unwrap());
|
||||||
|
assert!(
|
||||||
|
Baseline::from_json(&text.replacen("\"system\"", "\"zz\":1,\"system\"", 1)).is_err(),
|
||||||
|
"unknown keys are rejected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replay_keeps_only_messages_in_order_and_unchanged() {
|
||||||
|
let b = Baseline {
|
||||||
|
system: "sys".to_string(),
|
||||||
|
tools: vec![],
|
||||||
|
};
|
||||||
|
let call = ToolCall {
|
||||||
|
id: "c1".to_string(),
|
||||||
|
name: "clock".to_string(),
|
||||||
|
arguments: "{}".to_string(),
|
||||||
|
};
|
||||||
|
let records = vec![
|
||||||
|
LogRecord::SessionStart {
|
||||||
|
time: ts(),
|
||||||
|
session: SessionId::new("s").unwrap(),
|
||||||
|
epoch: Epoch(0),
|
||||||
|
slot: 0,
|
||||||
|
baseline: Hash32::ZERO,
|
||||||
|
},
|
||||||
|
LogRecord::User {
|
||||||
|
time: ts(),
|
||||||
|
content: "hi".to_string(),
|
||||||
|
},
|
||||||
|
LogRecord::Assistant {
|
||||||
|
time: ts(),
|
||||||
|
content: None,
|
||||||
|
reasoning_content: Some("think".to_string()),
|
||||||
|
tool_calls: vec![call.clone()],
|
||||||
|
},
|
||||||
|
LogRecord::Usage {
|
||||||
|
time: ts(),
|
||||||
|
cache_n: 1,
|
||||||
|
prompt_n: 2,
|
||||||
|
predicted_n: 3,
|
||||||
|
reasoning_tokens: 1,
|
||||||
|
thinking_capped: false,
|
||||||
|
},
|
||||||
|
LogRecord::ToolResult {
|
||||||
|
time: ts(),
|
||||||
|
call: CallId(1),
|
||||||
|
tool_call_id: "c1".to_string(),
|
||||||
|
content: "noon".to_string(),
|
||||||
|
class: DataClass::Public,
|
||||||
|
untrusted: false,
|
||||||
|
truncated: false,
|
||||||
|
},
|
||||||
|
LogRecord::CacheLoss {
|
||||||
|
time: ts(),
|
||||||
|
expected: 10,
|
||||||
|
got: 0,
|
||||||
|
},
|
||||||
|
LogRecord::Assistant {
|
||||||
|
time: ts(),
|
||||||
|
content: Some("It is noon.".to_string()),
|
||||||
|
reasoning_content: None,
|
||||||
|
tool_calls: vec![],
|
||||||
|
},
|
||||||
|
LogRecord::EpochEnd {
|
||||||
|
time: ts(),
|
||||||
|
next: Epoch(1),
|
||||||
|
summary: "x".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let want = vec![
|
||||||
|
ChatMessage::System {
|
||||||
|
content: "sys".to_string(),
|
||||||
|
},
|
||||||
|
ChatMessage::User {
|
||||||
|
content: "hi".to_string(),
|
||||||
|
},
|
||||||
|
ChatMessage::Assistant {
|
||||||
|
content: None,
|
||||||
|
reasoning_content: Some("think".to_string()),
|
||||||
|
tool_calls: vec![call],
|
||||||
|
},
|
||||||
|
ChatMessage::Tool {
|
||||||
|
tool_call_id: "c1".to_string(),
|
||||||
|
content: "noon".to_string(),
|
||||||
|
},
|
||||||
|
ChatMessage::Assistant {
|
||||||
|
content: Some("It is noon.".to_string()),
|
||||||
|
reasoning_content: None,
|
||||||
|
tool_calls: vec![],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert_eq!(messages(&b, &records), want);
|
||||||
|
assert_eq!(
|
||||||
|
messages(&b, &[]),
|
||||||
|
vec![ChatMessage::System {
|
||||||
|
content: "sys".to_string()
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaying a prefix of the log gives a prefix of the messages: the function never reorders
|
||||||
|
/// or rewrites. Checked for every prefix of a generated log.
|
||||||
|
#[test]
|
||||||
|
fn replay_of_a_prefix_is_a_prefix() {
|
||||||
|
let b = Baseline {
|
||||||
|
system: "sys".to_string(),
|
||||||
|
tools: vec![],
|
||||||
|
};
|
||||||
|
let mut records = Vec::new();
|
||||||
|
let mut seed: u64 = 0x9e3779b97f4a7c15;
|
||||||
|
let mut next = || {
|
||||||
|
seed ^= seed << 13;
|
||||||
|
seed ^= seed >> 7;
|
||||||
|
seed ^= seed << 17;
|
||||||
|
seed
|
||||||
|
};
|
||||||
|
for i in 0..200u64 {
|
||||||
|
let r = match next() % 5 {
|
||||||
|
0 => LogRecord::User {
|
||||||
|
time: ts(),
|
||||||
|
content: format!("u{i}"),
|
||||||
|
},
|
||||||
|
1 => LogRecord::Assistant {
|
||||||
|
time: ts(),
|
||||||
|
content: Some(format!("a{i}")),
|
||||||
|
reasoning_content: None,
|
||||||
|
tool_calls: vec![],
|
||||||
|
},
|
||||||
|
2 => LogRecord::ToolResult {
|
||||||
|
time: ts(),
|
||||||
|
call: CallId(i),
|
||||||
|
tool_call_id: format!("t{i}"),
|
||||||
|
content: "x".repeat((next() % 50) as usize),
|
||||||
|
class: DataClass::Private,
|
||||||
|
untrusted: true,
|
||||||
|
truncated: false,
|
||||||
|
},
|
||||||
|
3 => LogRecord::Usage {
|
||||||
|
time: ts(),
|
||||||
|
cache_n: i,
|
||||||
|
prompt_n: 1,
|
||||||
|
predicted_n: 1,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
thinking_capped: false,
|
||||||
|
},
|
||||||
|
_ => LogRecord::CacheLoss {
|
||||||
|
time: ts(),
|
||||||
|
expected: i,
|
||||||
|
got: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
records.push(r);
|
||||||
|
}
|
||||||
|
let whole = messages(&b, &records);
|
||||||
|
for n in 0..=records.len() {
|
||||||
|
let part = messages(&b, &records[..n]);
|
||||||
|
assert_eq!(whole[..part.len()], part[..], "prefix of {n} records");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,90 @@ retry_window_ms = 5000
|
|||||||
loopd::config::Config::parse(&text).unwrap()
|
loopd::config::Config::parse(&text).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A temporary `BOXMAKER_HOME` with a `system.md` beside a `config.toml`, for session tests.
|
||||||
|
pub struct Home {
|
||||||
|
pub dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Home {
|
||||||
|
pub fn new() -> Home {
|
||||||
|
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let dir = std::env::temp_dir().join(format!("loopd-home-{}-{n}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
std::fs::write(dir.join("system.md"), "You are Boxmaker, a test agent.\n").unwrap();
|
||||||
|
Home { dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A config for `socket` whose home, system prompt and channel socket are all under this
|
||||||
|
/// directory, with the fast test limits.
|
||||||
|
pub fn config(&self, socket: &Path) -> loopd::config::Config {
|
||||||
|
let mut cfg = test_config(socket);
|
||||||
|
cfg.paths.home = self.dir.clone();
|
||||||
|
cfg.baseline.system = self.dir.join("system.md");
|
||||||
|
cfg.channel.socket = self.dir.join("loop.sock");
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self, relative: &str, text: &str) {
|
||||||
|
let path = self.dir.join(relative);
|
||||||
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
std::fs::write(path, text).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self, relative: &str) -> String {
|
||||||
|
std::fs::read_to_string(self.dir.join(relative)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The records of a session's log, epoch 0.
|
||||||
|
pub fn records(&self, session: &str) -> Vec<proto::LogRecord> {
|
||||||
|
let text = self.read(&format!("sessions/{session}/0.jsonl"));
|
||||||
|
text.lines()
|
||||||
|
.map(|l| serde_json::from_str(l).unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tool port that answers from a script and records what it was asked.
|
||||||
|
pub struct ScriptedPort {
|
||||||
|
replies: Mutex<VecDeque<proto::ToolResponse>>,
|
||||||
|
calls: Mutex<Vec<proto::ToolRequest>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptedPort {
|
||||||
|
/// Replies are given in order; when they run out, every call gets `fallback`.
|
||||||
|
pub fn new(replies: Vec<proto::ToolResponse>) -> ScriptedPort {
|
||||||
|
ScriptedPort {
|
||||||
|
replies: Mutex::new(replies.into()),
|
||||||
|
calls: Mutex::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calls(&self) -> Vec<proto::ToolRequest> {
|
||||||
|
self.calls.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ok_result(content: &str) -> proto::ToolResponse {
|
||||||
|
proto::ToolResponse::Result {
|
||||||
|
content: content.to_string(),
|
||||||
|
class: proto::DataClass::Private,
|
||||||
|
untrusted: true,
|
||||||
|
truncated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl loopd::tools::ToolPort for ScriptedPort {
|
||||||
|
fn call(&self, request: &proto::ToolRequest) -> proto::ToolResponse {
|
||||||
|
self.calls.lock().unwrap().push(request.clone());
|
||||||
|
self.replies
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.pop_front()
|
||||||
|
.unwrap_or_else(|| ok_result("scripted"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What an independent script derived from a recorded stream: `tests/fixtures/expected/`.
|
/// What an independent script derived from a recorded stream: `tests/fixtures/expected/`.
|
||||||
pub fn expected(name: &str) -> serde_json::Value {
|
pub fn expected(name: &str) -> serde_json::Value {
|
||||||
serde_json::from_slice(&fixture_bytes("expected", &format!("{name}.json"))).unwrap()
|
serde_json::from_slice(&fixture_bytes("expected", &format!("{name}.json"))).unwrap()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ reviewer adds findings under "Reviews" once per milestone.
|
|||||||
| M2b/01-proto-channel-types | 2026-09-18 | done | 1 | pass | none | Added `Usage` struct and a `Usage` variant (between ToolResult and CacheLoss) in log.rs, and `Turn`, `TurnEvent`, `TurnDone` plus six `ErrorCode` variants (SessionFull..Inference) and three `Message` variants (after Error) in wire.rs; re-exported Usage, Turn, TurnEvent, TurnDone from lib.rs. All four new types carry `deny_unknown_fields`; field order matches the byte-exact fixtures (attempt/after_ms/error, name/class/truncated). 55 proto tests pass (turn_wire 5, strict 5, wire 9, ids 12, frame 13, grant 4, hash 4, records 3) and `make gate` prints `gate: ok`; the old fixtures stay byte-identical. One duplicate block of the three wire types left by an interrupted edit had to be removed mid-task. | Ornith-1.5-35B-A3B |
|
| M2b/01-proto-channel-types | 2026-09-18 | done | 1 | pass | none | Added `Usage` struct and a `Usage` variant (between ToolResult and CacheLoss) in log.rs, and `Turn`, `TurnEvent`, `TurnDone` plus six `ErrorCode` variants (SessionFull..Inference) and three `Message` variants (after Error) in wire.rs; re-exported Usage, Turn, TurnEvent, TurnDone from lib.rs. All four new types carry `deny_unknown_fields`; field order matches the byte-exact fixtures (attempt/after_ms/error, name/class/truncated). 55 proto tests pass (turn_wire 5, strict 5, wire 9, ids 12, frame 13, grant 4, hash 4, records 3) and `make gate` prints `gate: ok`; the old fixtures stay byte-identical. One duplicate block of the three wire types left by an interrupted edit had to be removed mid-task. | Ornith-1.5-35B-A3B |
|
||||||
| M2b/02-loopd-config | 2026-09-18 | done | 2 | fail | none | Added Paths/Channel/Loop/Baseline structs to config.rs with `#[serde(deny_unknown_fields, default)]` and Debug/Clone/PartialEq/Eq/Deserialize (Default derived for Channel, hand-written for the other three: home is $BOXMAKER_HOME else /var/lib/boxmaker, loop is 8/true/16384, baseline system is system.md); Config gained the four `#[serde(default)]` fields and `channel_socket()` fills the default `<home>/run/loop/loop.sock` when the socket is empty. `load` joins `baseline.system` to the config file's directory via `parent.join` (which replaces an already-absolute path); `parse` leaves it. 9 config tests pass, `deny_unknown_fields` count is 10. Two clippy fixes on the first (failing) gate run: the nested if in `load` collapsed by relying on Path::join replacing absolute paths instead of a 1.98 let-chain, and `Path::is_empty` (stable 1.98) replaced with `as_os_str().is_empty()`. | ? |
|
| M2b/02-loopd-config | 2026-09-18 | done | 2 | fail | none | Added Paths/Channel/Loop/Baseline structs to config.rs with `#[serde(deny_unknown_fields, default)]` and Debug/Clone/PartialEq/Eq/Deserialize (Default derived for Channel, hand-written for the other three: home is $BOXMAKER_HOME else /var/lib/boxmaker, loop is 8/true/16384, baseline system is system.md); Config gained the four `#[serde(default)]` fields and `channel_socket()` fills the default `<home>/run/loop/loop.sock` when the socket is empty. `load` joins `baseline.system` to the config file's directory via `parent.join` (which replaces an already-absolute path); `parse` leaves it. 9 config tests pass, `deny_unknown_fields` count is 10. Two clippy fixes on the first (failing) gate run: the nested if in `load` collapsed by relying on Path::join replacing absolute paths instead of a 1.98 let-chain, and `Path::is_empty` (stable 1.98) replaced with `as_os_str().is_empty()`. | ? |
|
||||||
| M2b/03-loopd-tools | 2026-09-18 | done | 1 | pass | none | Added `pub mod tools;` to lib.rs and `serde::Serialize`/`serde::Deserialize`/`deny_unknown_fields` to ToolSchema; wrote crates/loopd/src/tools.rs with ToolPort, Entry, Registry (m2b/config), core_schemas/get/find, FIND_TOOL/CALL_TOOL constants, clock_schema/echo_schema, Dispatch with dispatch (find_tool/call_tool/local dispatch rows), cap_result via floor_char_boundary, and FakeTools recording calls and answering clock/echo/denying others with `unwrap_or_else(|p| p.into_inner())` on Mutex::lock. 7 tools tests pass, `make gate` prints `gate: ok`, no `unwrap()` in tools.rs. | ? |
|
| M2b/03-loopd-tools | 2026-09-18 | done | 1 | pass | none | Added `pub mod tools;` to lib.rs and `serde::Serialize`/`serde::Deserialize`/`deny_unknown_fields` to ToolSchema; wrote crates/loopd/src/tools.rs with ToolPort, Entry, Registry (m2b/config), core_schemas/get/find, FIND_TOOL/CALL_TOOL constants, clock_schema/echo_schema, Dispatch with dispatch (find_tool/call_tool/local dispatch rows), cap_result via floor_char_boundary, and FakeTools recording calls and answering clock/echo/denying others with `unwrap_or_else(|p| p.into_inner())` on Mutex::lock. 7 tools tests pass, `make gate` prints `gate: ok`, no `unwrap()` in tools.rs. | ? |
|
||||||
|
| M2b/04-loopd-baseline | 2026-09-18 | done | 2 | fail | none | Wrote crates/loopd/src/baseline.rs: `Baseline` (system prompt + core tool schemas, `deny_unknown_fields`), `BaselineError` (Read/Parse name the file, plus Hash) with Display/std::error::Error, `assemble` (system prompt trimmed of trailing whitespace, core memory appended with a blank line when its trimmed content is non-empty), `to_json`/`from_json`, `load`, and `hash` (sha256 of the canonical JSON). `messages` prepends the system message and replays every `LogRecord` variant explicitly named, so a new one is a compile error. The `\n\n` separator between system prompt and core memory had to be two newlines (a blank line), not one. All 6 baseline tests pass and all loopd tests pass with the new support module; first gate run failed on a rustfmt import-order diff, fixed with `cargo fmt`. | ? |
|
||||||
|
|
||||||
|
|
||||||
## Reviews
|
## Reviews
|
||||||
|
|||||||
Reference in New Issue
Block a user