docs: consolidate design documentation and standardize crate English

This commit is contained in:
2026-10-12 14:58:00 +09:00
parent a633a53274
commit 4efb0ace1f
404 changed files with 4556 additions and 11654 deletions
+11 -28
View File
@@ -1,32 +1,15 @@
# session-store
## Role
`session-store` stores replayable Session history and current Worker restoration data.
Session history uses append-only JSONL; current Segment references and child/peer
registrations live separately in Worker metadata. It does not own process liveness
checks or Runtime catalog lifecycle management.
`session-store` owns replayable append-only session logs.
## Design entry points
## Boundaries
Owns:
- session identifiers and segment lineage
- JSONL log entries for replayable conversation/runtime history
- restoring Engine/session state from committed records
- schema surfaces that should make drift compile-visible
Does not own:
- current Worker-name metadata (`session-store` worker metadata)
- live process/socket discovery (`worker-allocation`, `client`)
- UI state (`tui`)
- generated memory summaries (`memory`)
## Design notes
A session log records what happened. It is not the current Worker registry and should not be queried as the only source of "what does Worker X mean now?"
Prefer explicit current log variants over broad legacy compatibility when schema changes; hidden compatibility can make future replay bugs silent.
## See also
- [`../../docs/design/worker-session-state.md`](../../docs/design/worker-session-state.md)
- [`../../docs/design/context-history.md`](../../docs/design/context-history.md)
- [`segment_log`] / [`segment`]: history replay and fork/compaction lineage
- [`worker_session_store`]: storage boundary for a Session bound to a Worker
- [`worker_metadata`]: current Worker state and active Segment switching
- [`system_item`]: records of system input presented to the model
- [`public_snapshot`] / [`public_index`]: public projections of stored history
- [`event_trace`]: diagnostic records independent of history replay
+5 -7
View File
@@ -1,11 +1,9 @@
//! Debug-only LLM request/stream trace recording.
//! Diagnostic records of LLM transport, independent of history replay.
//!
//! [`TraceEntry`] captures stream lifecycle markers and normalized provider
//! stream events for debugging stalls. It is not a byte-for-byte raw SSE
//! capture. Written to a separate `.trace.jsonl` file,
//! completely independent of the segment log used for state restoration.
//!
//! Disabled by default. Enable via `SessionConfig::record_event_trace`.
//! [`TraceEntry`] records normalized events and lifecycle boundaries, not a complete
//! copy of raw SSE. Stored separately from Segment logs, it is not a source for
//! restored model input. This separates observed transport state from input/output
//! committed to history.
use agen::llm_client::event::Event;
use serde::{Deserialize, Serialize};
+4 -36
View File
@@ -1,35 +1,4 @@
//! Session persistence via append-only JSONL logs.
//!
//! # Architecture
//!
//! A [`Session`](SessionId) is a fork-tree of [`Segment`](SegmentId)s
//! belonging to the same logical conversation. Each Segment is recorded
//! as a sequence of [`LogEntry`] values, one per line in a `.jsonl`
//! file. Reading a segment log and collecting entries reconstructs the
//! Engine state at that segment — no separate snapshots or checkpoints
//! needed. Compaction and fork operations mint a fresh Segment within
//! the same Session.
//!
//! This crate provides free functions for persistence operations.
//! The caller (typically Worker) holds the Engine directly and calls these
//! functions after state-mutating operations.
//!
//! Debug-mode [`TraceEntry`] records capture raw stream events in a separate
//! `.trace.jsonl` file, independent of the segment log.
//!
//! # Quick start
//!
//! ```ignore
//! use session_store::{create_segment, restore, save_delta, FsStore, SegmentStartState};
//!
//! let store = FsStore::new("./sessions")?;
//! let (session_id, segment_id) = create_segment(&store, SegmentStartState {
//! system_prompt: None,
//! config: &config,
//! history: Vec::new(),
//! user_segments: Vec::new(),
//! })?;
//! ```
#![doc = include_str!("../README.md")]
use std::io;
use std::path::Path;
@@ -141,11 +110,10 @@ pub use worker_metadata::{
};
pub use worker_session_store::WorkerSessionStore;
/// Session identifier — the fork-tree root. UUID v7 (time-ordered).
/// UUID v7 identifying Segments belonging to the same conversation.
///
/// All Segments belonging to the same Session share this ID. Compaction
/// and fork operations create a new Segment within the same Session, so
/// `WHERE session_id = ?` retrieves the full lineage.
/// Fork/compaction preserves this ID and only adds Segments. Switching the current
/// history projection therefore does not change the conversation-wide reference.
pub type SessionId = uuid::Uuid;
/// Segment identifier. UUID v7 (time-ordered, lexicographically sortable).
+8 -8
View File
@@ -82,8 +82,8 @@ pub enum LoggedItem {
summary: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
encrypted_content: Option<String>,
/// Anthropic extended thinking signature。新世代 Claude で round-trip
/// 必須。OpenAI Responses など他 scheme では `None`。
/// Anthropic extended thinking signature, required for round-trips with
/// newer Claude generations. `None` for other schemes such as OpenAI Responses.
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
@@ -418,13 +418,13 @@ mod tests {
#[test]
fn round_trip_reasoning_preserves_signature() {
// 新世代 Claude の thinking signature が history.json に永続化され、
// resume 後の Item::Reasoning に復元されること。
// A newer Claude thinking signature persists in history.json and is
// restored to Item::Reasoning after resume.
let original = Item::reasoning("inner thought").with_signature("SIG-OPUS-XYZ");
let logged: LoggedItem = (&original).into();
let json = serde_json::to_string(&logged).unwrap();
// wire 形式に signature キーが乗ること(古い形式との互換のため
// 値が None のときは省略される。Some の値は載る)
// The wire format includes Some signatures; None is omitted for
// compatibility with older formats.
assert!(
json.contains("SIG-OPUS-XYZ"),
"serialised JSON must carry signature: {json}",
@@ -443,8 +443,8 @@ mod tests {
#[test]
fn legacy_reasoning_without_signature_field_deserializes() {
// signature フィールドが無い旧形式の history.json を読み込んでも
// None としてロードできる(後方互換性)。
// Legacy history.json without a signature field loads it as None
// for backward compatibility.
let legacy_json =
r#"{"kind":"reasoning","text":"old","summary":[],"encrypted_content":null}"#;
let parsed: LoggedItem = serde_json::from_str(legacy_json).unwrap();
@@ -1,3 +1,14 @@
//! Projects stored history for clients.
//!
//! Converts committed entry IDs and provenance to public DTOs, keeping JSONL replay
//! rules out of clients. Model reasoning, system prompts, internal response boundaries,
//! and extension payloads are not exposed in conversation views. Measurements use
//! dedicated readers and do not enter ordinary snapshots.
//!
//! Retained-history reads validate active references and Session IDs and cap read volume.
//! Observation neither migrates old formats nor repairs corruption; required migration
//! or repair returns an error. Matches live output by entry ID, not timestamps alone.
mod run_stats;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
+13 -9
View File
@@ -1,8 +1,13 @@
//! Free functions for segment persistence operations.
//! Segment creation, append, and restoration.
//!
//! These functions record and restore segment state without owning a Engine.
//! The caller (typically Worker) holds the Engine directly and calls these
//! functions after state-mutating operations.
//! Records host-committed history and execution boundaries in Store without owning
//! Engine. Fork and compaction create a Segment with initial history in the same
//! Session without rewriting the source. Initial records retain the source Segment
//! and adopted boundary so replacement and branching decisions remain traceable.
//!
//! Saving a new Segment does not change the active reference. The host coordinates
//! compaction activation with Worker metadata and the live append target. On failure,
//! a saved Segment alone does not establish that current history has switched.
use crate::segment_log::{self, LogEntry, SegmentOrigin};
use crate::store::{Store, StoreError};
@@ -400,12 +405,11 @@ pub fn save_run_errored(
)
}
/// Log an `LlmUsage` entry — 1 LLM リクエスト分の Usage スナップショット。
/// Logs an `LlmUsage` entry: a usage snapshot for one LLM request.
///
/// `history_len` は送信時の `history.len()`。`input_total_tokens` は
/// その prefix をプロバイダが実測した占有量(プロンプト全長)で、
/// プロバイダ別の正規化(Anthropic では `input + cache_read + cache_creation`)を
/// 済ませた値を渡す。
/// `history_len` is `history.len()` at send time. `input_total_tokens` is the
/// provider-measured size of that prefix (the full prompt). Pass a value normalized
/// for the provider: `input + cache_read + cache_creation` for Anthropic.
pub fn save_usage(
store: &impl Store,
session_id: SessionId,
+40 -46
View File
@@ -1,13 +1,13 @@
//! Segment log types for append-only JSONL persistence.
//! Replays Segment state from append-only JSONL.
//!
//! Each [`LogEntry`] represents a single state transition within one
//! segment, serialized as one line in a `.jsonl` file. Reading all
//! entries and collecting them via [`collect_state`] reconstructs the
//! full [`Engine`] state at that segment.
//! [`collect_state`] restores model input, configuration, and execution interruption
//! state from [`LogEntry`] position order and initial history. Fork/compaction seeds
//! retain lineage to the old Segment and history item IDs and provenance.
//!
//! The on-disk format is one `LogEntry` per line — entries are positionally
//! ordered. Fork lineage references between segments use turn-number indices
//! (`SegmentOrigin.at_turn_index`) rather than per-entry hashes.
//! An execution-start record alone does not imply success. Runs without terminal
//! records restore as interrupted; uncommitted tool execution is not treated as
//! successful. Yield/resume records mark intermediate logical-Run boundaries,
//! preserving the turn budget even when resuming after compaction.
use agen::llm_client::types::{Item, RequestConfig};
use agen::{EngineResult, UsageRecord};
@@ -17,20 +17,10 @@ use serde::{Deserialize, Serialize};
use crate::history::{LoggedHistoryEntry, LoggedSystemHistoryEntry};
use crate::logged_item::LoggedItem;
/// A single segment log entry, serialized as one JSONL line.
/// Host-owned extension data.
///
/// Variants correspond to specific mutation points in `Engine`:
/// - `SegmentStart` — always the first entry; captures initial state
/// - `Invoke` — IDLE → active marker (start of a new self-driving cycle)
/// - `UserInput` / `AssistantItem` / `ToolResult` / `SystemItem` — history appends
/// - `TurnEnd` — AgentTurn boundary marker; carries the post-increment
/// `turn_count`. With retry unimplemented today this fires once per
/// `run()`/`resume()` (current callers persist a single TurnEnd at
/// run completion); the fork-point seq for `at_turn_index` is the
/// preceding `Invoke` entry, not the TurnEnd.
/// - `RunCompleted` / `RunCancelled` / `RunErrored` — terminal run outcomes
/// - `PausedTurnAbandoned` — explicit abandon/cancel of a paused interrupted turn
/// - `ConfigChanged` — `RequestConfig` mutation
/// Excluded from public conversation-history projections; interpreted by
/// dedicated domain readers, such as metrics readers.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionExtension {
pub domain: String,
@@ -46,6 +36,11 @@ impl SessionExtension {
}
}
/// JSONL record of history additions and state transitions within a Segment.
///
/// History items and provenance commit in the same record, separately from execution
/// boundaries and extension data. These records do not establish current Worker
/// catalog state or process liveness.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LogEntry {
@@ -123,7 +118,7 @@ pub enum LogEntry {
/// Turn boundary. Records the turn count after increment.
TurnEnd { ts: u64, turn_count: usize },
/// `run()` / `resume()` が `EngineResult` で正常終了した。
/// `run()` / `resume()` returned normally with an `EngineResult`.
/// Replay restores both interruption state and any resumable logical-run
/// turn budget.
RunCompleted {
@@ -172,9 +167,9 @@ pub enum LogEntry {
entry_id: Option<crate::LoggedSessionHistoryEntryId>,
},
/// `run()` / `resume()` が `EngineError` で終了した。
/// `EngineError` は `Serialize` 不可なので `message` のみ lossy 保持する。
/// Audit-only metadata: replay は `interrupted` のみ反映する。
/// `run()` / `resume()` ended with an `EngineError`.
/// `EngineError` is not `Serialize`, so only `message` is retained lossily.
/// Audit-only metadata: replay applies only `interrupted`.
RunErrored {
ts: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -204,19 +199,19 @@ pub enum LogEntry {
/// `RequestConfig` changed.
ConfigChanged { ts: u64, config: RequestConfig },
/// LLM リクエスト 1 件分の Usage スナップショット。
/// Usage snapshot for one LLM request.
///
/// `history_len` は送信時の `history.len()`。`input_total_tokens` は
/// その prefix をプロバイダが実測した占有量(プロンプト全長)。
/// このリクエスト 1 件で新しく追加された分ではない。
/// `history_len` is `history.len()` at send time. `input_total_tokens` is the
/// provider-measured size of that prefix (the full prompt), not the amount
/// newly added by this request.
///
/// プロバイダ別の正規化(呼び出し側で行う想定):
/// Provider-specific normalization (expected from the caller):
/// - Anthropic: `input_tokens + cache_read + cache_creation`
/// - OpenAI: `prompt_tokens`
/// - Gemini: `promptTokenCount`
/// - Ollama: `prompt_eval_count`
///
/// `cache_read_tokens` / `cache_write_tokens` は上記の内訳で、料金会計用。
/// `cache_read_tokens` / `cache_write_tokens` break down the above for billing.
LlmUsage {
ts: u64,
history_len: usize,
@@ -226,14 +221,13 @@ pub enum LogEntry {
output_tokens: u64,
},
/// 汎用拡張点。ドメイン名で名前空間を切って任意 JSON を載せる。
/// session-store は payload を不透明扱いし、replay 時は
/// `RestoredState.extensions` に `(domain, payload)` を順に積むだけ。
/// 各ドメイン側が自前で fold して最新値を取り出す前提。
/// Generic extension point for arbitrary JSON namespaced by domain.
/// session-store treats payloads as opaque; replay only appends
/// `(domain, payload)` to `RestoredState.extensions` in order.
/// Each domain folds its own records to obtain the latest value.
///
/// 想定用途: memory subsystem の extract 処理境界 pointer 等、
/// 「session 寿命に縛りたいが session-store の型を汚したくない」
/// メタデータ。
/// Intended for metadata tied to the Session lifetime without adding
/// session-store types, such as memory extraction boundary pointers.
Extension {
ts: u64,
domain: String,
@@ -317,12 +311,12 @@ pub struct RestoredState {
/// Writers track their own append count via the same counter so
/// `ensure_head_or_fork` can compare it with the on-disk count.
pub entries_count: usize,
/// LLM リクエストごとの Usage スナップショット時系列。
/// `LogEntry::LlmUsage` を replay して時系列順に積まれる。
/// 任意位置のトークン数推定に使う。
/// Per-request usage snapshots in chronological order.
/// Populated by replaying `LogEntry::LlmUsage`.
/// Used to estimate token counts at arbitrary positions.
pub usage_history: Vec<UsageRecord>,
/// `LogEntry::Extension` を replay 順に積んだもの。`(domain, payload)`。
/// session-store は domain を不透明扱いし、各ドメインが自前で fold する。
/// `(domain, payload)` values from `LogEntry::Extension` in replay order.
/// session-store treats domains as opaque; each domain folds its own records.
pub extensions: Vec<(String, serde_json::Value)>,
/// User submissions in original typed form, in submit order.
/// One entry per `LogEntry::AnnotatedUserInput`; the K-th entry corresponds to
@@ -766,9 +760,9 @@ mod tests {
output_tokens: 5,
},
]);
// history は LlmUsage で変化しない
// LlmUsage does not change history.
assert_eq!(state.history.len(), 2);
// usage_history は時系列順
// usage_history is in chronological order.
assert_eq!(state.usage_history.len(), 2);
assert_eq!(state.usage_history[0].history_len, 1);
assert_eq!(state.usage_history[0].input_total_tokens, 50);
@@ -1114,7 +1108,7 @@ mod tests {
payload: serde_json::json!({ "x": 1 }),
},
]);
// 順序保持で全件積まれる。fold は呼び出し側の責務。
// All records retain their order. Folding is the caller's responsibility.
assert_eq!(state.extensions.len(), 3);
assert_eq!(state.extensions[0].0, "memory.extract");
assert_eq!(state.extensions[1].1["processed_through_entry"], 12);
+9 -17
View File
@@ -1,22 +1,14 @@
//! Typed system-message items injected by the agent system.
//! Typed records of system input a Worker presents to the model.
//!
//! Items in worker history with `role:system` are never produced by the
//! LLM — they are always inserted by the Worker itself (notifications,
//! file ref resolutions, child-worker lifecycle events, reminders, …).
//! [`SystemItem`] carries the
//! typed shape of each such injection so clients can dispatch on
//! `kind` instead of parsing text prefixes like `[Notification] …` or
//! `[File: …]`.
//! Notifications, child events, reference resolution, and reminders are Worker-added
//! input, not model output. [`SystemItem`] retains the display kind and model-visible
//! body, representing the same input in history and live events. Stored bodies allow
//! old input to be reproduced even if prompt rendering rules later change.
//!
//! Persisted as the payload of [`crate::LogEntry::AnnotatedSystemItem`] (one
//! entry per item), and broadcast live as the payload of
//! `Event::SystemItem` on the wire.
//!
//! For LLM context replay, each `SystemItem` reduces to an
//! `Item::system_message(...)` whose body matches the legacy free-text
//! shape (see [`SystemItem::history_text`]). The kind metadata is
//! preserved only on the log/wire side; the LLM still sees plain
//! system-message text.
//! Input affecting the model is committed via [`crate::LogEntry::AnnotatedSystemItem`]
//! before use. Request-only transient insertion would leave later turns unable to
//! explain decisions from stored history. The model projection uses only the body;
//! clients can render using type information without parsing body prefixes.
use agen::llm_client::types::Item;
use protocol::WorkerEvent;
+14 -11
View File
@@ -1,16 +1,19 @@
//! Durable Worker-name metadata/state persistence.
//! Stores current Worker restoration data separately from Session history.
//!
//! This crate owns the name-keyed Worker state surface under a Worker-state root,
//! e.g. `{data_dir}/workers/{worker_name}/metadata.json`. Session JSONL replay stays
//! in `session-store`; Worker metadata may point at a `(SessionId, SegmentId)` but
//! does not own or replay session logs.
//! [`WorkerAggregateStore`] handles metadata for one Runtime-selected Worker.
//! The active Segment, resolved manifest, and child/peer visibility are current
//! state, separate from state replayed from past conversation. Session model input
//! is restored from Segment-log initial history and appended records.
//!
//! `resolved_manifest_snapshot` is authority only for Worker-name restore before
//! loading the session log. Existing segment replay still uses `SegmentStart`
//! entries from `session-store`. `spawned_children` is durable current parent
//! Worker state for child registry/reclaim; child lifecycle messages shown to the
//! model remain session JSONL history. Socket and callback paths are last-known
//! runtime hints, not proof of liveness.
//! Active-reference compare-and-swap replaces only a matching source Segment.
//! Updates preserve unrelated metadata and prevent stale compaction results from
//! overwriting newer references. Callers coordinate switching with append targets
//! and allocation.
//!
//! Child delegation/reclamation state is current restoration data; model-facing
//! child notifications are recorded separately in Session history. Peer registration
//! includes no delegated scope. Stored socket/callback locations are last-known
//! execution data, not proof of liveness.
use crate::{SegmentId, SessionId};
use serde::{Deserialize, Serialize};
@@ -1,13 +1,19 @@
//! Filesystem store for the canonical `1 Worker = 1 Session` aggregate.
//! File storage for a Session bound to one Worker.
//!
//! Layout under one Worker aggregate:
//! - `session/session.json` — immutable Session identity
//! - `session/segments/<segment_id>.jsonl`
//! - `session/segments/<segment_id>.trace.jsonl`
//! The manifest fixes the Session ID; requests to switch Sessions are rejected.
//! Fork/compaction adds Segments within the same Session, so Worker restoration and
//! history observation need not change the conversation ID. Runtime execution attempts
//! and sockets are not part of this ID.
//!
//! Unlike [`crate::FsStore`], this store cannot enumerate or switch between
//! arbitrary Sessions. The first segment materializes the sole Session identity;
//! every later operation must use that same ID.
//! Validates complete JSONL records before append; only a trailing fragment without
//! a closing newline is discarded as uncommitted. Corrupt complete records are rejected.
//! Failed appends attempt rollback to the previous boundary; if rollback also fails,
//! both failures are returned. Whole-Segment and manifest saves sync temporary files
//! and rename them.
//!
//! Retained-history observation uses a read path without creation or migration.
//! Reads are rejected where access time cannot be preserved, avoiding observation-induced
//! changes to stored state.
use crate::event_trace::TraceEntry;
use crate::paste_artifact::{read_from_dir, write_to_dir};