pub mod actions;
pub(crate) mod wire;
use crate::server::connection::ConnectionId;
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncWriteExt, BufReader};
use tokio::sync::mpsc;
use tracing::{debug, error, info, trace};
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::server::irc::wire::{read_irc_line, IrcLine, MAX_IRC_READ_LINE};
use crate::server::IrcProtocol;
use crate::state::app_state::AppState;
use actions::IRC_MESSAGE_RECEIVED_EVENT;
const FIRST_MESSAGE_READ_TIMEOUT: Duration = Duration::from_secs(60);
const IDLE_BETWEEN_MESSAGES_TIMEOUT: Duration = Duration::from_secs(1800);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"ERROR :Closing Link: too many connections\r\n";
pub struct IrcServer;
impl IrcServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
) -> Result<SocketAddr> {
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
info!("IRC server (action-based) listening on {}", local_addr);
let _ = status_tx.send(format!("IRC server listening on {}", local_addr));
let protocol = Arc::new(IrcProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"IRC",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
let llm_clone = llm_client.clone();
let state_clone = app_state.clone();
let status_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner.spawn_server_task(server_id, async move {
let _permit = permit;
let (read_half, write_half) = tokio::io::split(stream);
let write_half_arc = Arc::new(tokio::sync::Mutex::new(write_half));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
state_clone
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
let peer_rx = crate::server::peer_support::register_peer_channel(
&state_clone,
server_id,
connection_id.as_u32(),
)
.await;
crate::server::peer_support::spawn_peer_command_task(
peer_rx,
protocol_clone.clone(),
state_clone.clone(),
server_id,
connection_id.as_u32(),
write_half_arc.clone(),
status_clone.clone(),
);
let mut reader = BufReader::new(read_half);
let mut answered_one = false;
loop {
let read_timeout = if answered_one {
IDLE_BETWEEN_MESSAGES_TIMEOUT
} else {
FIRST_MESSAGE_READ_TIMEOUT
};
let framed = tokio::time::timeout(
read_timeout,
read_irc_line(&mut reader, MAX_IRC_READ_LINE),
)
.await;
let (read, n) = match framed {
Err(_) => {
info!(
"IRC connection {} from {} sent nothing for {}s; \
closing idle connection",
connection_id,
remote_addr,
read_timeout.as_secs()
);
break;
}
Ok(Ok(v)) => v,
Ok(Err(e)) => {
debug!(
"IRC read error on connection {}: {}",
connection_id, e
);
break;
}
};
answered_one = true;
let line = match read {
IrcLine::Line(line) => line,
IrcLine::Eof => break,
IrcLine::TooLong => {
error!(
"IRC connection {} sent {} bytes with no newline \
(limit {}), closing decision=refused_body_too_large",
connection_id, n, MAX_IRC_READ_LINE
);
let _ = status_clone.send(format!(
"[ERROR] IRC connection {} exceeded {} bytes with no \
newline decision=refused_body_too_large",
connection_id, MAX_IRC_READ_LINE
));
let reply = format!(
"ERROR :Closing link: message exceeds {MAX_IRC_READ_LINE} bytes\r\n"
);
let mut write = write_half_arc.lock().await;
let _ = write.write_all(reply.as_bytes()).await;
let _ = write.flush().await;
let _ = write.shutdown().await;
break;
}
};
state_clone
.update_connection_stats(
server_id,
connection_id,
Some(n as u64),
None,
Some(1),
None,
)
.await;
let preview = crate::utils::truncate_for_log(&line, 100);
debug!(
"IRC received {} bytes on connection {}: {}",
n,
connection_id,
preview.trim()
);
let _ = status_clone.send(format!(
"[DEBUG] IRC received {} bytes on connection {}: {}",
n,
connection_id,
preview.trim()
));
trace!("IRC data (text): {:?}", line.trim());
let _ = status_clone
.send(format!("[TRACE] IRC data (text): {:?}", line.trim()));
let event = Event::new(
&IRC_MESSAGE_RECEIVED_EVENT,
serde_json::json!({
"message": line.trim()
}),
);
debug!("IRC calling LLM for connection {}", connection_id);
let _ = status_clone.send(format!(
"[DEBUG] IRC calling LLM for connection {}",
connection_id
));
match call_llm(
&llm_clone,
&state_clone,
server_id,
Some(connection_id),
&event,
protocol_clone.as_ref(),
)
.await
{
Ok(execution_result) => {
for message in &execution_result.messages {
info!("{}", message);
let _ =
status_clone.send(format!("[INFO] {}", message));
}
debug!(
"IRC got {} protocol results",
execution_result.protocol_results.len()
);
let _ = status_clone.send(format!(
"[DEBUG] IRC got {} protocol results",
execution_result.protocol_results.len()
));
let mut wrote_output = false;
let mut asked_to_close = false;
let failures = execution_result.failures.len();
for protocol_result in execution_result.protocol_results {
match protocol_result {
ActionResult::Output(data) => {
wrote_output = true;
let response = String::from_utf8_lossy(&data);
let formatted = if response.ends_with("\r\n") {
response.to_string()
} else if response.ends_with('\n') {
format!("{response}\r")
} else {
format!("{response}\r\n")
};
{
let mut write = write_half_arc.lock().await;
let _ = write
.write_all(formatted.as_bytes())
.await;
let _ = write.flush().await;
}
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(formatted.len() as u64),
None,
Some(1),
)
.await;
let preview = crate::utils::truncate_for_log(
&formatted, 100,
);
debug!(
"IRC sent {} bytes on connection {}: {}",
formatted.len(),
connection_id,
preview.trim()
);
let _ = status_clone.send(format!("[DEBUG] IRC sent {} bytes on connection {}: {}", formatted.len(), connection_id, preview.trim()));
trace!(
"IRC sent (text): {:?}",
formatted.trim()
);
let _ = status_clone.send(format!(
"[TRACE] IRC sent (text): {:?}",
formatted.trim()
));
}
ActionResult::CloseConnection => {
asked_to_close = true;
break;
}
_ => {}
}
}
let decision = if wrote_output {
"model_answer"
} else if asked_to_close {
"model_reject"
} else if failures == 0 {
"model_silent"
} else {
"fail_closed_bad_action"
};
let summary = format!(
"IRC {} on connection {} from {} decision={} ({} failed action(s))",
irc_command_token(&line),
connection_id,
remote_addr,
decision,
failures
);
if decision == "fail_closed_bad_action" {
error!("{}", summary);
let _ =
status_clone.send(format!("[ERROR] {}", summary));
} else if decision == "model_silent" {
tracing::warn!("{}", summary);
let _ =
status_clone.send(format!("[WARN] {}", summary));
} else {
info!("{}", summary);
let _ =
status_clone.send(format!("[INFO] {}", summary));
}
if asked_to_close {
break;
}
}
Err(e) => {
let token = if crate::utils::WireFailure::classify(&e)
.is_overloaded()
{
"fail_closed_llm_overloaded"
} else {
"fail_closed_llm_error"
};
let command = irc_command_token(&line);
error!(
"IRC {} on connection {} from {} decision={}: {}",
command, connection_id, remote_addr, token, e
);
let _ = status_clone.send(format!(
"[ERROR] IRC {} on connection {} from {} decision={}",
command, connection_id, remote_addr, token
));
let (reply, close) = irc_failure_reply(&command, &e);
let _ = status_clone.send(format!(
"[ERROR] IRC connection {} replying: {}",
connection_id,
reply.trim_end()
));
{
let mut write = write_half_arc.lock().await;
let _ = write.write_all(reply.as_bytes()).await;
let _ = write.flush().await;
if close {
let _ = write.shutdown().await;
}
}
state_clone
.update_connection_stats(
server_id,
connection_id,
None,
Some(reply.len() as u64),
None,
Some(1),
)
.await;
if close {
break;
}
}
}
}
state_clone
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_clone.send("__UPDATE_UI__".to_string());
}).await;
}
Err(e) => {
error!("Failed to accept IRC connection: {}", e);
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
fn irc_command_token(line: &str) -> String {
let trimmed = line.trim();
let without_prefix = if let Some(rest) = trimmed.strip_prefix(':') {
rest.split_once(' ').map(|(_, r)| r).unwrap_or("")
} else {
trimmed
};
let token: String = without_prefix
.split_whitespace()
.next()
.unwrap_or("*")
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect();
if token.is_empty() {
"*".to_string()
} else {
token.to_uppercase()
}
}
fn irc_failure_reply(command: &str, err: &anyhow::Error) -> (String, bool) {
let failure = crate::utils::WireFailure::classify(err);
let text = failure.prefixed_text();
if failure.is_overloaded() {
(format!(":netget 400 * {command} :{text}\r\n"), false)
} else {
(
format!(
":netget 400 * {command} :{text}\r\nERROR :Closing link: netget backend unavailable\r\n"
),
true,
)
}
}