use std::cell::{Cell, RefCell};
use std::rc::Rc;
use futures::future::{AbortHandle, Abortable};
use perspective_js::utils::{ApiError, ApiResult};
use super::runtime::AgentRuntime;
use crate::utils::PubSub;
#[derive(Clone, PartialEq)]
pub enum ChatEntry {
User(String),
Assistant {
text: String,
reasoning: Option<String>,
},
Tool {
name: String,
args: String,
error: Option<String>,
},
Error(String),
}
type PendingTail = (String, String);
#[derive(Clone, Default)]
pub struct AgentSlot {
runtime: Rc<RefCell<Option<Rc<AgentRuntime>>>>,
transcript: Rc<RefCell<Vec<ChatEntry>>>,
pending: Rc<RefCell<Option<PendingTail>>>,
busy: Rc<Cell<bool>>,
abort: Rc<RefCell<Option<AbortHandle>>>,
pub on_update: Rc<PubSub<()>>,
}
impl PartialEq for AgentSlot {
fn eq(&self, rhs: &Self) -> bool {
Rc::ptr_eq(&self.runtime, &rhs.runtime)
}
}
impl AgentSlot {
pub fn configure(&self, runtime: AgentRuntime) {
self.abort_in_flight();
*self.runtime.borrow_mut() = Some(Rc::new(runtime));
self.transcript.borrow_mut().clear();
*self.pending.borrow_mut() = None;
self.on_update.emit(());
}
pub fn is_configured(&self) -> bool {
self.runtime.borrow().is_some()
}
pub fn is_busy(&self) -> bool {
self.busy.get()
}
pub fn label(&self) -> Option<String> {
self.runtime.borrow().as_ref().map(|x| x.label())
}
pub fn transcript(&self) -> Vec<ChatEntry> {
self.transcript.borrow().clone()
}
pub fn record_tool(&self, name: &str, args: &serde_json::Value) {
self.transcript.borrow_mut().push(ChatEntry::Tool {
name: name.to_owned(),
args: args.to_string(),
error: None,
});
self.on_update.emit(());
}
pub fn record_tool_error(&self, message: &str) {
if let Some(ChatEntry::Tool { error, .. }) = self
.transcript
.borrow_mut()
.iter_mut()
.rev()
.find(|x| matches!(x, ChatEntry::Tool { .. }))
{
*error = Some(message.to_owned());
}
self.on_update.emit(());
}
pub fn pending(&self) -> Option<(String, String)> {
self.pending.borrow().clone()
}
pub async fn run_prompt(&self, prompt: String) -> ApiResult<String> {
let runtime = self
.runtime
.borrow()
.clone()
.ok_or_else(|| ApiError::from("`agentConfig()` has not been called"))?;
if self.busy.replace(true) {
return Err(ApiError::from("A prompt is already running"));
}
let (handle, registration) = AbortHandle::new_pair();
*self.abort.borrow_mut() = Some(handle);
self.transcript
.borrow_mut()
.push(ChatEntry::User(prompt.clone()));
self.on_update.emit(());
let on_delta = {
let pending = self.pending.clone();
let on_update = self.on_update.clone();
move |text: &str, reasoning: &str| {
*pending.borrow_mut() = Some((text.to_owned(), reasoning.to_owned()));
on_update.emit(());
}
};
let result = Abortable::new(runtime.prompt(prompt, &on_delta), registration).await;
*self.abort.borrow_mut() = None;
self.busy.set(false);
let reasoning = self
.pending
.borrow_mut()
.take()
.map(|(_, x)| x)
.filter(|x| !x.is_empty());
let result = match result {
Ok(Ok(text)) => {
self.transcript.borrow_mut().push(ChatEntry::Assistant {
text: text.clone(),
reasoning,
});
Ok(text)
},
Ok(Err(err)) => {
self.transcript
.borrow_mut()
.push(ChatEntry::Error(format!("{err}")));
Err(err)
},
Err(futures::future::Aborted) => {
self.transcript
.borrow_mut()
.push(ChatEntry::Error("Stopped".to_owned()));
Err(ApiError::from("Stopped"))
},
};
self.on_update.emit(());
result
}
pub fn stop(&self) {
self.abort_in_flight();
}
pub async fn reset(&self) {
self.abort_in_flight();
let runtime = self.runtime.borrow().clone();
if let Some(runtime) = runtime {
runtime.reset().await;
}
self.transcript.borrow_mut().clear();
*self.pending.borrow_mut() = None;
self.on_update.emit(());
}
fn abort_in_flight(&self) {
if let Some(handle) = self.abort.borrow_mut().take() {
handle.abort();
}
}
}