session-storeとして分離
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user