session-storeとして分離

This commit is contained in:
2026-04-12 06:31:34 +09:00
parent eb670bfba5
commit cdafd5d914
25 changed files with 910 additions and 657 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ license.workspace = true
async-trait = "0.1.89"
clap = { version = "4.6.0", features = ["derive"] }
llm-worker = { version = "0.2.1", path = "../llm-worker" }
llm-worker-persistence = { version = "0.1.0", path = "../llm-worker-persistence" }
session-store = { version = "0.1.0", path = "../session-store" }
manifest = { version = "0.1.0", path = "../manifest" }
protocol = { version = "0.1.0", path = "../protocol" }
provider = { version = "0.1.0", path = "../provider" }
@@ -23,6 +23,6 @@ tracing = "0.1.44"
async-trait = "0.1.89"
dotenv = "0.15.0"
futures = "0.3.32"
llm-worker-persistence = { path = "../llm-worker-persistence" }
session-store = { path = "../session-store" }
tempfile = "3.27.0"
tokio = { version = "1.49", features = ["macros", "rt-multi-thread", "time"] }
+2 -2
View File
@@ -12,7 +12,7 @@
//! ```
use pod::{Pod, PodManifest, PodRunResult};
use llm_worker_persistence::FsStore;
use session_store::FsStore;
const MANIFEST_TOML: &str = r#"
[pod]
@@ -52,7 +52,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
// 5. Extract the assistant's reply from history
let history = pod.session_mut().worker().history();
let history = pod.worker().history();
if let Some(text) = history
.iter()
.rev()
+1 -1
View File
@@ -6,7 +6,7 @@
//! ```
use pod::{Event, Method, PodController, PodManifest};
use llm_worker_persistence::FsStore;
use session_store::FsStore;
const MANIFEST_TOML: &str = r#"
[pod]
+10 -17
View File
@@ -3,7 +3,7 @@ use std::sync::Arc;
use llm_worker::llm_client::client::LlmClient;
use llm_worker::WorkerError;
use llm_worker_persistence::Store;
use session_store::Store;
use tokio::sync::{broadcast, mpsc};
use crate::pod::{Pod, PodRunResult, PodError};
@@ -85,7 +85,7 @@ impl PodController {
// Register event bridge callbacks on the worker
{
let worker = pod.session_mut().worker_mut();
let worker = pod.worker_mut();
let tx = event_tx.clone();
worker.on_turn_start(move |turn| {
@@ -158,7 +158,7 @@ impl PodController {
}
// Clone cancel sender before moving pod
let cancel_tx = pod.session_mut().worker_mut().cancel_sender();
let cancel_tx = pod.worker_mut().cancel_sender();
tokio::spawn(async move {
// Hold socket server alive for the lifetime of the controller task
@@ -191,7 +191,7 @@ impl PodController {
)
.await;
let items = pod.session_mut().worker_mut().history().to_vec();
let items = pod.worker().history().to_vec();
shared_state.update_history(items);
shared_state.set_status(new_status);
let _ = runtime_dir.write_status(&shared_state).await;
@@ -218,7 +218,7 @@ impl PodController {
)
.await;
let items = pod.session_mut().worker_mut().history().to_vec();
let items = pod.worker().history().to_vec();
shared_state.update_history(items);
shared_state.set_status(new_status);
let _ = runtime_dir.write_status(&shared_state).await;
@@ -307,19 +307,12 @@ where
fn worker_error_code(e: &PodError) -> ErrorCode {
match e {
PodError::Session(se) => {
use llm_worker_persistence::SessionError;
match se {
SessionError::Worker(we) => match we {
WorkerError::Tool(_) => ErrorCode::ToolError,
WorkerError::Client(_) => ErrorCode::ProviderError,
_ => ErrorCode::Internal,
},
_ => ErrorCode::Internal,
}
}
PodError::Worker(we) => match we {
WorkerError::Tool(_) => ErrorCode::ToolError,
WorkerError::Client(_) => ErrorCode::ProviderError,
_ => ErrorCode::Internal,
},
PodError::Provider(_) => ErrorCode::ProviderError,
_ => ErrorCode::Internal,
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Parser;
use llm_worker_persistence::FsStore;
use session_store::FsStore;
use pod::{Pod, PodController};
#[derive(Parser)]
+167 -39
View File
@@ -3,9 +3,10 @@ use std::sync::Arc;
use llm_worker::llm_client::client::LlmClient;
use llm_worker::llm_client::RequestConfig;
use llm_worker::Worker;
use llm_worker_persistence::{
Session, SessionConfig, SessionError, SessionId, Store, StoreError,
use llm_worker::state::Mutable;
use llm_worker::{Worker, WorkerError, WorkerResult};
use session_store::{
EntryHash, Outcome, SessionId, SessionStartState, Store, StoreError,
};
use manifest::{PodManifest, Scope, WorkerManifest};
@@ -18,11 +19,15 @@ use crate::hook_interceptor::HookInterceptor;
/// An independent agent execution unit.
///
/// Wraps a persistent [`Session`] with manifest metadata and an optional
/// directory scope. This is the primary abstraction in insomnia.
/// Holds a [`Worker`] directly and persists session state via
/// `session-store` functions after each turn.
pub struct Pod<C: LlmClient, St: Store> {
manifest: PodManifest,
session: Session<C, St>,
/// Always `Some` outside of `run()`/`resume()`.
worker: Option<Worker<C, Mutable>>,
store: St,
session_id: SessionId,
head_hash: Option<EntryHash>,
scope: Option<Scope>,
hook_builder: HookRegistryBuilder,
interceptor_installed: bool,
@@ -30,20 +35,24 @@ pub struct Pod<C: LlmClient, St: Store> {
impl<C: LlmClient, St: Store> Pod<C, St> {
/// Create a new Pod from a pre-built Worker and store.
///
/// The caller is responsible for constructing the `LlmClient` from the
/// manifest's provider config. This keeps Pod free of provider-specific
/// dependencies.
pub async fn new(
manifest: PodManifest,
worker: Worker<C>,
store: St,
scope: Option<Scope>,
) -> Result<Self, PodError> {
let session = Session::new(worker, store, SessionConfig::default()).await?;
let state = SessionStartState {
system_prompt: worker.get_system_prompt(),
config: worker.request_config(),
history: worker.history(),
};
let (session_id, head_hash) = session_store::create_session(&store, state).await?;
Ok(Self {
manifest,
session,
worker: Some(worker),
store,
session_id,
head_hash: Some(head_hash),
scope,
hook_builder: HookRegistryBuilder::new(),
interceptor_installed: false,
@@ -58,10 +67,22 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
store: St,
scope: Option<Scope>,
) -> Result<Self, PodError> {
let session = Session::restore(client, store, session_id, SessionConfig::default()).await?;
let state = session_store::restore(&store, session_id).await?;
let mut worker = Worker::new(client);
if let Some(ref prompt) = state.system_prompt {
worker.set_system_prompt(prompt);
}
worker.set_history(state.history);
worker.set_request_config(state.config);
worker.set_turn_count(state.turn_count);
worker.set_last_run_interrupted(state.last_run_interrupted);
Ok(Self {
manifest,
session,
worker: Some(worker),
store,
session_id,
head_hash: state.head_hash,
scope,
hook_builder: HookRegistryBuilder::new(),
interceptor_installed: false,
@@ -70,7 +91,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// The session ID used for persistence.
pub fn session_id(&self) -> SessionId {
self.session.session_id()
self.session_id
}
/// The Pod's manifest.
@@ -83,18 +104,25 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
self.scope.as_ref()
}
/// Direct access to the underlying session.
/// Direct access to the underlying Worker.
pub fn worker(&self) -> &Worker<C, Mutable> {
self.worker.as_ref().expect("worker taken during run")
}
/// Mutable access to the underlying Worker.
///
/// Use this to register tools, hooks, or subscribers on the worker
/// before calling [`run`](Self::run).
pub fn session_mut(&mut self) -> &mut Session<C, St> {
&mut self.session
/// Use this to register tools, hooks, or subscribers before calling
/// [`run`](Self::run).
pub fn worker_mut(&mut self) -> &mut Worker<C, Mutable> {
self.worker.as_mut().expect("worker taken during run")
}
/// Reference to the store.
pub fn store(&self) -> &St {
&self.store
}
// --- Hook registration ---
//
// Hooks must be registered before the first call to `run()` or `resume()`.
// Attempting to add a hook after execution has started will panic.
fn assert_hooks_open(&self) {
assert!(
@@ -145,7 +173,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let builder = std::mem::take(&mut self.hook_builder);
let registry = Arc::new(builder.build());
let interceptor = HookInterceptor::new(registry);
self.session.worker_mut().set_interceptor(interceptor);
self.worker_mut().set_interceptor(interceptor);
self.interceptor_installed = true;
}
}
@@ -153,23 +181,114 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// Send user input and run until the LLM turn completes.
pub async fn run(&mut self, input: impl Into<String>) -> Result<PodRunResult, PodError> {
self.ensure_interceptor_installed();
let result = self.session.run(input).await?;
Ok(result.into())
// Split borrow: access worker field directly to allow concurrent
// mutable borrows on session_id / head_hash.
let w = self.worker.as_ref().unwrap();
session_store::ensure_head_or_fork(
&self.store,
&mut self.session_id,
&mut self.head_hash,
SessionStartState {
system_prompt: w.get_system_prompt(),
config: w.request_config(),
history: w.history(),
},
)
.await?;
let history_before = self.worker.as_ref().unwrap().history().len();
// lock → run → unlock
let worker = self.worker.take().expect("worker taken during run");
let mut locked = worker.lock();
let result = locked.run(input).await;
self.worker = Some(locked.unlock());
self.persist_turn(history_before, &result).await?;
result.map(PodRunResult::from).map_err(PodError::Worker)
}
/// Resume from a paused state.
pub async fn resume(&mut self) -> Result<PodRunResult, PodError> {
self.ensure_interceptor_installed();
let result = self.session.resume().await?;
Ok(result.into())
let w = self.worker.as_ref().unwrap();
session_store::ensure_head_or_fork(
&self.store,
&mut self.session_id,
&mut self.head_hash,
SessionStartState {
system_prompt: w.get_system_prompt(),
config: w.request_config(),
history: w.history(),
},
)
.await?;
let history_before = self.worker.as_ref().unwrap().history().len();
// lock → resume → unlock
let worker = self.worker.take().expect("worker taken during run");
let mut locked = worker.lock();
let result = locked.resume().await;
self.worker = Some(locked.unlock());
self.persist_turn(history_before, &result).await?;
result.map(PodRunResult::from).map_err(PodError::Worker)
}
/// Persist delta + turn end + outcome after a run/resume.
async fn persist_turn(
&mut self,
history_before: usize,
result: &Result<WorkerResult, WorkerError>,
) -> Result<(), StoreError> {
// Use direct field access for split borrows (worker immutable,
// head_hash mutable).
let w = self.worker.as_ref().unwrap();
let new_items = &w.history()[history_before..];
session_store::save_delta(
&self.store,
self.session_id,
&mut self.head_hash,
new_items,
)
.await?;
let turn_count = self.worker.as_ref().unwrap().turn_count();
session_store::save_turn_end(
&self.store,
self.session_id,
&mut self.head_hash,
turn_count,
)
.await?;
let interrupted = self.worker.as_ref().unwrap().last_run_interrupted();
let outcome = match result {
Ok(WorkerResult::Finished) => Outcome::Finished,
Ok(WorkerResult::Paused) => Outcome::Paused,
Ok(WorkerResult::LimitReached) => Outcome::LimitReached,
Err(e) => Outcome::Error {
message: e.to_string(),
},
};
session_store::save_outcome(
&self.store,
self.session_id,
&mut self.head_hash,
outcome,
interrupted,
)
.await?;
Ok(())
}
}
impl<St: Store> Pod<Box<dyn LlmClient>, St> {
/// Create a Pod entirely from a manifest.
///
/// Builds the LLM client from the provider config, applies worker
/// settings, and creates a new persistent session.
pub async fn from_manifest(
manifest: PodManifest,
store: St,
@@ -179,10 +298,19 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
let client = provider::build_client(&manifest.provider, manifest_dir.as_deref())?;
let mut worker = Worker::new(client);
apply_worker_manifest(&mut worker, &manifest.worker);
let session = Session::new(worker, store, SessionConfig::default()).await?;
let state = SessionStartState {
system_prompt: worker.get_system_prompt(),
config: worker.request_config(),
history: worker.history(),
};
let (session_id, head_hash) = session_store::create_session(&store, state).await?;
Ok(Self {
manifest,
session,
worker: Some(worker),
store,
session_id,
head_hash: Some(head_hash),
scope,
hook_builder: HookRegistryBuilder::new(),
interceptor_installed: false,
@@ -217,12 +345,12 @@ pub enum PodRunResult {
LimitReached,
}
impl From<llm_worker::WorkerResult> for PodRunResult {
fn from(r: llm_worker::WorkerResult) -> Self {
impl From<WorkerResult> for PodRunResult {
fn from(r: WorkerResult) -> Self {
match r {
llm_worker::WorkerResult::Finished => PodRunResult::Finished,
llm_worker::WorkerResult::Paused => PodRunResult::Paused,
llm_worker::WorkerResult::LimitReached => PodRunResult::LimitReached,
WorkerResult::Finished => PodRunResult::Finished,
WorkerResult::Paused => PodRunResult::Paused,
WorkerResult::LimitReached => PodRunResult::LimitReached,
}
}
}
@@ -231,7 +359,7 @@ impl From<llm_worker::WorkerResult> for PodRunResult {
#[derive(Debug, thiserror::Error)]
pub enum PodError {
#[error(transparent)]
Session(#[from] SessionError),
Worker(#[from] WorkerError),
#[error(transparent)]
Store(#[from] StoreError),
+1 -1
View File
@@ -107,7 +107,7 @@ mod tests {
fn test_state() -> PodSharedState {
PodSharedState::new(
"test-pod".into(),
llm_worker_persistence::new_session_id(),
session_store::new_session_id(),
"[pod]\nname = \"test-pod\"".into(),
)
}
+2 -2
View File
@@ -1,7 +1,7 @@
use std::sync::RwLock;
use llm_worker::llm_client::types::Item;
use llm_worker_persistence::SessionId;
use session_store::SessionId;
use serde::{Deserialize, Serialize};
/// Shared state between PodController and runtime directory.
@@ -88,7 +88,7 @@ mod tests {
fn test_state() -> PodSharedState {
PodSharedState::new(
"test-pod".into(),
llm_worker_persistence::new_session_id(),
session_store::new_session_id(),
"[pod]\nname = \"test-pod\"".into(),
)
}
+1 -1
View File
@@ -7,7 +7,7 @@ use futures::Stream;
use llm_worker::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
use llm_worker::llm_client::{ClientError, LlmClient, Request};
use llm_worker::Worker;
use llm_worker_persistence::FsStore;
use session_store::FsStore;
use pod::{
Event, Method, Pod, PodController, PodManifest, PodStatus,