use crate::repository::{ExportedMessage, ExportedRepository, MessageEntry, MessageRepository};
use crate::runtime::{
ComposerHost, RunFuture, RuntimeCapabilities, RuntimeError, StartRunConfig, ThreadRuntime,
ToolResult,
};
use crate::subscribe::{Subscribers, Subscription};
use crate::types::{AppendMessage, MessageId, SubmittedFeedback, ThreadMessage};
use crate::{adapter::RunError, Composer};
use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::rc::{Rc, Weak};
use std::sync::Arc;
pub type StoreFuture = Pin<Box<dyn Future<Output = Result<(), String>>>>;
pub trait ExternalStore {
fn on_new(&self, message: AppendMessage) -> StoreFuture;
fn on_edit(&self, _message: AppendMessage) -> Option<StoreFuture> {
None
}
fn on_reload(&self, _config: StartRunConfig) -> Option<StoreFuture> {
None
}
fn on_cancel(&self) {}
fn on_add_tool_result(&self, _result: ToolResult) -> Option<StoreFuture> {
None
}
fn on_respond_to_tool_approval(
&self,
_message_id: &MessageId,
_approval_id: &str,
_approved: bool,
_reason: Option<String>,
) -> Option<StoreFuture> {
None
}
fn on_feedback(&self, _message_id: &MessageId, _feedback: Option<&SubmittedFeedback>) {}
fn on_branch_change(&self, _head_id: Option<&MessageId>) {}
fn capabilities(&self) -> RuntimeCapabilities {
RuntimeCapabilities {
switch_to_branch: true,
edit: false,
reload: false,
cancel: false,
copy: true,
feedback: false,
answer_tool_call: false,
}
}
}
struct State {
repository: MessageRepository,
running: bool,
disabled: bool,
}
struct Inner {
me: Weak<Inner>,
store: Rc<dyn ExternalStore>,
state: RefCell<State>,
subscribers: Rc<Subscribers>,
thread_composer: RefCell<Option<Composer>>,
edits: RefCell<HashMap<MessageId, Composer>>,
}
impl Inner {
fn notify(&self) {
self.subscribers.notify();
let composers: Vec<Composer> = self
.thread_composer
.borrow()
.iter()
.cloned()
.chain(self.edits.borrow().values().cloned())
.collect();
for composer in composers {
composer.notify();
}
}
}
#[derive(Clone)]
pub struct ExternalStoreRuntime {
inner: Rc<Inner>,
}
fn into_run(future: StoreFuture) -> RunFuture {
Box::pin(async move { future.await.map_err(RunError::failed) })
}
impl ExternalStoreRuntime {
pub fn new(store: impl ExternalStore + 'static) -> Self {
Self {
inner: Rc::new_cyclic(|me| Inner {
me: me.clone(),
store: Rc::new(store),
state: RefCell::new(State {
repository: MessageRepository::new(),
running: false,
disabled: false,
}),
subscribers: Rc::default(),
thread_composer: RefCell::new(None),
edits: RefCell::new(HashMap::new()),
}),
}
}
pub fn subscribe(&self, callback: impl Fn() + 'static) -> Subscription {
self.inner.subscribers.subscribe(callback)
}
pub fn set_messages<M: Into<Arc<ThreadMessage>>>(&self, messages: impl IntoIterator<Item = M>) {
let mut parent: Option<MessageId> = None;
let items = messages
.into_iter()
.map(|message| {
let message: Arc<ThreadMessage> = message.into();
let item = ExportedMessage {
message: message.clone(),
parent_id: parent.take(),
run_config: None,
};
parent = Some(message.id().clone());
item
})
.collect();
self.set_repository(ExportedRepository {
head_id: None,
messages: items,
});
}
pub fn set_repository(&self, mut exported: ExportedRepository) {
{
let mut state = self.inner.state.borrow_mut();
for item in &mut exported.messages {
if let Ok(existing) = state.repository.get(item.message.id()) {
if Arc::ptr_eq(&existing.message, &item.message)
|| *existing.message == *item.message
{
item.message = existing.message;
}
}
}
let keep_head = exported
.head_id
.is_none()
.then(|| state.repository.head_id().cloned());
state.repository.clear();
let _ = state.repository.import(exported);
if let Some(Some(head)) = keep_head {
if state.repository.contains(&head) {
let _ = state.repository.switch_to_branch(&head);
}
}
}
self.inner.notify();
}
pub fn set_running(&self, running: bool) {
let changed = {
let mut state = self.inner.state.borrow_mut();
std::mem::replace(&mut state.running, running) != running
};
if changed {
self.inner.notify();
}
}
pub fn set_disabled(&self, disabled: bool) {
let changed = {
let mut state = self.inner.state.borrow_mut();
std::mem::replace(&mut state.disabled, disabled) != disabled
};
if changed {
self.inner.notify();
}
}
fn handle(inner: &Rc<Inner>) -> Self {
Self {
inner: inner.clone(),
}
}
}
impl ComposerHost for Inner {
fn last_message_id(&self) -> Option<MessageId> {
self.state
.borrow()
.repository
.messages()
.last()
.map(|m| m.id().clone())
}
fn append(&self, message: AppendMessage) -> Result<RunFuture, RuntimeError> {
let inner = self.me.upgrade().ok_or(RuntimeError::Disposed)?;
ExternalStoreRuntime::handle(&inner).append(message)
}
fn cancel_run(&self) {
self.store.on_cancel();
}
fn is_running(&self) -> bool {
self.state.borrow().running
}
fn end_edit(&self, id: &MessageId) {
self.edits.borrow_mut().remove(id);
self.notify();
}
}
impl ThreadRuntime for ExternalStoreRuntime {
fn messages(&self) -> Arc<[Arc<ThreadMessage>]> {
self.inner.state.borrow().repository.messages()
}
fn message(&self, id: &MessageId) -> Result<MessageEntry, RuntimeError> {
Ok(self.inner.state.borrow().repository.get(id)?)
}
fn is_running(&self) -> bool {
self.inner.state.borrow().running
}
fn capabilities(&self) -> RuntimeCapabilities {
self.inner.store.capabilities()
}
fn branches(&self, id: &MessageId) -> Result<Vec<MessageId>, RuntimeError> {
Ok(self.inner.state.borrow().repository.branches(id)?.to_vec())
}
fn branch_position(&self, id: &MessageId) -> Result<(usize, usize), RuntimeError> {
Ok(self.inner.state.borrow().repository.branch_position(id)?)
}
fn switch_to_branch(&self, id: &MessageId) -> Result<(), RuntimeError> {
let head = {
let mut state = self.inner.state.borrow_mut();
state.repository.switch_to_branch(id)?;
state.repository.head_id().cloned()
};
self.inner.notify();
self.inner.store.on_branch_change(head.as_ref());
Ok(())
}
fn append(&self, message: AppendMessage) -> Result<RunFuture, RuntimeError> {
if self.inner.state.borrow().disabled {
return Err(RuntimeError::Unsupported("sending while disabled"));
}
let editing = message.source_id.is_some();
let edit = editing
.then(|| self.inner.store.on_edit(message.clone()))
.flatten();
Ok(into_run(
edit.unwrap_or_else(|| self.inner.store.on_new(message)),
))
}
fn reload(&self, config: StartRunConfig) -> Result<RunFuture, RuntimeError> {
self.inner
.store
.on_reload(config)
.map(into_run)
.ok_or(RuntimeError::Unsupported("reload"))
}
fn cancel_run(&self) {
self.inner.store.on_cancel();
}
fn add_tool_result(&self, result: ToolResult) -> Result<Option<RunFuture>, RuntimeError> {
self.inner
.store
.on_add_tool_result(result)
.map(|f| Some(into_run(f)))
.ok_or(RuntimeError::Unsupported("tool results"))
}
fn respond_to_tool_approval(
&self,
message_id: &MessageId,
approval_id: &str,
approved: bool,
reason: Option<String>,
) -> Result<Option<RunFuture>, RuntimeError> {
self.inner
.store
.on_respond_to_tool_approval(message_id, approval_id, approved, reason)
.map(|f| Some(into_run(f)))
.ok_or(RuntimeError::Unsupported("tool approvals"))
}
fn submit_feedback(
&self,
id: &MessageId,
feedback: Option<SubmittedFeedback>,
) -> Result<(), RuntimeError> {
if !self.inner.store.capabilities().feedback {
return Err(RuntimeError::Unsupported("feedback"));
}
self.message(id)?;
self.inner.store.on_feedback(id, feedback.as_ref());
Ok(())
}
fn composer(&self) -> Composer {
self.inner
.thread_composer
.borrow_mut()
.get_or_insert_with(|| {
Composer::thread(Rc::downgrade(&self.inner) as Weak<dyn ComposerHost>)
})
.clone()
}
fn begin_edit(&self, id: &MessageId) -> Result<Composer, RuntimeError> {
if self.inner.edits.borrow().contains_key(id) {
return Err(RuntimeError::EditInProgress(id.clone()));
}
let entry = ThreadRuntime::message(self, id)?;
let composer = Composer::edit(
Rc::downgrade(&self.inner) as Weak<dyn ComposerHost>,
entry.parent_id,
&entry.message,
);
self.inner
.edits
.borrow_mut()
.insert(id.clone(), composer.clone());
self.inner.notify();
Ok(composer)
}
fn edit_composer(&self, id: &MessageId) -> Option<Composer> {
self.inner.edits.borrow().get(id).cloned()
}
fn subscribe_dyn(&self, callback: Box<dyn Fn()>) -> Subscription {
self.inner.subscribers.subscribe(callback)
}
fn export(&self) -> ExportedRepository {
self.inner.state.borrow().repository.export()
}
fn import(&self, exported: ExportedRepository) -> Result<(), RuntimeError> {
self.set_repository(exported);
Ok(())
}
}