use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Serialize};
macro_rules! string_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<T: Into<String>> From<T> for $name {
fn from(value: T) -> Self {
Self(value.into())
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
};
}
pub(crate) use string_id;
string_id!(AgentId); string_id!(ConfigId); string_id!(ResumeToken);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum InstallationSource {
EnvOverride,
Path,
LoginShellPath,
VersionManager,
KnownLocation,
Pinned,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct AgentInstallation {
pub id: AgentId,
pub name: String,
pub executable_path: PathBuf,
pub source: InstallationSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upgrade: Option<crate::runtime::MissingAgent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) acp_args: Option<Vec<String>>,
}
impl AgentInstallation {
pub fn at(id: impl Into<AgentId>, executable: impl Into<PathBuf>) -> Self {
let id = id.into();
Self {
name: id.to_string(),
id,
executable_path: executable.into(),
source: InstallationSource::Pinned,
upgrade: None,
acp_args: None,
}
}
pub fn acp(name: impl Into<String>, executable: impl Into<PathBuf>, args: Vec<String>) -> Self {
let name = name.into();
Self {
id: AgentId::new(&name),
name,
executable_path: executable.into(),
source: InstallationSource::Pinned,
upgrade: None,
acp_args: Some(args),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum AuthStatus {
Authenticated {
kind: AuthKind,
account: Option<AccountInfo>,
},
Unauthenticated {
login: Vec<LoginMethod>,
},
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum AuthKind {
Subscription,
ApiKey,
CloudProvider,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct AccountInfo {
pub email: Option<String>,
pub plan: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum LoginMethod {
Terminal {
command: Vec<String>,
env: BTreeMap<String, String>,
description: String,
},
EnvVar { name: String },
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum Capability {
Images,
Resume,
Steer,
Permissions,
Questions,
Rollback,
RollbackFiles,
Fork,
Compact,
SlashCommands,
Plan,
Subagents,
ContextUsage,
PlanUsage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum RollbackScope {
Conversation,
ConversationAndFiles,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum McpTransport {
Stdio,
Http,
Sse,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct McpServer {
pub(crate) name: String,
pub(crate) connection: McpConnection,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub(crate) enum McpConnection {
Stdio {
command: PathBuf,
args: Vec<String>,
env: BTreeMap<String, String>,
},
Http {
url: String,
headers: BTreeMap<String, String>,
},
Sse {
url: String,
headers: BTreeMap<String, String>,
},
}
impl McpServer {
pub fn stdio(
name: impl Into<String>,
command: impl Into<PathBuf>,
args: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
name: name.into(),
connection: McpConnection::Stdio {
command: command.into(),
args: args.into_iter().map(Into::into).collect(),
env: BTreeMap::new(),
},
}
}
pub fn http(name: impl Into<String>, url: impl Into<String>) -> Self {
Self {
name: name.into(),
connection: McpConnection::Http {
url: url.into(),
headers: BTreeMap::new(),
},
}
}
pub fn sse(name: impl Into<String>, url: impl Into<String>) -> Self {
Self {
name: name.into(),
connection: McpConnection::Sse {
url: url.into(),
headers: BTreeMap::new(),
},
}
}
pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
match &mut self.connection {
McpConnection::Stdio { env, .. } => env.insert(key.into(), value.into()),
McpConnection::Http { headers, .. } | McpConnection::Sse { headers, .. } => {
headers.insert(key.into(), value.into())
}
};
self
}
pub(crate) fn transport(&self) -> McpTransport {
match self.connection {
McpConnection::Stdio { .. } => McpTransport::Stdio,
McpConnection::Http { .. } => McpTransport::Http,
McpConnection::Sse { .. } => McpTransport::Sse,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Capabilities {
features: BTreeSet<Capability>,
pub mcp_transports: Vec<McpTransport>,
}
impl Capabilities {
pub fn new(features: impl IntoIterator<Item = Capability>) -> Self {
Self {
features: features.into_iter().collect(),
mcp_transports: Vec::new(),
}
}
pub fn supports(&self, cap: Capability) -> bool {
self.features.contains(&cap)
}
pub fn iter(&self) -> impl Iterator<Item = &Capability> {
self.features.iter()
}
pub(crate) fn add(&mut self, cap: Capability) {
self.features.insert(cap);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ConfigOption {
pub id: ConfigId,
pub name: String,
pub category: Option<String>,
pub kind: ConfigKind,
pub current: Option<ConfigValue>,
pub live: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum ConfigKind {
Select { choices: Vec<ConfigChoice> },
Boolean,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ConfigChoice {
pub value: String,
pub label: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum ConfigValue {
Text(String),
Bool(bool),
}
impl From<&str> for ConfigValue {
fn from(value: &str) -> Self {
Self::Text(value.to_owned())
}
}
impl From<String> for ConfigValue {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<bool> for ConfigValue {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SlashCommand {
pub name: String,
pub description: String,
pub input_hint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct AgentDetails {
pub version: Option<String>,
pub auth: AuthStatus,
pub capabilities: Capabilities,
pub config_options: Vec<ConfigOption>,
pub commands: Vec<SlashCommand>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SessionConfiguration {
pub options: BTreeMap<ConfigId, ConfigValue>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum PermissionMode {
Ask,
AutoApprove,
}
#[derive(Debug, Clone)]
pub struct SessionOptions {
pub(crate) cwd: PathBuf,
pub(crate) start: SessionStart,
pub(crate) permission_mode: PermissionMode,
pub(crate) no_tools: bool,
pub(crate) throwaway: bool,
pub(crate) quiet_window: Option<Duration>,
pub(crate) stall_after: Option<Duration>,
pub(crate) mcp_servers: Vec<McpServer>,
pub(crate) configure: Vec<(ConfigId, ConfigValue)>,
pub(crate) config_home: Option<PathBuf>,
pub(crate) record_wire: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub(crate) enum SessionStart {
New,
Resume(ResumeToken),
Fork {
from: ResumeToken,
at: Option<crate::event::MessageId>,
},
}
impl SessionOptions {
pub fn in_dir(cwd: impl Into<PathBuf>) -> Self {
let cwd = cwd.into();
Self {
cwd: std::path::absolute(&cwd).unwrap_or(cwd),
start: SessionStart::New,
permission_mode: PermissionMode::Ask,
no_tools: false,
throwaway: false,
quiet_window: None,
stall_after: None,
mcp_servers: Vec::new(),
configure: Vec::new(),
config_home: None,
record_wire: None,
}
}
pub fn configure(mut self, id: impl Into<ConfigId>, value: impl Into<ConfigValue>) -> Self {
self.configure.push((id.into(), value.into()));
self
}
pub fn resume(mut self, token: ResumeToken) -> Self {
self.start = SessionStart::Resume(token);
self
}
pub fn fork_from(mut self, token: ResumeToken, at: Option<crate::event::MessageId>) -> Self {
self.start = SessionStart::Fork { from: token, at };
self
}
pub fn mcp_server(mut self, server: McpServer) -> Self {
self.mcp_servers.push(server);
self
}
pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
self.permission_mode = mode;
self
}
pub fn config_home(mut self, dir: impl Into<PathBuf>) -> Self {
self.config_home = Some(dir.into());
self
}
pub fn record_wire(mut self, path: impl Into<PathBuf>) -> Self {
self.record_wire = Some(path.into());
self
}
pub fn quiet_window(mut self, window: Duration) -> Self {
self.quiet_window = Some(window);
self
}
pub fn stall_after(mut self, after: Duration) -> Self {
self.stall_after = Some(after);
self
}
pub fn cwd(&self) -> &PathBuf {
&self.cwd
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Input {
pub(crate) text: String,
pub(crate) attachments: Vec<PathBuf>,
}
impl Input {
pub fn text(text: impl Into<String>) -> Self {
Self {
text: text.into(),
attachments: Vec::new(),
}
}
pub fn attach(mut self, path: impl Into<PathBuf>) -> Self {
self.attachments.push(path.into());
self
}
pub fn as_text(&self) -> &str {
&self.text
}
}
impl From<&str> for Input {
fn from(text: &str) -> Self {
Self::text(text)
}
}
impl From<String> for Input {
fn from(text: String) -> Self {
Self::text(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_dir_makes_cwd_absolute() {
assert_eq!(
SessionOptions::in_dir(".").cwd(),
&std::env::current_dir().unwrap()
);
let abs = std::env::temp_dir();
assert_eq!(SessionOptions::in_dir(&abs).cwd(), &abs);
}
}