use std::collections::BTreeMap;
use openquiver_acp::{Agents, Session, SessionHandle};
use openquiver_compile::CompiledNode;
use openquiver_graph::NodeName;
use crate::journal::{Printed, Record};
use crate::{RunValue, RuntimeError};
pub(crate) struct Effects<'a> {
pub agents: &'a dyn Agents,
pub sessions: BTreeMap<(String, String), Box<dyn Session>>,
}
fn key(handle: &SessionHandle) -> (String, String) {
(handle.agent.clone(), handle.session_id.clone())
}
impl Effects<'_> {
async fn session(&mut self, handle: &SessionHandle) -> Result<&mut dyn Session, RuntimeError> {
let key = key(handle);
if !self.sessions.contains_key(&key) {
let session = self.agents.resume_session(handle).await?;
self.sessions.insert(key.clone(), session);
}
Ok(self.sessions.get_mut(&key).expect("just inserted").as_mut())
}
}
pub(crate) struct Outcome {
pub outputs: BTreeMap<String, RunValue>,
pub exec: Option<&'static str>,
pub records: Vec<Record>,
}
impl Outcome {
fn goes_to(pin: &'static str) -> Self {
Self {
outputs: BTreeMap::new(),
exec: Some(pin),
records: Vec::new(),
}
}
fn with_output(mut self, pin: &str, value: RunValue) -> Self {
self.outputs.insert(pin.to_owned(), value);
self
}
fn with_record(mut self, record: Record) -> Self {
self.records.push(record);
self
}
}
pub(crate) async fn perform(
effects: &mut Effects<'_>,
name: &NodeName,
node: &CompiledNode,
inputs: &BTreeMap<String, RunValue>,
) -> Result<Outcome, RuntimeError> {
let at = Pins { name, inputs };
Ok(match node.type_id.as_str() {
"event.manual" => Outcome::goes_to("out"),
"acp.connect_agent" => {
Outcome::goes_to("out").with_output("agent", RunValue::Agent(at.text("agent")?))
}
"acp.new_session" => {
let agent = at.agent("agent")?;
let cwd = at.optional_text("cwd")?.unwrap_or_else(|| ".".to_owned());
let session = effects.agents.new_session(&agent, &cwd).await?;
let handle = session.handle();
effects.sessions.insert(key(&handle), session);
Outcome::goes_to("out").with_output("session", RunValue::Session(handle))
}
"acp.prompt" => {
let handle = at.session("session")?;
let text = at.text("text")?;
let turn = effects.session(&handle).await?.prompt(&text).await?;
Outcome::goes_to(turn.stop_reason.pin()).with_output("text", RunValue::Text(turn.text))
}
"io.print" => Outcome::goes_to("out").with_record(Record::Printed(Printed {
text: at.text("value")?,
})),
"flow.branch" => Outcome::goes_to(if at.bool("condition")? {
"true"
} else {
"false"
}),
other => return Err(RuntimeError::NoSuchNodeType(other.to_owned())),
})
}
struct Pins<'a> {
name: &'a NodeName,
inputs: &'a BTreeMap<String, RunValue>,
}
impl Pins<'_> {
fn get(&self, pin: &str) -> Result<&RunValue, RuntimeError> {
self.inputs.get(pin).ok_or_else(|| RuntimeError::NoInput {
node: self.name.as_str().to_owned(),
pin: pin.to_owned(),
})
}
fn wrong(&self, pin: &str, expected: &'static str) -> RuntimeError {
RuntimeError::WrongValue {
node: self.name.as_str().to_owned(),
pin: pin.to_owned(),
expected,
}
}
fn text(&self, pin: &str) -> Result<String, RuntimeError> {
self.get(pin)?
.as_text()
.ok_or_else(|| self.wrong(pin, "text"))
}
fn optional_text(&self, pin: &str) -> Result<Option<String>, RuntimeError> {
match self.inputs.get(pin) {
None => Ok(None),
Some(value) => value
.as_text()
.map(Some)
.ok_or_else(|| self.wrong(pin, "text")),
}
}
fn bool(&self, pin: &str) -> Result<bool, RuntimeError> {
self.get(pin)?
.as_bool()
.ok_or_else(|| self.wrong(pin, "true or false"))
}
fn agent(&self, pin: &str) -> Result<String, RuntimeError> {
self.get(pin)?
.as_agent()
.map(str::to_owned)
.ok_or_else(|| self.wrong(pin, "a connected agent"))
}
fn session(&self, pin: &str) -> Result<SessionHandle, RuntimeError> {
self.get(pin)?
.as_session()
.cloned()
.ok_or_else(|| self.wrong(pin, "an open session"))
}
}