memoryを抽出する仕組みの実装

This commit is contained in:
2026-04-28 12:58:33 +09:00
parent 141b77b7e4
commit 3d04f793de
21 changed files with 1065 additions and 5 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ pub use fs_store::FsStore;
pub use session::{
SessionStartState, create_compacted_session, create_session, create_session_with_id,
ensure_head_or_fork, fork, fork_at, restore, save_cache_locked, save_cache_unlocked,
save_config_changed, save_delta, save_outcome, save_turn_end, save_usage,
save_config_changed, save_delta, save_extension, save_outcome, save_turn_end, save_usage,
};
pub use session_log::{
EntryHash, HashedEntry, LogEntry, Outcome, RestoredState, SessionOrigin, UsageRecord,
+25
View File
@@ -326,6 +326,31 @@ pub async fn save_cache_unlocked(
.await
}
/// Log an `Extension` entry — domain-tagged opaque payload.
///
/// session-store treats `payload` as an unstructured `serde_json::Value`.
/// Each domain is responsible for serializing into and folding out of it.
/// Use `RestoredState.extensions` to read entries back at restore time.
pub async fn save_extension(
store: &impl Store,
session_id: SessionId,
head_hash: &mut Option<EntryHash>,
domain: impl Into<String>,
payload: serde_json::Value,
) -> Result<(), StoreError> {
append_entry(
store,
session_id,
head_hash,
LogEntry::Extension {
ts: session_log::now_millis(),
domain: domain.into(),
payload,
},
)
.await
}
/// Log a `ConfigChanged` entry.
pub async fn save_config_changed(
store: &impl Store,
+82
View File
@@ -163,6 +163,20 @@ pub enum LogEntry {
cache_write_tokens: u64,
output_tokens: u64,
},
/// 汎用拡張点。ドメイン名で名前空間を切って任意 JSON を載せる。
/// session-store は payload を不透明扱いし、replay 時は
/// `RestoredState.extensions` に `(domain, payload)` を順に積むだけ。
/// 各ドメイン側が自前で fold して最新値を取り出す前提。
///
/// 想定用途: memory subsystem の Phase 1 処理境界 pointer 等、
/// 「session 寿命に縛りたいが session-store の型を汚したくない」
/// メタデータ。
Extension {
ts: u64,
domain: String,
payload: serde_json::Value,
},
}
/// Provenance reference to a parent session.
@@ -204,6 +218,9 @@ pub struct RestoredState {
/// `LogEntry::LlmUsage` を replay して時系列順に積まれる。
/// 任意位置のトークン数推定に使う。
pub usage_history: Vec<UsageRecord>,
/// `LogEntry::Extension` を replay 順に積んだもの。`(domain, payload)`。
/// session-store は domain を不透明扱いし、各ドメインが自前で fold する。
pub extensions: Vec<(String, serde_json::Value)>,
}
/// LLM リクエスト送信時点での占有量スナップショット。
@@ -234,6 +251,7 @@ pub fn collect_state(entries: &[HashedEntry]) -> RestoredState {
last_run_interrupted: false,
head_hash: None,
usage_history: Vec::new(),
extensions: Vec::new(),
};
for hashed in entries {
@@ -295,6 +313,11 @@ pub fn collect_state(entries: &[HashedEntry]) -> RestoredState {
output_tokens: *output_tokens,
});
}
LogEntry::Extension {
domain, payload, ..
} => {
state.extensions.push((domain.clone(), payload.clone()));
}
}
}
@@ -618,6 +641,65 @@ mod tests {
}
}
#[test]
fn replay_extension_collects_domain_payload_pairs() {
let entries = build_chain(&[
LogEntry::SessionStart {
ts: 1000,
system_prompt: None,
config: RequestConfig::default(),
history: vec![],
forked_from: None,
compacted_from: None,
},
LogEntry::Extension {
ts: 2000,
domain: "memory.extract".to_string(),
payload: serde_json::json!({ "processed_through_entry": 7 }),
},
LogEntry::Extension {
ts: 3000,
domain: "memory.extract".to_string(),
payload: serde_json::json!({ "processed_through_entry": 12 }),
},
LogEntry::Extension {
ts: 4000,
domain: "other.domain".to_string(),
payload: serde_json::json!({ "x": 1 }),
},
]);
let state = collect_state(&entries);
// 順序保持で全件積まれる。fold は呼び出し側の責務。
assert_eq!(state.extensions.len(), 3);
assert_eq!(state.extensions[0].0, "memory.extract");
assert_eq!(state.extensions[1].1["processed_through_entry"], 12);
assert_eq!(state.extensions[2].0, "other.domain");
}
#[test]
fn extension_entry_round_trip_via_json() {
let entry = LogEntry::Extension {
ts: 9999,
domain: "memory.extract".to_string(),
payload: serde_json::json!({ "a": 1, "b": "two" }),
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: LogEntry = serde_json::from_str(&json).unwrap();
match parsed {
LogEntry::Extension {
ts,
domain,
payload,
} => {
assert_eq!(ts, 9999);
assert_eq!(domain, "memory.extract");
assert_eq!(payload["a"], 1);
assert_eq!(payload["b"], "two");
}
other => panic!("expected Extension, got {:?}", other),
}
}
#[test]
fn hash_hex_round_trip() {
let entry = LogEntry::SessionStart {