use crate::config::Config;
use crate::session::Session;
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
pub struct LayerResult {
pub outputs: Vec<String>,
pub total_time_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputMode {
Last, All, Summary, }
impl InputMode {
pub fn as_str(&self) -> &'static str {
match self {
InputMode::Last => "last",
InputMode::All => "all",
InputMode::Summary => "summary",
}
}
}
impl FromStr for InputMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"last" => Ok(InputMode::Last),
"all" => Ok(InputMode::All),
"summary" => Ok(InputMode::Summary),
_ => Err(format!(
"Unknown input mode: '{}'. Valid options: last, all, summary",
s
)),
}
}
}
fn deserialize_input_mode<'de, D>(deserializer: D) -> Result<InputMode, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
InputMode::from_str(&s).map_err(D::Error::custom)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputMode {
None, Append, Replace, Last, Restart, }
impl OutputMode {
pub fn as_str(&self) -> &'static str {
match self {
OutputMode::None => "none",
OutputMode::Append => "append",
OutputMode::Replace => "replace",
OutputMode::Last => "last",
OutputMode::Restart => "restart",
}
}
}
impl FromStr for OutputMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" => Ok(OutputMode::None),
"append" => Ok(OutputMode::Append),
"replace" => Ok(OutputMode::Replace),
"last" => Ok(OutputMode::Last),
"restart" => Ok(OutputMode::Restart),
_ => Err(format!(
"Unknown output mode: '{}'. Valid options: none, append, replace, last, restart",
s
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputRole {
Assistant, User, }
impl OutputRole {
pub fn as_str(&self) -> &'static str {
match self {
OutputRole::Assistant => "assistant",
OutputRole::User => "user",
}
}
}
impl FromStr for OutputRole {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"assistant" => Ok(OutputRole::Assistant),
"user" => Ok(OutputRole::User),
_ => Err(format!(
"Unknown output role: '{}'. Valid options: assistant, user",
s
)),
}
}
}
fn deserialize_output_mode<'de, D>(deserializer: D) -> Result<OutputMode, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
OutputMode::from_str(&s).map_err(D::Error::custom)
}
fn deserialize_output_role<'de, D>(deserializer: D) -> Result<OutputRole, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
OutputRole::from_str(&s).map_err(D::Error::custom)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LayerConfig {
pub name: String,
pub description: String,
pub command: String,
#[serde(default = "default_workdir")]
pub workdir: String,
#[serde(deserialize_with = "deserialize_input_mode")]
pub input_mode: InputMode,
#[serde(deserialize_with = "deserialize_output_mode")]
pub output_mode: OutputMode,
#[serde(deserialize_with = "deserialize_output_role")]
pub output_role: OutputRole,
}
fn default_workdir() -> String {
".".to_string()
}
impl LayerConfig {
pub fn get_resolved_workdir(&self, session_workdir: &std::path::Path) -> std::path::PathBuf {
let workdir_path = std::path::PathBuf::from(&self.workdir);
if workdir_path.is_absolute() {
workdir_path
} else {
session_workdir.join(&self.workdir)
}
}
}
#[async_trait]
pub trait Layer {
fn name(&self) -> &str;
fn config(&self) -> &LayerConfig;
async fn process(
&self,
input: &str,
session: &Session,
config: &Config,
operation_cancelled: tokio::sync::watch::Receiver<bool>,
) -> Result<LayerResult>;
fn prepare_input(&self, input: &str, session: &Session) -> String {
match self.config().input_mode {
InputMode::Last => {
if input.trim().is_empty() {
session
.messages
.iter()
.rfind(|m| m.role == "assistant")
.map(|m| m.content.clone())
.unwrap_or_else(|| {
session
.messages
.iter()
.rfind(|m| m.role == "user")
.map(|m| m.content.clone())
.unwrap_or_else(|| "No previous messages found".to_string())
})
} else {
let last_assistant = session
.messages
.iter()
.rfind(|m| m.role == "assistant")
.map(|m| {
format!(
"Previous response:\n{}\n\nCurrent input:\n{}",
m.content, input
)
})
.unwrap_or_else(|| input.to_string());
last_assistant
}
}
InputMode::All => {
let transcript = session
.messages
.iter()
.filter(|m| m.role != "system")
.map(|m| {
let label = match m.role.as_str() {
"assistant" => "Assistant",
"user" => "User",
other => other,
};
format!("[{}]\n{}", label, m.content)
})
.collect::<Vec<_>>()
.join("\n\n");
if transcript.is_empty() {
input.to_string()
} else {
format!("{}\n\n[Current task]\n{}", transcript, input)
}
}
InputMode::Summary => {
crate::session::summarize_context(session, input)
}
}
}
}