persistence
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 永続化設計
|
||||
|
||||
## 概要
|
||||
|
||||
`llm-worker-persistence` クレートは、`llm-worker` の `Worker` セッション状態を
|
||||
JSONL append-only ログとして永続化する。ログを replay することで Worker 状態を完全に復元する。
|
||||
|
||||
## 設計方針
|
||||
|
||||
- **JSONL append-only ログ**: 1セッション = 1つの `.jsonl` ファイル。書き込みは末尾追記のみ。
|
||||
- **Pause/正常終了で構造に差異なし**: Worker の状態は Pause 時も正常終了時も同じ形
|
||||
(`history: Vec<Item>` + `turn_count` + `request_config`)。
|
||||
`resume()` は「ユーザー入力を追加せず `run_turn_loop()` に再入する」だけなので、
|
||||
復元に必要なのは history の中身であり、前回の終了理由ではない。
|
||||
`RunOutcome` の `Finished`/`Paused` 区分は監査用メタデータであり、replay の分岐には使わない。
|
||||
- **クレート分離**: `llm-worker` は永続化を知らない。`Session` ラッパーが外から Worker を包む。
|
||||
|
||||
## 命名規約
|
||||
|
||||
| 名前 | 用途 |
|
||||
|---|---|
|
||||
| **SessionLog / LogEntry** | 状態復元用の構造化された記録(永続化の本体) |
|
||||
| **EventTrace / TraceEntry** | デバッグ用の生ストリームイベント全録(オプション、デフォルト OFF) |
|
||||
|
||||
## クレート構成
|
||||
|
||||
```
|
||||
llm-worker-persistence → llm-worker → llm-worker-macros
|
||||
```
|
||||
|
||||
`llm-worker-persistence` は `llm-worker` に依存するが、逆方向の依存はない。
|
||||
|
||||
## ファイル配置
|
||||
|
||||
```
|
||||
{root}/{session_id}.jsonl -- セッションログ
|
||||
{root}/{session_id}.trace.jsonl -- イベントトレース(デバッグ時のみ)
|
||||
```
|
||||
|
||||
`SessionId` は UUID v7(`uuid` クレート)。タイムスタンプ埋め込みで辞書順 = 時系列順。
|
||||
|
||||
## LogEntry
|
||||
|
||||
各エントリは Worker の特定の状態変更に対応する:
|
||||
|
||||
| エントリ | Worker 上の対応箇所 | replay での効果 |
|
||||
|---|---|---|
|
||||
| `SessionStart` | セッション開始 / fork | system_prompt, config, history を初期化 |
|
||||
| `UserInput` | `worker.rs:229` | history に追加 |
|
||||
| `AssistantItems` | `worker.rs:1040-1041` | history に追加 |
|
||||
| `ToolResults` | `worker.rs:897-900, 1072-1076` | history に追加 |
|
||||
| `HookInjectedItems` | `worker.rs:1055` | history に追加 |
|
||||
| `TurnEnd` | `worker.rs:1033` | turn_count を更新 |
|
||||
| `CacheLocked` | `Worker::lock()` | locked_prefix_len を設定 |
|
||||
| `CacheUnlocked` | `Worker::unlock()` | locked_prefix_len を 0 に |
|
||||
| `RunOutcome` | `run()` / `resume()` 終了時 | interrupted フラグのみ(監査用) |
|
||||
| `ConfigChanged` | `set_*` メソッド群 | config を更新 |
|
||||
|
||||
## Session ラッパー
|
||||
|
||||
```rust
|
||||
pub struct Session<C: LlmClient, St: Store> {
|
||||
pub worker: Worker<C, Mutable>,
|
||||
store: St,
|
||||
session_id: SessionId,
|
||||
}
|
||||
```
|
||||
|
||||
- `Session::new()` — SessionStart を書き込み
|
||||
- `Session::run()` — Worker::run() の前後で history を比較、差分をログ記録
|
||||
- `Session::resume()` — 同上
|
||||
- `Session::restore()` — ログを replay して Worker を再構築
|
||||
- `Session::fork()` — 現在の history をシードにした新セッションを作成
|
||||
- `Session::fork_at()` — 任意のログ地点から分岐
|
||||
|
||||
## Store trait
|
||||
|
||||
```rust
|
||||
pub trait Store: Send + Sync {
|
||||
fn append(&self, id: SessionId, entry: &LogEntry) -> impl Future<...> + Send;
|
||||
fn read_all(&self, id: SessionId) -> impl Future<...> + Send;
|
||||
fn list_sessions(&self) -> impl Future<...> + Send;
|
||||
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> impl Future<...> + Send;
|
||||
fn exists(&self, id: SessionId) -> impl Future<...> + Send;
|
||||
fn append_trace(&self, id: SessionId, entry: &TraceEntry) -> impl Future<...> + Send;
|
||||
}
|
||||
```
|
||||
|
||||
初期実装は `FsStore`(ファイルシステム JSONL)。RPITIT 使用、`async_trait` 不要。
|
||||
@@ -0,0 +1,235 @@
|
||||
# 永続化データ構造の設計・実装プラン
|
||||
|
||||
## Context
|
||||
|
||||
INSOMNIA の `llm-worker` クレートは現在すべてのセッション状態をインメモリで保持しており、
|
||||
プロセス終了時に会話履歴やターン状態が失われる。
|
||||
Coding Agent として Pause/Resume・Fork をまたいだセッション継続を実現するため、
|
||||
永続化レイヤーを追加する。
|
||||
|
||||
**設計方針**: Codex CLI / Claude Code と同様の **JSONL append-only ログ** 方式を採用。
|
||||
セッションログを replay することで Worker 状態を完全に復元する。
|
||||
Pause/正常終了で永続化データの構造に差異を設けない。
|
||||
理由: Worker の状態は Pause 時も正常終了時も同じ形(`history: Vec<Item>` + `turn_count` + `request_config`)であり、
|
||||
APIレスポンスのデータ構造上、両者に本質的な違いがない。
|
||||
`resume()` は「ユーザー入力を追加せず `run_turn_loop()` に再入する」だけなので、
|
||||
復元に必要なのは history の中身であり、前回の終了理由ではない。
|
||||
`RunOutcome` の `Finished`/`Paused` 区分は監査・デバッグ用メタデータであり、replay ロジックの分岐には使わない。
|
||||
|
||||
**命名規約**:
|
||||
- **SessionLog / LogEntry** — 状態復元用の構造化された記録(永続化の本体)
|
||||
- **EventTrace / TraceEntry** — デバッグ用の生ストリームイベント全録(オプション)
|
||||
|
||||
## クレート構成
|
||||
|
||||
永続化は `llm-worker` とは別クレートに分離する。
|
||||
`llm-worker` は永続化を一切知らず、Session ラッパーが外から Worker を包む。
|
||||
|
||||
```
|
||||
crates/
|
||||
llm-worker/ ← 既存(LLM クライアント + Worker、変更なし以外は最小限)
|
||||
llm-worker-macros/ ← 既存(変更なし)
|
||||
llm-worker-persistence/ ← 新規クレート
|
||||
Cargo.toml
|
||||
src/
|
||||
lib.rs -- モジュールルート・re-exports・SessionId 型エイリアス
|
||||
session_log.rs -- LogEntry enum(JSONL 1行 = 1エントリ)
|
||||
event_trace.rs -- TraceEntry(デバッグ用生イベント記録)
|
||||
store.rs -- Store trait(バックエンド抽象)
|
||||
fs_store.rs -- JSONL ファイルシステム実装
|
||||
session.rs -- Session<C, St> ラッパー + replay/restore
|
||||
insomnia/ ← 将来のトップレベルアプリ
|
||||
|
||||
docs/persistence.md -- 設計ドキュメント(このプランの清書版)
|
||||
```
|
||||
|
||||
依存グラフ:
|
||||
```
|
||||
llm-worker-persistence → llm-worker → llm-worker-macros
|
||||
insomnia (将来) → llm-worker-persistence, llm-worker
|
||||
```
|
||||
|
||||
### llm-worker-persistence/Cargo.toml 依存
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
llm-worker = { path = "../llm-worker" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["fs", "io-util"] }
|
||||
uuid = { version = "1", features = ["v7", "serde"] }
|
||||
thiserror = "2"
|
||||
```
|
||||
|
||||
## 既存コードへの変更(llm-worker 側)
|
||||
|
||||
### 1. `RequestConfig` に Serialize/Deserialize 追加
|
||||
- **ファイル**: `crates/llm-worker/src/llm_client/types.rs:504`
|
||||
- `#[derive(Debug, Clone, Default)]` → `#[derive(Debug, Clone, Default, Serialize, Deserialize)]`
|
||||
|
||||
### 2. Worker に復元用セッター追加
|
||||
- **ファイル**: `crates/llm-worker/src/worker.rs`
|
||||
- `impl<C: LlmClient> Worker<C, Mutable>` ブロックに追加:
|
||||
- `pub fn set_turn_count(&mut self, count: usize)`
|
||||
- `pub fn set_last_run_interrupted(&mut self, interrupted: bool)`
|
||||
|
||||
### 3. ワークスペース Cargo.toml にメンバー追加
|
||||
- **ファイル**: `Cargo.toml`(ワークスペースルート)
|
||||
- `members` に `"crates/llm-worker-persistence"` を追加
|
||||
|
||||
## 新規コード: データ型
|
||||
|
||||
### LogEntry(session_log.rs)
|
||||
|
||||
状態復元に必要な構造化記録。JSONL 1行 = 1エントリ。
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum LogEntry {
|
||||
// セッション開始(ログ先頭、fork 時は history にシード状態を含む)
|
||||
SessionStart {
|
||||
ts: u64,
|
||||
system_prompt: Option<String>,
|
||||
config: RequestConfig,
|
||||
history: Vec<Item>,
|
||||
},
|
||||
|
||||
// ユーザー入力(worker.rs:229 に対応)
|
||||
UserInput { ts: u64, item: Item },
|
||||
|
||||
// アシスタント応答(worker.rs:1040-1041 に対応)
|
||||
AssistantItems { ts: u64, items: Vec<Item> },
|
||||
|
||||
// ツール実行結果(worker.rs:897-900, 1072-1076 に対応)
|
||||
ToolResults { ts: u64, items: Vec<Item> },
|
||||
|
||||
// Hook 注入 Items(worker.rs:1055 ContinueWithMessages に対応)
|
||||
HookInjectedItems { ts: u64, items: Vec<Item> },
|
||||
|
||||
// ターン境界
|
||||
TurnEnd { ts: u64, turn_count: usize },
|
||||
|
||||
// KV キャッシュロック/アンロック
|
||||
CacheLocked { ts: u64, locked_prefix_len: usize },
|
||||
CacheUnlocked { ts: u64 },
|
||||
|
||||
// run/resume の終了結果
|
||||
RunOutcome { ts: u64, outcome: Outcome, interrupted: bool },
|
||||
|
||||
// RequestConfig 変更
|
||||
ConfigChanged { ts: u64, config: RequestConfig },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Outcome { Finished, Paused, Error { message: String } }
|
||||
```
|
||||
|
||||
**Replay ロジック**: 全エントリ種別を走査し、`*Items` / `UserInput` → history に append、
|
||||
`TurnEnd` → turn_count 更新、`CacheLocked` → locked_prefix_len 設定。
|
||||
|
||||
### TraceEntry(event_trace.rs)
|
||||
|
||||
デバッグ用の生ストリームイベント全録。デフォルト OFF。
|
||||
セッションログとは別ファイル `{session_id}.trace.jsonl` に記録。
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TraceEntry {
|
||||
pub ts: u64,
|
||||
pub turn: usize,
|
||||
pub event: Event,
|
||||
}
|
||||
```
|
||||
|
||||
replay 対象外。状態復元には使わない。
|
||||
|
||||
### SessionId
|
||||
|
||||
`uuid` クレートの UUID v7 をそのまま使用。型エイリアスのみ。
|
||||
|
||||
```rust
|
||||
pub type SessionId = uuid::Uuid;
|
||||
|
||||
pub fn new_session_id() -> SessionId {
|
||||
uuid::Uuid::now_v7()
|
||||
}
|
||||
```
|
||||
|
||||
UUID v7 はタイムスタンプ埋め込みで辞書順 = 時系列順。独自フォーマット不要。
|
||||
|
||||
### Store trait(store.rs)
|
||||
|
||||
```rust
|
||||
pub trait Store: Send + Sync {
|
||||
fn append(&self, id: SessionId, entry: &LogEntry) -> impl Future<Output = Result<(), StoreError>> + Send;
|
||||
fn read_all(&self, id: SessionId) -> impl Future<Output = Result<Vec<LogEntry>, StoreError>> + Send;
|
||||
fn list_sessions(&self) -> impl Future<Output = Result<Vec<SessionId>, StoreError>> + Send;
|
||||
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> impl Future<Output = Result<(), StoreError>> + Send;
|
||||
fn exists(&self, id: SessionId) -> impl Future<Output = Result<bool, StoreError>> + Send;
|
||||
|
||||
// EventTrace 用(デバッグモード時のみ使用)
|
||||
fn append_trace(&self, id: SessionId, entry: &TraceEntry) -> impl Future<Output = Result<(), StoreError>> + Send;
|
||||
}
|
||||
```
|
||||
|
||||
RPITIT (Rust 1.75+) 使用。`async_trait` 不要。
|
||||
|
||||
### FsStore(fs_store.rs)
|
||||
|
||||
ファイル配置:
|
||||
- セッションログ: `{root}/{session_id}.jsonl`
|
||||
- イベントトレース: `{root}/{session_id}.trace.jsonl`
|
||||
|
||||
append モードで書き込み。SQLite インデックスなし。
|
||||
|
||||
### Session ラッパー(session.rs)
|
||||
|
||||
Worker を直接変更せず、**外部ラッパー** として実装:
|
||||
|
||||
```rust
|
||||
pub struct Session<C: LlmClient, St: Store> {
|
||||
pub worker: Worker<C, Mutable>, // pub で直接アクセス可能
|
||||
store: St,
|
||||
session_id: SessionId,
|
||||
config: SessionConfig,
|
||||
}
|
||||
```
|
||||
|
||||
- `Session::new()` → SessionStart を append
|
||||
- `Session::run()` → Worker::run() の前後で history.len() を比較、差分をログ記録
|
||||
- `Session::resume()` → 同上
|
||||
- `Session::fork()` → 現在の history をシードにした新 SessionStart を書き込み
|
||||
- `Session::fork_at(store, source_id, entry_idx)` → 任意地点から分岐
|
||||
|
||||
**復元**: `restore_session(client, store, session_id)` → read_all → replay_entries → Worker 再構築
|
||||
|
||||
### EventTrace 記録(デバッグモード)
|
||||
|
||||
`SessionConfig::record_event_trace: bool`(デフォルト `false`)が `true` の場合、
|
||||
Session が Worker に `OnStreamChunk` Hook を登録。
|
||||
Hook 内で `TraceEntry` を `{session_id}.trace.jsonl` に append。
|
||||
セッションログとは完全に分離。
|
||||
|
||||
## 実装順序
|
||||
|
||||
1. `RequestConfig` に Serialize/Deserialize 追加(llm-worker 側)
|
||||
2. Worker に `set_turn_count` / `set_last_run_interrupted` 追加(llm-worker 側)
|
||||
3. `crates/llm-worker-persistence/` クレート作成(Cargo.toml + ワークスペース登録)
|
||||
4. `session_log.rs` 作成(LogEntry + Outcome + replay_entries)
|
||||
5. `event_trace.rs` 作成(TraceEntry)
|
||||
6. `store.rs` 作成(Store trait + StoreError)
|
||||
7. `fs_store.rs` 作成(JSONL ファイルシステム実装)
|
||||
8. `session.rs` 作成(Session ラッパー + restore_session)
|
||||
9. `lib.rs` 作成(re-exports・SessionId 型エイリアス・new_session_id)
|
||||
10. テスト作成(replay round-trip, FsStore 読み書き, Session::run ログ記録)
|
||||
11. `docs/persistence.md` 設計ドキュメント作成
|
||||
|
||||
## 検証方法
|
||||
|
||||
1. **ユニットテスト**: `replay_entries` に手動構築した LogEntry 列を渡し、復元状態を検証
|
||||
2. **統合テスト**: MockLlmClient + FsStore で Session::run → restore_session → history 一致を確認
|
||||
3. **Fork テスト**: fork → 新セッションの history が fork 時点と一致することを確認
|
||||
4. **cargo test**: 既存テストが壊れていないことを確認
|
||||
5. **cargo clippy / cargo check**: 警告なし
|
||||
Reference in New Issue
Block a user