use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use crate::message::AgentEvent;
use crate::message::Message;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SessionOp {
Submit { message: String },
PreviewRequest { message: String },
SetSkillContext {
name: Option<String>,
content: Option<String>,
},
Interrupt,
Shutdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum SessionEvent {
AgentEvent {
event: AgentEvent,
},
ApprovalRequired {
tool_name: String,
arguments: String,
call_id: String,
},
TurnStarted { turn_id: String },
TurnCompleted {
turn_id: String,
status: TurnCompletionStatus,
},
Error { message: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum TurnCompletionStatus {
Success {
#[serde(default)]
final_text: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
new_messages: Vec<crate::message::Message>,
},
Cancelled,
Error {
message: String,
},
}
pub struct SessionHandle {
pub sq_tx: mpsc::Sender<SessionOp>,
pub eq_rx: mpsc::UnboundedReceiver<SessionEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionConfig {
#[serde(default)]
pub runtime_policy: RuntimePolicy,
pub workspace_root: PathBuf,
#[serde(default)]
pub initial_history: Vec<Message>,
#[serde(default = "default_model_context_limit")]
pub model_context_limit: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct RuntimePolicy {
pub approval_mode: ApprovalMode,
}
impl RuntimePolicy {
#[must_use]
pub fn interactive() -> Self {
Self {
approval_mode: ApprovalMode::Interactive,
}
}
#[must_use]
pub fn headless_deny() -> Self {
Self {
approval_mode: ApprovalMode::HeadlessDeny,
}
}
}
impl Default for RuntimePolicy {
fn default() -> Self {
Self::interactive()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalMode {
#[default]
Interactive,
HeadlessDeny,
}
fn default_model_context_limit() -> u32 {
128_000
}
#[cfg(test)]
#[allow(warnings)]
#[allow(warnings)]
#[allow(warnings)]
#[allow(warnings)]
mod tests {
use super::*;
#[test]
fn session_op_serde_roundtrip() {
let ops = vec![
SessionOp::Submit {
message: "hello".into(),
},
SessionOp::PreviewRequest {
message: "diagnostic".into(),
},
SessionOp::Interrupt,
SessionOp::Shutdown,
];
for op in &ops {
let json = serde_json::to_string(op).unwrap();
let back: SessionOp = serde_json::from_str(&json).unwrap();
assert_eq!(
serde_json::to_value(op).unwrap(),
serde_json::to_value(&back).unwrap()
);
}
}
#[test]
fn session_event_serde_roundtrip() {
let events = vec![
SessionEvent::AgentEvent {
event: AgentEvent::TextDelta {
delta: "hello".into(),
},
},
SessionEvent::ApprovalRequired {
tool_name: "write".into(),
arguments: "{}".into(),
call_id: "call_1".into(),
},
SessionEvent::TurnStarted {
turn_id: "1".into(),
},
SessionEvent::TurnCompleted {
turn_id: "1".into(),
status: TurnCompletionStatus::Success {
final_text: String::new(),
new_messages: vec![],
},
},
SessionEvent::TurnCompleted {
turn_id: "2".into(),
status: TurnCompletionStatus::Cancelled,
},
SessionEvent::TurnCompleted {
turn_id: "3".into(),
status: TurnCompletionStatus::Error {
message: "boom".into(),
},
},
SessionEvent::Error {
message: "fail".into(),
},
];
for event in &events {
let json = serde_json::to_string(event).unwrap();
let back: SessionEvent = serde_json::from_str(&json).unwrap();
assert_eq!(
serde_json::to_value(event).unwrap(),
serde_json::to_value(&back).unwrap()
);
}
}
#[test]
fn session_config_serde_roundtrip() {
let config = SessionConfig {
runtime_policy: RuntimePolicy::headless_deny(),
workspace_root: PathBuf::from("/tmp/test"),
initial_history: vec![],
model_context_limit: 128_000,
};
let json = serde_json::to_string(&config).unwrap();
let back: SessionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.runtime_policy, back.runtime_policy);
assert_eq!(config.workspace_root, back.workspace_root);
assert_eq!(config.initial_history, back.initial_history);
assert_eq!(config.model_context_limit, back.model_context_limit);
}
#[test]
fn session_config_defaults_to_interactive_runtime_policy() {
let json = r#"{
"workspace_root": "/tmp/test",
"initial_history": [],
"model_context_limit": 128000
}"#;
let back: SessionConfig = serde_json::from_str(json).unwrap();
assert_eq!(back.runtime_policy, RuntimePolicy::interactive());
}
}