use std::pin::Pin; use config::AppConfig; use db::cache::AppCache; use db::database::AppDatabase; use models::agents::model; use models::projects::{project, project_context_setting}; use models::repos::repo; use models::rooms::{room, room_message}; use models::users::user; use std::collections::HashMap; use uuid::Uuid; /// Maximum recursion rounds for tool-call loops (AI → tool → result → AI). /// Previous default of 3 caused frequent silent termination on realistic multi-step queries. pub const DEFAULT_MAX_TOOL_DEPTH: usize = 99; /// A single chunk from an AI streaming response. #[derive(Debug, Clone)] pub struct AiStreamChunk { pub content: String, pub done: bool, /// What kind of content this chunk contains — helps the frontend render /// thinking, tool calls, and results with different styles. pub chunk_type: AiChunkType, /// Structured metadata for tool_call / tool_result events. /// tool_call: {"tool": "...", "args": {...}} /// tool_result: {"tool": "...", "status": "ok|error", "result": "..."} pub metadata: Option, /// Optional ID of a child process/agent, sent to frontend via SSE. pub children_id: Option, } /// Type of streaming chunk, used by the frontend for rendering. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AiChunkType { /// AI reasoning/thinking text before a tool call or answer. Thinking, /// Final answer text from the AI. Answer, /// A tool call is being executed (content = tool name + args summary). ToolCall, /// Tool execution result (content = result or error). ToolResult, } impl Default for AiChunkType { fn default() -> Self { Self::Answer } } const THINK_OPEN: &str = "\x3cthinking\x3e"; const THINK_CLOSE: &str = "\x3c/response\x3e"; /// Strip XML-format thinking tags that some models (e.g. DeepSeek-R1) embed /// in reasoning output. Also normalizes excessive consecutive newlines (3+ → 2). pub fn normalize_thinking_content(content: &str) -> String { let content = content .replace(THINK_CLOSE, "") .replace(THINK_OPEN, "") .replace("\x3cthinking", "") .replace("/response\x3e", ""); let mut result = String::with_capacity(content.len()); let mut newline_count = 0usize; for ch in content.chars() { if ch == '\n' { newline_count += 1; if newline_count <= 2 { result.push(ch); } } else { newline_count = 0; result.push(ch); } } result.trim().to_string() } pub type StreamCallback = Box< dyn Fn(AiStreamChunk) -> Pin + Send>> + Send + Sync, >; #[derive(Debug, Clone, PartialEq, Eq)] pub enum AgentRole { Default, Supervisor, Researcher, Analyst, Reviewer, Architect, Debugger, Implementer, Tester, Security, } #[derive(Debug, Clone, Default)] pub struct AgentExecutionProfile { pub role: AgentRole, pub system_prompt: Option, pub temperature: Option, pub max_tokens: Option, pub top_p: Option, pub frequency_penalty: Option, pub presence_penalty: Option, pub max_tool_depth: Option, pub allowed_tools: Option>, pub disable_orchestration: bool, } impl Default for AgentRole { fn default() -> Self { Self::Default } } #[derive(Clone)] pub struct AiRequest { pub db: AppDatabase, pub cache: AppCache, pub config: AppConfig, pub model: model::Model, pub project: project::Model, pub context_setting: Option, pub sender: user::Model, pub room: room::Model, pub input: String, pub mention: Vec, pub history: Vec, pub history_cutoff_seq: Option, pub user_names: HashMap, pub temperature: f64, pub max_tokens: i32, pub top_p: f64, pub frequency_penalty: f64, pub presence_penalty: f64, pub think: bool, pub tools: Option>, pub max_tool_depth: usize, pub execution_profile: Option, pub room_preamble: Option, } #[derive(Clone)] pub enum Mention { User(user::Model), Repo(repo::Model), } pub mod agent_profile; pub mod chat_execution; pub mod context; pub mod message_builder; pub mod nonstreaming_execution; pub mod orchestrator; pub mod react_execution; pub mod service; pub mod session_recording; pub mod state; pub mod streaming_execution; pub use context::{AiContextSenderType, RoomMessageContext}; pub use service::ChatService; pub use state::{AgentRuntime, AgentState};