use std::sync::Arc;
use foundation_core::valtron::execute;
use foundation_db::traits::DocumentStore;
use foundation_db::{MemoryDocumentStore, MemoryStorage};
use foundation_errstacks::ErrorTrace;
use crate::agentic::access::{AllowAllAccess, SessionAccessProvider};
use crate::agentic::agent_loop::{AgentConfig, AgentLoop, ModelSelection};
use crate::agentic::context::{ContextConfig, ContextProvider};
use crate::agentic::errors::{AgenticError, UserId};
use crate::agentic::memory::{MemoryConfig, MemoryHierarchy};
use crate::agentic::memory_coordinator::MemoryCoordinator;
use crate::agentic::memory_store::{KvMemoryStore, MemoryStore};
use crate::agentic::message_api::MessageApi;
use crate::agentic::steering::SteeringQueues;
use crate::agentic::token_ledger::TokenLedger;
use crate::agentic::tool_impl::ToolCallManager;
use crate::agentic::toolshed::{SessionParts, ToolShed};
use crate::agentic::turn::{Answer, Turn, TurnStream};
use crate::agentic::ErrorPolicy;
use crate::harness::RouterPreset;
use crate::types::{Messages, ModelId, ProviderRouter, SessionId, SessionRecord};
pub struct AgentSession<D = MemoryDocumentStore, M = KvMemoryStore<MemoryStorage>> {
inner: Arc<SessionInner<D, M>>,
}
impl<D, M> Clone for AgentSession<D, M> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
struct SessionInner<D, M> {
session_id: SessionId,
router: ProviderRouter,
tool_manager: ToolCallManager,
queues: SteeringQueues,
memory: Arc<MemoryHierarchy<M, D>>,
message_api: MessageApi<D>,
policy: ErrorPolicy,
ledger: TokenLedger,
context_provider: Arc<ContextProvider<D, M>>,
access: Arc<dyn SessionAccessProvider>,
user: UserId,
config: AgentConfig,
models: ModelSelection,
}
pub struct AgentSessionBuilder<D = MemoryDocumentStore, M = KvMemoryStore<MemoryStorage>> {
session_id: SessionId,
require_existing: bool,
router: ProviderRouter,
toolshed: ToolShed,
access: Arc<dyn SessionAccessProvider>,
user: UserId,
policy: Option<ErrorPolicy>,
model: Option<ModelId>,
fallback_models: Vec<ModelId>,
memory_model: Option<ModelId>,
doc_store: D,
memory_store: M,
system_prompt: Option<String>,
config: AgentConfig,
context_config: ContextConfig,
memory_config: MemoryConfig,
embedder: Option<(Arc<dyn crate::agentic::embedding::EmbeddingProvider>, String)>,
}
impl AgentSession {
#[must_use]
pub fn builder(router: impl Into<RouterPreset>) -> AgentSessionBuilder {
let RouterPreset {
router,
primary_model,
memory_model,
fallback_models,
} = router.into();
let mut builder = AgentSessionBuilder::new(
router,
MemoryDocumentStore::new(),
KvMemoryStore::new(MemoryStorage::new()),
);
builder.model = primary_model;
builder.memory_model = memory_model;
builder.fallback_models = fallback_models;
builder
}
}
impl<D: DocumentStore + 'static, M: MemoryStore + 'static> AgentSession<D, M> {
#[deprecated(
note = "use AgentSession::builder(router).with_session_id(id), plus with_doc_store / with_memory_store for other stores"
)]
#[must_use]
pub fn builder_for(session_id: SessionId, router: ProviderRouter) -> AgentSessionBuilder<D, M>
where
D: Default,
M: Default,
{
AgentSessionBuilder::new(router, D::default(), M::default()).with_session_id(session_id)
}
#[deprecated(
note = "use AgentSession::builder(router).with_session_id(id).with_doc_store(doc_store).with_memory_store(memory_store)"
)]
#[must_use]
pub fn builder_with_stores(
session_id: SessionId,
router: ProviderRouter,
doc_store: D,
memory_store: M,
) -> AgentSessionBuilder<D, M> {
AgentSessionBuilder::new(router, doc_store, memory_store).with_session_id(session_id)
}
}
impl<D: DocumentStore + 'static, M: MemoryStore + 'static> AgentSessionBuilder<D, M> {
fn new(router: ProviderRouter, doc_store: D, memory_store: M) -> Self {
Self {
session_id: SessionId::new(),
require_existing: false,
router,
toolshed: ToolShed::new(),
access: Arc::new(AllowAllAccess),
user: UserId("local".into()),
model: None,
fallback_models: Vec::new(),
memory_model: None,
doc_store,
memory_store,
system_prompt: None,
policy: Some(ErrorPolicy::new()),
config: AgentConfig::default(),
context_config: ContextConfig::default(),
memory_config: MemoryConfig::default(),
embedder: None,
}
}
fn map_stores<D2, M2>(self, f: impl FnOnce(D, M) -> (D2, M2)) -> AgentSessionBuilder<D2, M2> {
let (doc_store, memory_store) = f(self.doc_store, self.memory_store);
AgentSessionBuilder {
session_id: self.session_id,
require_existing: self.require_existing,
router: self.router,
toolshed: self.toolshed,
access: self.access,
user: self.user,
policy: self.policy,
model: self.model,
fallback_models: self.fallback_models,
memory_model: self.memory_model,
doc_store,
memory_store,
system_prompt: self.system_prompt,
config: self.config,
context_config: self.context_config,
memory_config: self.memory_config,
embedder: self.embedder,
}
}
#[must_use]
pub fn with_session_id(mut self, id: SessionId) -> Self {
self.session_id = id;
self
}
#[must_use]
pub fn resume(mut self, id: SessionId) -> Self {
self.session_id = id;
self.require_existing = true;
self
}
#[must_use]
pub fn with_doc_store<D2: DocumentStore + 'static>(
self,
store: D2,
) -> AgentSessionBuilder<D2, M> {
self.map_stores(|_, memory_store| (store, memory_store))
}
#[must_use]
pub fn with_memory_store<M2: MemoryStore + 'static>(
self,
store: M2,
) -> AgentSessionBuilder<D, M2> {
self.map_stores(|doc_store, _| (doc_store, store))
}
#[must_use]
pub fn with_toolshed(mut self, toolshed: ToolShed) -> Self {
self.toolshed = toolshed;
self
}
#[must_use]
pub fn with_access(mut self, access: Arc<dyn SessionAccessProvider>) -> Self {
self.access = access;
self
}
#[must_use]
pub fn with_user(mut self, user: UserId) -> Self {
self.user = user;
self
}
#[must_use]
pub fn with_model(mut self, model: impl Into<ModelId>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn with_fallback_models<I>(mut self, models: I) -> Self
where
I: IntoIterator,
I::Item: Into<ModelId>,
{
self.fallback_models = models.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_memory_model(mut self, model: impl Into<ModelId>) -> Self {
self.memory_model = Some(model.into());
self
}
#[must_use]
pub fn with_error_policy(mut self, policy: ErrorPolicy) -> Self {
self.policy = Some(policy);
self
}
#[must_use]
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
#[must_use]
pub fn with_config(mut self, config: AgentConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn with_embedder(
mut self,
embedder: Arc<dyn crate::agentic::embedding::EmbeddingProvider>,
embedding_model: impl Into<String>,
) -> Self {
self.embedder = Some((embedder, embedding_model.into()));
self
}
#[must_use]
pub fn with_context_config(mut self, config: ContextConfig) -> Self {
self.context_config = config;
self
}
#[must_use]
pub fn with_memory_config(mut self, config: MemoryConfig) -> Self {
self.memory_config = config;
self
}
pub fn build(self) -> Result<AgentSession<D, M>, ErrorTrace<AgenticError>> {
let Some(primary) = self.model else {
return Err(ErrorTrace::new(AgenticError::Session(
"no model set".into(),
)));
};
let models = ModelSelection {
primary,
fallbacks: self.fallback_models,
memory: self.memory_model,
};
let mut memory_config = self.memory_config;
if let Some(memory) = &models.memory {
memory_config.memory_model = Some(memory.clone());
}
let session_id = self.session_id;
let doc_store = Arc::new(self.doc_store);
let memory_store = Arc::new(self.memory_store);
let ledger = TokenLedger::new();
let message_api = MessageApi::from_shared(session_id.clone(), Arc::clone(&doc_store));
if self.require_existing {
let existing = message_api.recent(1).map_err(|e| {
ErrorTrace::new(AgenticError::MessageStore(format!(
"reading session {session_id} to resume it: {e}"
)))
})?;
if existing.is_empty() {
return Err(ErrorTrace::new(AgenticError::SessionNotFound(session_id)));
}
}
let embedder = self.embedder;
let context_provider = Arc::new({
let cp = ContextProvider::new(
session_id.clone(),
message_api.clone(),
Arc::clone(&memory_store),
ledger.clone(),
self.system_prompt,
self.context_config,
);
match &embedder {
Some((embedder, model)) => cp.with_embedder(Arc::clone(embedder), model.clone()),
None => cp,
}
});
let queues = SteeringQueues::new();
let coordinator = MemoryCoordinator::from_shared(memory_store, doc_store);
let memory = Arc::new(MemoryHierarchy::new(
session_id.clone(),
coordinator,
ledger.clone(),
memory_config,
));
let parts = SessionParts {
session_id: session_id.clone(),
context: Arc::clone(&context_provider) as _,
memory: Arc::clone(&memory) as _,
embedder,
};
let tool_manager = self
.toolshed
.build(&parts)
.map_err(|e| ErrorTrace::new(AgenticError::from(e)))?;
let inner = SessionInner {
session_id,
policy: self.policy.unwrap_or_default(),
router: self.router,
tool_manager,
queues,
memory,
message_api,
ledger,
context_provider,
access: self.access,
user: self.user,
config: self.config,
models,
};
inner.preflight()?;
Ok(AgentSession {
inner: Arc::new(inner),
})
}
}
impl<D: DocumentStore, M: MemoryStore> SessionInner<D, M> {
fn preflight(&self) -> Result<(), ErrorTrace<AgenticError>> {
if !self
.access
.can_access_session(&self.user, &self.session_id)
.map_err(|e| ErrorTrace::new(AgenticError::Auth(e)))?
{
return Err(ErrorTrace::new(AgenticError::Session(
"access denied: cannot access session".into(),
)));
}
let model_name = self.models.primary.name();
if !self
.access
.can_use_model(&self.user, model_name)
.map_err(|e| ErrorTrace::new(AgenticError::Auth(e)))?
{
return Err(ErrorTrace::new(AgenticError::Session(format!(
"access denied: cannot use model '{model_name}'"
))));
}
let budget = self
.access
.token_budget(&self.user)
.map_err(|e| ErrorTrace::new(AgenticError::Auth(e)))?;
self.ledger.set_budget(budget.remaining());
Ok(())
}
}
impl<D: DocumentStore + 'static, M: MemoryStore + 'static> AgentSession<D, M> {
#[must_use]
pub fn session_id(&self) -> &SessionId {
&self.inner.session_id
}
#[must_use]
pub fn models(&self) -> &ModelSelection {
&self.inner.models
}
#[must_use]
pub fn message_api(&self) -> &MessageApi<D> {
&self.inner.message_api
}
#[must_use]
pub fn ledger(&self) -> &TokenLedger {
&self.inner.ledger
}
#[must_use]
pub fn steering_queues(&self) -> &SteeringQueues {
&self.inner.queues
}
#[must_use]
pub fn router(&self) -> &ProviderRouter {
&self.inner.router
}
#[must_use]
pub fn tool_manager(&self) -> &ToolCallManager {
&self.inner.tool_manager
}
#[must_use]
pub fn memory_hierarchy(&self) -> &MemoryHierarchy<M, D> {
&self.inner.memory
}
#[must_use]
pub fn context_provider(&self) -> &ContextProvider<D, M> {
&self.inner.context_provider
}
pub fn run_turn_stream(
&self,
prompt: impl Into<Messages>,
) -> Result<TurnStream<D, M>, ErrorTrace<AgenticError>> {
self.inner.queues.push_follow_up(prompt.into());
tracing::trace!(
follow_up_len = self.inner.queues.follow_up.len(),
"run_turn_stream: queued prompt for the loop"
);
let agent_loop = AgentLoop::new(
self.inner.session_id.clone(),
(*self.inner.context_provider).clone(),
self.inner.tool_manager.clone(),
SteeringQueues::from_shared(
self.inner.queues.priority.clone(),
self.inner.queues.follow_up.clone(),
self.inner.queues.cancel_signal.clone(),
),
(*self.inner.memory).clone(),
self.inner.message_api.clone(),
self.inner.ledger.clone(),
self.inner.policy.clone(),
self.inner.router.clone(),
self.inner.config.clone(),
self.inner.models.clone(),
)
.with_access(Arc::clone(&self.inner.access), self.inner.user.clone());
execute(agent_loop, None).map(TurnStream::new).map_err(|e| {
ErrorTrace::new(AgenticError::Session(format!(
"failed to schedule agent loop: {e}"
)))
})
}
pub fn run_turn(&self, prompt: impl Into<Messages>) -> Result<Turn, ErrorTrace<AgenticError>> {
Ok(Turn::collect(self.run_turn_stream(prompt)?))
}
pub fn ask(&self, prompt: impl Into<Messages>) -> Result<Answer, ErrorTrace<AgenticError>> {
Ok(Answer::collect(self.run_turn_stream(prompt)?))
}
pub fn steer(&self, msg: impl Into<Messages>) {
self.inner.queues.push_priority(msg.into());
}
pub fn follow_up(&self, msg: impl Into<Messages>) {
self.inner.queues.push_follow_up(msg.into());
}
pub fn abort(&self) {
self.inner.queues.abort();
}
pub fn end(&self) -> Result<(), ErrorTrace<AgenticError>> {
let priority_msgs = self.inner.queues.drain_priority();
let follow_up_msgs = self.inner.queues.drain_follow_up();
for msg in priority_msgs.into_iter().chain(follow_up_msgs) {
let _id = self
.inner
.message_api
.append(SessionRecord::Conversation { message: msg });
}
let flushed = self.inner.message_api.flush().map_err(|e| {
ErrorTrace::new(AgenticError::MessageStore(format!(
"ending session {}: flushing the message log: {e}",
self.inner.session_id
)))
});
self.inner.queues.reset_cancel();
flushed.map(|_| ())
}
}