pub mod cache;
pub mod cancellation; pub mod chat; mod chat_helper; pub mod helper_functions; pub mod history; pub mod image; pub mod layers; pub mod logger; mod model_utils; mod project_context; pub mod report; pub mod smart_summarizer; mod token_counter;
pub use crate::providers::{
AiProvider, ProviderExchange, ProviderFactory, ProviderResponse, TokenUsage,
};
pub use cache::{CacheManager, CacheStatistics};
pub use helper_functions::summarize_context;
pub use layers::{process_with_layers, InputMode, Layer, LayerConfig, LayerMcpConfig, LayerResult};
pub use model_utils::model_supports_caching;
pub use project_context::ProjectContext;
pub use smart_summarizer::SmartSummarizer;
pub use token_counter::{
calculate_minimum_session_tokens, estimate_full_context_tokens, estimate_message_tokens,
estimate_tokens, validate_session_token_threshold,
};
use crate::config::Config;
use crate::providers::ChatCompletionParams;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs::{self as std_fs, File, OpenOptions};
use std::io::Write;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::watch;
pub struct ChatCompletionWithValidationParams<'a> {
pub messages: &'a [Message],
pub model: &'a str,
pub temperature: f32,
pub top_p: f32,
pub top_k: u32,
pub max_tokens: u32,
pub max_retries: u32,
pub config: &'a Config,
pub chat_session: Option<&'a mut crate::session::chat::session::ChatSession>,
pub cancellation_token: Option<tokio::sync::watch::Receiver<bool>>,
pub is_continuation_call: bool,
}
impl<'a> ChatCompletionWithValidationParams<'a> {
pub fn new(
messages: &'a [Message],
model: &'a str,
temperature: f32,
top_p: f32,
top_k: u32,
max_tokens: u32,
config: &'a Config,
) -> Self {
Self {
messages,
model,
temperature,
top_p,
top_k,
max_tokens,
max_retries: 0,
config,
chat_session: None,
cancellation_token: None,
is_continuation_call: false,
}
}
pub fn with_max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = max_retries;
self
}
pub fn with_chat_session(
mut self,
chat_session: &'a mut crate::session::chat::session::ChatSession,
) -> Self {
self.chat_session = Some(chat_session);
self
}
pub fn with_cancellation_token(mut self, token: watch::Receiver<bool>) -> Self {
self.cancellation_token = Some(token);
self
}
pub fn as_continuation_call(mut self) -> Self {
self.is_continuation_call = true;
self
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
pub role: String,
pub content: String,
pub timestamp: u64,
#[serde(default = "default_cache_marker")]
pub cached: bool, #[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<serde_json::Value>, #[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<crate::session::image::ImageAttachment>>, #[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<serde_json::Value>, }
fn default_cache_marker() -> bool {
false
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
impl Default for Message {
fn default() -> Self {
Self {
role: String::new(),
content: String::new(),
timestamp: current_timestamp(),
cached: false,
tool_call_id: None,
name: None,
tool_calls: None,
images: None,
thinking: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SessionInfo {
pub name: String,
pub created_at: u64,
pub model: String,
pub provider: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub cached_tokens: u64, pub total_cost: f64,
pub duration_seconds: u64,
pub layer_stats: Vec<LayerStats>, #[serde(default)]
pub tool_calls: u64, #[serde(default)]
pub total_api_time_ms: u64, #[serde(default)]
pub total_tool_time_ms: u64, #[serde(default)]
pub total_layer_time_ms: u64, }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LayerStats {
pub layer_type: String,
pub model: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub cost: f64,
pub timestamp: u64,
#[serde(default)]
pub api_time_ms: u64, #[serde(default)]
pub tool_time_ms: u64, #[serde(default)]
pub total_time_ms: u64, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCostData {
pub agent_name: String,
pub model: String,
pub input_tokens: u64,
pub output_tokens: u64,
pub cached_tokens: u64,
pub cost: f64,
pub api_time_ms: u64,
pub tool_time_ms: u64,
pub layer_time_ms: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Session {
pub info: SessionInfo,
pub messages: Vec<Message>,
pub session_file: Option<PathBuf>,
pub current_non_cached_tokens: u64,
pub current_total_tokens: u64,
#[serde(default = "current_timestamp")]
pub last_cache_checkpoint_time: u64,
}
impl Session {
pub fn new(name: String, model: String, provider: String) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
info: SessionInfo {
name,
created_at: timestamp,
model,
provider,
input_tokens: 0,
output_tokens: 0,
cached_tokens: 0,
total_cost: 0.0,
duration_seconds: 0,
layer_stats: Vec::new(), tool_calls: 0, total_api_time_ms: 0,
total_tool_time_ms: 0,
total_layer_time_ms: 0,
},
messages: Vec::new(),
session_file: None,
current_non_cached_tokens: 0,
current_total_tokens: 0,
last_cache_checkpoint_time: timestamp,
}
}
pub fn add_message(&mut self, role: &str, content: &str) -> Message {
let message = Message {
role: role.to_string(),
content: content.to_string(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
cached: false,
..Default::default()
};
self.messages.push(message.clone());
message
}
pub fn add_cache_checkpoint(&mut self, system: bool) -> Result<bool, anyhow::Error> {
if system {
for msg in self.messages.iter_mut() {
if msg.role == "system" {
msg.cached = crate::session::model_supports_caching(&self.info.model);
if msg.cached {
self.current_non_cached_tokens = 0;
self.current_total_tokens = 0;
return Ok(true);
}
return Ok(false);
}
}
Ok(false)
} else {
Err(anyhow::anyhow!(
"Use CacheManager for content cache markers instead of add_cache_checkpoint"
))
}
}
pub fn add_layer_stats(
&mut self,
layer_type: &str,
model: &str,
input_tokens: u64,
output_tokens: u64,
cost: f64,
) {
self.add_layer_stats_with_time(
layer_type,
model,
input_tokens,
output_tokens,
cost,
0,
0,
0,
);
}
#[allow(clippy::too_many_arguments)]
pub fn add_layer_stats_with_time(
&mut self,
layer_type: &str,
model: &str,
input_tokens: u64,
output_tokens: u64,
cost: f64,
api_time_ms: u64,
tool_time_ms: u64,
total_time_ms: u64,
) {
let stats = LayerStats {
layer_type: layer_type.to_string(),
model: model.to_string(),
input_tokens,
output_tokens,
cost,
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
api_time_ms,
tool_time_ms,
total_time_ms,
};
self.info.layer_stats.push(stats);
self.info.input_tokens += input_tokens;
self.info.output_tokens += output_tokens;
self.info.total_cost += cost;
self.info.total_api_time_ms += api_time_ms;
self.info.total_tool_time_ms += tool_time_ms;
self.info.total_layer_time_ms += total_time_ms;
}
pub fn add_agent_cost(&mut self, agent_costs: AgentCostData) {
self.add_layer_stats_with_time(
&format!("agent_{}", agent_costs.agent_name),
&agent_costs.model,
agent_costs.input_tokens,
agent_costs.output_tokens,
agent_costs.cost,
agent_costs.api_time_ms,
agent_costs.tool_time_ms,
agent_costs.layer_time_ms,
);
self.info.cached_tokens += agent_costs.cached_tokens;
crate::log_debug!(
"Added agent '{}' costs to session: ${:.5} ({} input, {} output, {} cached tokens)",
agent_costs.agent_name,
agent_costs.cost,
agent_costs.input_tokens,
agent_costs.output_tokens,
agent_costs.cached_tokens
);
}
pub fn save(&self) -> Result<(), anyhow::Error> {
if let Some(session_file) = &self.session_file {
let summary_entry = serde_json::json!({
"type": "SUMMARY",
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
"session_info": &self.info
});
append_to_session_file(session_file, &serde_json::to_string(&summary_entry)?)?;
Ok(())
} else {
Err(anyhow::anyhow!("No session file specified"))
}
}
}
pub fn get_sessions_dir() -> Result<PathBuf, anyhow::Error> {
crate::directories::get_sessions_dir()
}
pub fn list_available_sessions() -> Result<Vec<(String, SessionInfo)>, anyhow::Error> {
let sessions_dir = get_sessions_dir()?;
let mut sessions = Vec::new();
if !sessions_dir.exists() {
return Ok(sessions);
}
for entry in std_fs::read_dir(sessions_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "jsonl") {
if let Ok(file) = File::open(&path) {
let reader = BufReader::new(file);
let first_line = reader.lines().next();
if let Some(Ok(line)) = first_line {
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&line) {
if let Some(log_type) = json_value.get("type").and_then(|t| t.as_str()) {
if log_type == "SUMMARY" {
if let Some(session_info_value) = json_value.get("session_info") {
if let Ok(info) = serde_json::from_value::<SessionInfo>(
session_info_value.clone(),
) {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();
sessions.push((name, info));
}
}
}
}
} else if let Some(content) = line.strip_prefix("SUMMARY: ") {
if let Ok(info) = serde_json::from_str::<SessionInfo>(content) {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();
sessions.push((name, info));
}
}
}
}
}
}
sessions.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at));
Ok(sessions)
}
pub fn find_most_recent_session_for_project(
project_dir: &Path,
) -> Result<Option<String>, anyhow::Error> {
let sessions_dir = get_sessions_dir()?;
if !sessions_dir.exists() {
return Ok(None);
}
let project_basename = project_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if project_basename.is_empty() {
return Ok(None);
}
let mut matching_sessions: Vec<(String, u64)> = Vec::new();
for entry in std_fs::read_dir(sessions_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "jsonl") {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default();
if name.contains(project_basename) {
if let Ok(metadata) = std_fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if let Ok(duration) =
modified.duration_since(std::time::SystemTime::UNIX_EPOCH)
{
matching_sessions.push((name.to_string(), duration.as_secs()));
}
}
}
}
}
}
matching_sessions.sort_by(|a, b| b.1.cmp(&a.1));
Ok(matching_sessions.first().map(|(name, _)| name.clone()))
}
fn has_incomplete_tool_calls(messages: &[Message]) -> bool {
for (i, msg) in messages.iter().enumerate() {
if msg.role == "assistant" && msg.tool_calls.is_some() {
if let Some(tool_calls_value) = &msg.tool_calls {
if let Ok(tool_calls) =
serde_json::from_value::<Vec<serde_json::Value>>(tool_calls_value.clone())
{
for tool_call in tool_calls {
if let Some(call_id) = tool_call.get("id").and_then(|id| id.as_str()) {
let has_response = messages.iter().skip(i + 1).any(|response_msg| {
response_msg.role == "tool"
&& response_msg.tool_call_id.as_ref()
== Some(&call_id.to_string())
});
if !has_response {
return true; }
}
}
}
}
}
}
false
}
pub fn clean_interrupted_tool_calls(
messages: &mut Vec<Message>,
session_name: &str,
context: &str,
) -> bool {
if messages.is_empty() {
return false;
}
let mut truncate_from_index = None;
for (i, msg) in messages.iter().enumerate() {
if msg.role == "assistant" && msg.tool_calls.is_some() {
if let Some(tool_calls_value) = &msg.tool_calls {
if let Ok(tool_calls) =
serde_json::from_value::<Vec<serde_json::Value>>(tool_calls_value.clone())
{
let mut has_incomplete_calls = false;
for tool_call in tool_calls {
if let Some(call_id) = tool_call.get("id").and_then(|id| id.as_str()) {
let has_response = messages.iter().skip(i + 1).any(|response_msg| {
response_msg.role == "tool"
&& response_msg.tool_call_id.as_ref()
== Some(&call_id.to_string())
});
if !has_response {
has_incomplete_calls = true;
break;
}
}
}
if has_incomplete_calls {
truncate_from_index = Some(i);
break; }
}
}
}
}
if let Some(truncate_index) = truncate_from_index {
let original_len = messages.len();
messages.truncate(truncate_index);
let removed_count = original_len - messages.len();
if removed_count > 0 {
eprintln!(
"🔧 {}: Truncated {} messages from incomplete tool sequence",
context, removed_count
);
let _ = crate::session::logger::log_system_message(
session_name,
&format!(
"{}: Truncated {} messages from incomplete tool sequence",
context, removed_count
),
);
return true;
}
}
false
}
pub fn load_session(session_file: &PathBuf) -> Result<Session, anyhow::Error> {
if !session_file.exists() {
return Err(anyhow::anyhow!("Session file does not exist"));
}
let file = File::open(session_file)?;
let reader = BufReader::new(file);
let mut session_info: Option<SessionInfo> = None;
let mut messages = Vec::new();
let mut restoration_point_found = false;
let mut restoration_messages = Vec::new();
let mut pending_tool_calls = Vec::new();
for line in reader.lines() {
let line = line?;
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&line) {
if let Some(log_type) = json_value.get("type").and_then(|t| t.as_str()) {
match log_type {
"SUMMARY" => {
if let Some(session_info_value) = json_value.get("session_info") {
session_info =
Some(serde_json::from_value(session_info_value.clone())?);
}
}
"RESTORATION_POINT" => {
restoration_point_found = true;
messages.clear();
restoration_messages.clear();
}
"COMMAND" => {
continue;
}
"OUTPUT_MODE_REPLACE" => {
if restoration_point_found {
restoration_messages.clear();
} else {
messages.clear();
}
if let Some(command) = json_value.get("command").and_then(|c| c.as_str()) {
println!(
"Session restoration: Found OUTPUT_MODE_REPLACE from command '{}'",
command
);
}
}
"OUTPUT_MODE_APPEND" => {
continue;
}
"STATS" => {
if let Some(info) = &mut session_info {
if let Some(total_cost) =
json_value.get("total_cost").and_then(|c| c.as_f64())
{
info.total_cost = total_cost;
}
if let Some(input_tokens) =
json_value.get("input_tokens").and_then(|t| t.as_u64())
{
info.input_tokens = input_tokens;
}
if let Some(output_tokens) =
json_value.get("output_tokens").and_then(|t| t.as_u64())
{
info.output_tokens = output_tokens;
}
if let Some(cached_tokens) =
json_value.get("cached_tokens").and_then(|t| t.as_u64())
{
info.cached_tokens = cached_tokens;
}
if let Some(tool_calls) =
json_value.get("tool_calls").and_then(|t| t.as_u64())
{
info.tool_calls = tool_calls;
}
if let Some(api_time) =
json_value.get("total_api_time_ms").and_then(|t| t.as_u64())
{
info.total_api_time_ms = api_time;
}
if let Some(tool_time) = json_value
.get("total_tool_time_ms")
.and_then(|t| t.as_u64())
{
info.total_tool_time_ms = tool_time;
}
if let Some(layer_time) = json_value
.get("total_layer_time_ms")
.and_then(|t| t.as_u64())
{
info.total_layer_time_ms = layer_time;
}
}
}
"TOOL_CALL" => {
if let (Some(tool_name), Some(tool_id), Some(parameters)) = (
json_value.get("tool_name").and_then(|n| n.as_str()),
json_value.get("tool_id").and_then(|id| id.as_str()),
json_value.get("parameters"),
) {
pending_tool_calls.push(serde_json::json!({
"id": tool_id,
"type": "function",
"function": {
"name": tool_name,
"arguments": serde_json::to_string(parameters).unwrap_or_default()
}
}));
}
}
"API_REQUEST" | "API_RESPONSE" | "TOOL_RESULT" | "CACHE" | "ERROR"
| "SYSTEM" | "USER" | "ASSISTANT" => {
continue;
}
_ => {
continue;
}
}
} else if line.contains("\"role\":") && line.contains("\"content\":") {
if let Ok(message) = serde_json::from_str::<Message>(&line) {
if message.role == "tool" && !pending_tool_calls.is_empty() {
let assistant_with_tool_calls = Message {
role: "assistant".to_string(),
content: "".to_string(), tool_calls: Some(serde_json::Value::Array(pending_tool_calls.clone())),
timestamp: message.timestamp,
cached: false,
..Default::default()
};
if restoration_point_found {
restoration_messages.push(assistant_with_tool_calls);
} else {
messages.push(assistant_with_tool_calls);
}
pending_tool_calls.clear();
}
if restoration_point_found {
restoration_messages.push(message);
} else {
messages.push(message);
}
}
}
} else {
if line.starts_with("SUMMARY: ") {
if let Some(content) = line.strip_prefix("SUMMARY: ") {
session_info = Some(serde_json::from_str(content)?);
}
} else if line.starts_with("INFO: ") {
if let Some(content) = line.strip_prefix("INFO: ") {
let mut old_info: SessionInfo = serde_json::from_str(content)?;
old_info.input_tokens = 0;
old_info.output_tokens = 0;
old_info.cached_tokens = 0;
old_info.total_cost = 0.0;
old_info.duration_seconds = 0;
old_info.layer_stats = Vec::new();
old_info.tool_calls = 0;
old_info.total_api_time_ms = 0;
old_info.total_tool_time_ms = 0;
old_info.total_layer_time_ms = 0;
session_info = Some(old_info);
}
} else if line.starts_with("RESTORATION_POINT: ") {
restoration_point_found = true;
messages.clear();
restoration_messages.clear();
} else if !line.starts_with("API_REQUEST: ")
&& !line.starts_with("API_RESPONSE: ")
&& !line.starts_with("TOOL_CALL: ")
&& !line.starts_with("TOOL_RESULT: ")
&& !line.starts_with("CACHE: ")
&& !line.starts_with("ERROR: ")
&& !line.starts_with("EXCHANGE: ")
&& !line.is_empty()
{
if line.contains("\"role\":") && line.contains("\"content\":") {
if let Ok(message) = serde_json::from_str::<Message>(&line) {
if restoration_point_found {
restoration_messages.push(message);
} else {
messages.push(message);
}
}
} else if let Some(content) = line.strip_prefix("SYSTEM: ") {
if let Ok(message) = serde_json::from_str::<Message>(content) {
if restoration_point_found {
restoration_messages.push(message);
} else {
messages.push(message);
}
}
} else if let Some(content) = line.strip_prefix("USER: ") {
if let Ok(message) = serde_json::from_str::<Message>(content) {
if restoration_point_found {
restoration_messages.push(message);
} else {
messages.push(message);
}
}
} else if let Some(content) = line.strip_prefix("ASSISTANT: ") {
if let Ok(message) = serde_json::from_str::<Message>(content) {
if restoration_point_found {
restoration_messages.push(message);
} else {
messages.push(message);
}
}
}
}
}
}
let final_messages = if restoration_point_found && !restoration_messages.is_empty() {
restoration_messages
} else {
messages
};
if let Some(mut info) = session_info {
let runtime_state = extract_runtime_state_from_log(session_file)?;
if let Some(model) = runtime_state.model {
info.model = model;
}
let mut cleaned_messages = final_messages;
if has_incomplete_tool_calls(&cleaned_messages) {
clean_interrupted_tool_calls(&mut cleaned_messages, &info.name, "Session restoration");
}
let session = Session {
info,
messages: cleaned_messages,
session_file: Some(session_file.clone()),
current_non_cached_tokens: 0,
current_total_tokens: 0,
last_cache_checkpoint_time: current_timestamp(), };
Ok(session)
} else {
let session_name = session_file
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
let default_model = final_messages
.iter()
.find_map(|_msg| {
None::<String> })
.unwrap_or_else(|| "openrouter:anthropic/claude-sonnet-4".to_string());
let created_at = session_file
.metadata()
.and_then(|meta| {
meta.created()
.ok()
.ok_or(std::io::Error::other("No creation time"))
})
.and_then(|time| {
time.duration_since(std::time::UNIX_EPOCH)
.ok()
.ok_or(std::io::Error::other("Invalid time"))
})
.map(|duration| duration.as_secs())
.unwrap_or_else(|_| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
});
let default_info = SessionInfo {
name: session_name,
created_at,
model: default_model.clone(),
provider: if default_model.starts_with("openrouter:") {
"openrouter".to_string()
} else if default_model.starts_with("anthropic:") {
"anthropic".to_string()
} else if default_model.starts_with("openai:") {
"openai".to_string()
} else {
"unknown".to_string()
},
input_tokens: 0,
output_tokens: 0,
cached_tokens: 0,
total_cost: 0.0,
duration_seconds: 0,
layer_stats: Vec::new(),
tool_calls: 0,
total_api_time_ms: 0,
total_tool_time_ms: 0,
total_layer_time_ms: 0,
};
let runtime_state = extract_runtime_state_from_log(session_file)?;
let mut info = default_info;
if let Some(model) = runtime_state.model {
info.model = model;
}
let file = File::open(session_file)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&line) {
if let Some(log_type) = json_value.get("type").and_then(|t| t.as_str()) {
if log_type == "STATS" {
if let Some(total_cost) =
json_value.get("total_cost").and_then(|c| c.as_f64())
{
info.total_cost = total_cost;
}
if let Some(input_tokens) =
json_value.get("input_tokens").and_then(|t| t.as_u64())
{
info.input_tokens = input_tokens;
}
if let Some(output_tokens) =
json_value.get("output_tokens").and_then(|t| t.as_u64())
{
info.output_tokens = output_tokens;
}
if let Some(cached_tokens) =
json_value.get("cached_tokens").and_then(|t| t.as_u64())
{
info.cached_tokens = cached_tokens;
}
if let Some(tool_calls) =
json_value.get("tool_calls").and_then(|t| t.as_u64())
{
info.tool_calls = tool_calls;
}
if let Some(api_time) =
json_value.get("total_api_time_ms").and_then(|t| t.as_u64())
{
info.total_api_time_ms = api_time;
}
if let Some(tool_time) = json_value
.get("total_tool_time_ms")
.and_then(|t| t.as_u64())
{
info.total_tool_time_ms = tool_time;
}
if let Some(layer_time) = json_value
.get("total_layer_time_ms")
.and_then(|t| t.as_u64())
{
info.total_layer_time_ms = layer_time;
}
}
}
}
}
let session = Session {
info,
messages: final_messages,
session_file: Some(session_file.clone()),
current_non_cached_tokens: 0,
current_total_tokens: 0,
last_cache_checkpoint_time: current_timestamp(),
};
println!("⚠️ Session loaded with default metadata (SUMMARY was missing)");
Ok(session)
}
}
#[derive(Debug, Default)]
pub struct SessionRuntimeState {
pub model: Option<String>,
pub layers_enabled: Option<bool>,
pub cache_next_message: bool,
pub role: Option<String>, }
pub fn extract_runtime_state_from_log(session_file: &PathBuf) -> Result<SessionRuntimeState> {
let file = File::open(session_file)?;
let reader = BufReader::new(file);
let mut state = SessionRuntimeState::default();
for line in reader.lines() {
let line = line?;
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&line) {
if let Some(log_type) = json_value.get("type").and_then(|t| t.as_str()) {
match log_type {
"RESTORATION_POINT" => {
state = SessionRuntimeState::default();
}
"COMMAND" => {
if let Some(command) = json_value.get("command").and_then(|c| c.as_str()) {
apply_command_to_runtime_state(&mut state, command);
}
}
_ => {}
}
}
}
}
Ok(state)
}
fn apply_command_to_runtime_state(state: &mut SessionRuntimeState, command_line: &str) {
let parts: Vec<&str> = command_line.split_whitespace().collect();
if parts.is_empty() {
return;
}
match parts[0] {
"/model" => {
if parts.len() > 1 {
let new_model = parts[1..].join(" ");
state.model = Some(new_model);
}
}
"/role" => {
if parts.len() > 1 {
let new_role = parts[1].to_string();
state.role = Some(new_role);
}
}
"/layers" => {
if parts.len() > 1 {
let state_str = parts[1];
state.layers_enabled = Some(state_str == "enabled");
}
}
"/cache" => {
state.cache_next_message = true;
}
_ => {
}
}
}
pub fn append_to_session_file(session_file: &PathBuf, content: &str) -> Result<(), anyhow::Error> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(session_file)?;
let single_line_content = content.replace(['\n', '\r'], " ");
writeln!(file, "{}", single_line_content)?;
Ok(())
}
pub async fn create_system_prompt(
project_dir: &Path,
config: &crate::config::Config,
mode: &str,
) -> String {
let (_, mcp_config, _, _, system_prompt) = config.get_role_config(mode);
let mut prompt = helper_functions::process_placeholders_async(system_prompt, project_dir).await;
if !mcp_config.server_refs.is_empty() {
let config_for_role = config.get_merged_config_for_role(mode);
let functions = crate::mcp::get_available_functions(&config_for_role).await;
if !functions.is_empty() {
prompt.push_str("\n\nYou have access to the following tools:");
for function in &functions {
prompt.push_str(&format!(
"\n\n- {} - {}",
function.name, function.description
));
}
}
}
prompt
}
pub async fn chat_completion_with_validation(
params: ChatCompletionWithValidationParams<'_>,
) -> Result<ProviderResponse> {
if let Some(ref token) = params.cancellation_token {
if *token.borrow() {
return Err(anyhow::anyhow!("Request cancelled before validation"));
}
}
let (provider, actual_model) = ProviderFactory::get_provider_for_model(params.model)?;
let max_input_tokens = provider.get_max_input_tokens(&actual_model);
let total_input_tokens = if let Some(ref session) = params.chat_session {
let (_, _, _, _, system_prompt) = params.config.get_role_config(&session.role);
let tools = crate::mcp::get_available_functions(params.config).await;
estimate_full_context_tokens(
params.messages,
Some(system_prompt),
if tools.is_empty() { None } else { Some(&tools) },
)
} else {
estimate_message_tokens(params.messages)
};
if total_input_tokens > max_input_tokens {
crate::log_error!(
"⚠️ Input too large for {} {} ({} tokens, max {} tokens)",
provider.name(),
actual_model,
total_input_tokens,
max_input_tokens
);
if let Some(session) = params.chat_session {
if !params.is_continuation_call {
if crate::session::chat::session_continuation::check_and_handle_continuation(
session,
params.config,
)
.await?
{
let messages = session.session.messages.clone();
let continuation_params = ChatCompletionWithValidationParams::new(
&messages,
params.model,
params.temperature,
params.top_p,
params.top_k,
params.max_tokens,
params.config,
)
.with_max_retries(params.max_retries)
.with_chat_session(session)
.as_continuation_call();
let continuation_params = if let Some(token) = params.cancellation_token {
continuation_params.with_cancellation_token(token)
} else {
continuation_params
};
return Box::pin(chat_completion_with_validation(continuation_params)).await;
} else {
return Err(anyhow::anyhow!(
"Input size ({} tokens) exceeds provider limit ({} tokens) for {} {}",
total_input_tokens,
max_input_tokens,
provider.name(),
actual_model
));
}
} else {
return Err(anyhow::anyhow!(
"Continuation call input size ({} tokens) exceeds provider limit ({} tokens) for {} {} - breaking infinite loop",
total_input_tokens,
max_input_tokens,
provider.name(),
actual_model
));
}
} else {
return Err(anyhow::anyhow!(
"Input size ({} tokens) exceeds provider limit ({} tokens) for {} {}",
total_input_tokens,
max_input_tokens,
provider.name(),
actual_model
));
}
}
if let Some(ref token) = params.cancellation_token {
if *token.borrow() {
return Err(anyhow::anyhow!("Request cancelled before API call"));
}
}
let chat_params = ChatCompletionParams::new(
params.messages,
&actual_model,
params.temperature,
params.top_p,
params.top_k,
params.max_tokens,
params.config,
)
.with_max_retries(params.max_retries);
let chat_params = if let Some(token) = params.cancellation_token {
chat_params.with_cancellation_token(token)
} else {
chat_params
};
let octolib_params = chat_params
.to_octolib_params()
.await
.map_err(|e| anyhow::anyhow!("Failed to convert message parameters: {}", e))?;
let octolib_response = provider.chat_completion(octolib_params).await?;
Ok(crate::providers::convert_response_from_octolib(
octolib_response,
))
}
pub struct ChatCompletionProviderParams<'a> {
pub messages: &'a [Message],
pub model: &'a str,
pub temperature: f32,
pub top_p: f32,
pub top_k: u32,
pub max_tokens: u32,
pub config: &'a Config,
pub max_retries: u32,
}
pub async fn chat_completion_with_provider(
params: ChatCompletionProviderParams<'_>,
) -> Result<ProviderResponse> {
let (provider, actual_model) = ProviderFactory::get_provider_for_model(params.model)?;
let chat_params = ChatCompletionParams::new(
params.messages,
&actual_model,
params.temperature,
params.top_p,
params.top_k,
params.max_tokens,
params.config,
)
.with_max_retries(params.max_retries);
let octolib_params = chat_params
.to_octolib_params()
.await
.map_err(|e| anyhow::anyhow!("Failed to convert message parameters: {}", e))?;
let octolib_response = provider.chat_completion(octolib_params).await?;
Ok(crate::providers::convert_response_from_octolib(
octolib_response,
))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn create_test_message(
role: &str,
content: &str,
tool_calls: Option<serde_json::Value>,
tool_call_id: Option<String>,
) -> Message {
Message {
role: role.to_string(),
content: content.to_string(),
timestamp: 1234567890,
cached: false,
tool_call_id,
name: None,
tool_calls,
images: None,
thinking: None,
}
}
#[test]
fn test_has_incomplete_tool_calls_complete_sequence() {
let messages = vec![
create_test_message("user", "List files", None, None),
create_test_message(
"assistant",
"I'll list the files for you.",
Some(
json!([{"id": "call_123", "name": "list_files", "arguments": {"directory": "."}}]),
),
None,
),
create_test_message(
"tool",
"file1.txt\nfile2.txt",
None,
Some("call_123".to_string()),
),
create_test_message(
"assistant",
"Here are the files in the directory.",
None,
None,
),
];
assert!(!has_incomplete_tool_calls(&messages));
}
#[test]
fn test_has_incomplete_tool_calls_incomplete_sequence() {
let messages = vec![
create_test_message("user", "List files", None, None),
create_test_message(
"assistant",
"I'll list the files for you.",
Some(
json!([{"id": "call_123", "name": "list_files", "arguments": {"directory": "."}}]),
),
None,
),
];
assert!(has_incomplete_tool_calls(&messages));
}
#[test]
fn test_has_incomplete_tool_calls_multiple_calls_partial() {
let messages = vec![
create_test_message("user", "Do multiple things", None, None),
create_test_message(
"assistant",
"I'll do multiple things.",
Some(json!([
{"id": "call_123", "name": "list_files", "arguments": {"directory": "."}},
{"id": "call_456", "name": "shell", "arguments": {"command": "pwd"}}
])),
None,
),
create_test_message(
"tool",
"file1.txt\nfile2.txt",
None,
Some("call_123".to_string()),
),
];
assert!(has_incomplete_tool_calls(&messages));
}
#[test]
fn test_has_incomplete_tool_calls_no_tool_calls() {
let messages = vec![
create_test_message("user", "Hello", None, None),
create_test_message("assistant", "Hello! How can I help you?", None, None),
];
assert!(!has_incomplete_tool_calls(&messages));
}
#[test]
fn test_clean_interrupted_tool_calls_preserves_complete() {
let mut messages = vec![
create_test_message("user", "List files", None, None),
create_test_message(
"assistant",
"I'll list the files for you.",
Some(
json!([{"id": "call_123", "name": "list_files", "arguments": {"directory": "."}}]),
),
None,
),
create_test_message(
"tool",
"file1.txt\nfile2.txt",
None,
Some("call_123".to_string()),
),
create_test_message("assistant", "Here are the files.", None, None),
];
let original_count = messages.len();
let cleaned = clean_interrupted_tool_calls(&mut messages, "test_session", "Test");
assert!(!cleaned);
assert_eq!(messages.len(), original_count);
}
#[test]
fn test_clean_interrupted_tool_calls_removes_incomplete() {
let mut messages = vec![
create_test_message("user", "List files", None, None),
create_test_message(
"assistant",
"I'll list the files for you.",
Some(
json!([{"id": "call_123", "name": "list_files", "arguments": {"directory": "."}}]),
),
None,
),
];
let cleaned = clean_interrupted_tool_calls(&mut messages, "test_session", "Test");
assert!(cleaned);
assert_eq!(messages.len(), 1); assert_eq!(messages[0].role, "user");
}
}