use std::path::{Path, PathBuf};
use tokio::sync::mpsc;
use crate::approve::Approver;
use crate::context::{bound, entry_cap_chars, ObsKind};
use crate::error::{Error, Result};
use crate::observe::{Ignore, Observer};
use crate::policy::Policy;
use crate::provider::Provider;
use crate::run::{RunOutcome, TurnExtras, NO_TOOL_CALL};
use crate::state::{Store, Turn};
use crate::verify::Verification;
use crate::TaskContract;
#[derive(Debug, Clone)]
pub struct Session {
id: i64,
root: PathBuf,
head: Option<i64>,
}
impl Session {
pub fn open(store: &Store, root: impl AsRef<Path>) -> Result<Self> {
let root = root.as_ref().to_path_buf();
let id = store.create_session(&root.display().to_string())?;
Ok(Self {
id,
root,
head: None,
})
}
pub fn reopen(store: &Store, id: i64) -> Result<Self> {
let Some(root) = store.session_root(id)? else {
return Err(Error::Config(format!(
"no session {id} in this store; the id must come from Session::id() \
on a session opened against the same database"
)));
};
Ok(Self {
id,
root: PathBuf::from(root),
head: store.session_head(id)?,
})
}
pub fn id(&self) -> i64 {
self.id
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn head(&self) -> Option<i64> {
self.head
}
pub fn history(&self, store: &Store) -> Result<Vec<Turn>> {
let all = store.session_turns(self.id)?;
let mut path = Vec::new();
let mut at = self.head;
while let Some(id) = at {
let Some(turn) = all.iter().find(|t| t.id == id) else {
break;
};
path.push(turn.clone());
at = turn.parent_turn_id;
if path.len() > all.len() {
break;
}
}
path.reverse();
Ok(path)
}
pub fn branch_from(&mut self, store: &Store, turn_id: i64) -> Result<()> {
let Some(turn) = store.session_turn(turn_id)? else {
return Err(Error::Config(format!("no turn {turn_id} in this store")));
};
if turn.session_id != self.id {
return Err(Error::Config(format!(
"turn {turn_id} belongs to session {}, not {}",
turn.session_id, self.id
)));
}
self.head = Some(turn_id);
store.set_session_head(self.id, self.head)?;
Ok(())
}
pub async fn turn<P: Provider>(
&mut self,
text: impl Into<String>,
provider: &P,
store: &Store,
policy: &Policy,
approver: &dyn Approver,
) -> Result<TurnResult> {
let contract = self.default_contract(text);
self.drive(
&contract,
provider,
store,
policy,
approver,
&Ignore,
TurnExtras::default(),
)
.await
}
pub async fn turn_observed<P: Provider>(
&mut self,
text: impl Into<String>,
provider: &P,
store: &Store,
policy: &Policy,
approver: &dyn Approver,
observer: &dyn Observer,
) -> Result<TurnResult> {
let contract = self.default_contract(text);
self.drive(
&contract,
provider,
store,
policy,
approver,
observer,
TurnExtras {
stream: true,
..Default::default()
},
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn turn_steered<P: Provider>(
&mut self,
text: impl Into<String>,
provider: &P,
store: &Store,
policy: &Policy,
approver: &dyn Approver,
observer: &dyn Observer,
steer: &SteerInbox,
) -> Result<TurnResult> {
let contract = self.default_contract(text);
self.drive(
&contract,
provider,
store,
policy,
approver,
observer,
TurnExtras {
stream: true,
steer: Some(steer),
..Default::default()
},
)
.await
}
pub async fn turn_bounded<P: Provider>(
&mut self,
contract: &TaskContract,
provider: &P,
store: &Store,
policy: &Policy,
approver: &dyn Approver,
) -> Result<TurnResult> {
let contract = self.rooted(contract);
self.drive(
&contract,
provider,
store,
policy,
approver,
&Ignore,
TurnExtras::default(),
)
.await
}
pub async fn turn_bounded_observed<P: Provider>(
&mut self,
contract: &TaskContract,
provider: &P,
store: &Store,
policy: &Policy,
approver: &dyn Approver,
observer: &dyn Observer,
) -> Result<TurnResult> {
let contract = self.rooted(contract);
self.drive(
&contract,
provider,
store,
policy,
approver,
observer,
TurnExtras {
stream: true,
..Default::default()
},
)
.await
}
fn default_contract(&self, text: impl Into<String>) -> TaskContract {
TaskContract::workspace(text, self.root.clone(), Verification::None)
}
fn rooted(&self, contract: &TaskContract) -> TaskContract {
let mut contract = contract.clone();
contract.root = Some(self.root.clone());
contract.file = self.root.clone();
contract
}
#[allow(clippy::too_many_arguments)]
async fn drive<P: Provider>(
&mut self,
contract: &TaskContract,
provider: &P,
store: &Store,
policy: &Policy,
approver: &dyn Approver,
observer: &dyn Observer,
mut extras: TurnExtras<'_>,
) -> Result<TurnResult> {
let seed = self.seed(store, contract)?;
extras.seed = &seed;
extras.turn = Some(SessionTurn {
session_id: self.id,
parent_turn_id: self.head,
prompt: &contract.goal,
});
let result = crate::run::run_with_extras(
contract, provider, store, policy, approver, observer, &extras,
)
.await?;
let turn_id = store
.turn_for_run(result.run_id)?
.ok_or_else(|| Error::Config(format!("run {} recorded no turn", result.run_id)))?;
let reply = last_message(store, result.run_id)?;
let outcome = store
.run_summary(result.run_id)?
.map(|s| s.outcome)
.unwrap_or_else(|| "running".into());
store.finish_turn(turn_id, reply.as_deref(), &outcome)?;
self.head = Some(turn_id);
store.set_session_head(self.id, self.head)?;
Ok(TurnResult {
turn_id,
run_id: result.run_id,
outcome: result.outcome,
reply,
})
}
fn seed(&self, store: &Store, contract: &TaskContract) -> Result<Vec<String>> {
let cap = entry_cap_chars(contract.context.effective_tokens(contract.max_tokens));
let mut out = Vec::new();
for turn in self.history(store)? {
out.push(bound(
&format!("\n[earlier turn] the operator asked: {}\n", turn.prompt),
cap,
ObsKind::Message,
));
if let Some(reply) = turn.reply.as_deref().filter(|r| !r.is_empty()) {
out.push(bound(
&format!("\n[earlier turn] you answered: {reply}\n"),
cap,
ObsKind::Message,
));
}
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct TurnResult {
pub turn_id: i64,
pub run_id: i64,
pub outcome: RunOutcome,
pub reply: Option<String>,
}
pub(crate) struct SessionTurn<'a> {
pub session_id: i64,
pub parent_turn_id: Option<i64>,
pub prompt: &'a str,
}
#[derive(Debug, Clone)]
pub struct Steer {
tx: mpsc::UnboundedSender<Steered>,
}
#[derive(Debug)]
pub struct SteerInbox {
rx: std::cell::RefCell<mpsc::UnboundedReceiver<Steered>>,
}
#[derive(Debug, Clone)]
enum Steered {
Say(String),
Interrupt,
}
impl Steer {
pub fn channel() -> (Steer, SteerInbox) {
let (tx, rx) = mpsc::unbounded_channel();
(
Steer { tx },
SteerInbox {
rx: std::cell::RefCell::new(rx),
},
)
}
pub fn say(&self, text: impl Into<String>) -> Result<()> {
self.send(Steered::Say(text.into()))
}
pub fn interrupt(&self) -> Result<()> {
self.send(Steered::Interrupt)
}
fn send(&self, message: Steered) -> Result<()> {
self.tx.send(message).map_err(|_| {
Error::Config(
"this turn has ended, so nothing will read the steer; start another turn".into(),
)
})
}
}
impl SteerInbox {
pub fn pending(&self) -> (Vec<String>, bool) {
let drained = self.drain();
(drained.messages, drained.interrupted)
}
pub(crate) fn drain(&self) -> Drained {
let mut out = Drained::default();
let mut rx = self.rx.borrow_mut();
while let Ok(message) = rx.try_recv() {
match message {
Steered::Say(text) => out.messages.push(text),
Steered::Interrupt => out.interrupted = true,
}
}
out
}
}
#[derive(Debug, Default)]
pub(crate) struct Drained {
pub messages: Vec<String>,
pub interrupted: bool,
}
fn last_message(store: &Store, run_id: i64) -> Result<Option<String>> {
for obs in store.observations(run_id)?.into_iter().rev() {
if obs.kind != ObsKind::Message {
continue;
}
if let Some((_, said)) = obs.text.split_once(NO_TOOL_CALL) {
let said = said.trim();
return Ok((!said.is_empty()).then(|| said.to_string()));
}
}
Ok(None)
}