use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
use tracing::{debug, info};
use crate::events::EventHandler;
use crate::llm::OllamaClient;
use crate::settings::Settings;
use crate::state::app_state::{AppState, Mode};
pub(crate) fn protocol_owns_stdout(protocol: &str) -> bool {
protocol.eq_ignore_ascii_case("stdio")
}
pub(crate) async fn server_owns_stdout(state: &AppState) -> bool {
state
.get_all_servers()
.await
.iter()
.any(|s| protocol_owns_stdout(&s.protocol_name))
}
pub(crate) fn actions_launch_stdout_owner(actions: &[serde_json::Value]) -> bool {
actions.iter().any(|a| {
if a.get("type").and_then(|v| v.as_str()) != Some("open_server") {
return false;
}
a.get("protocol")
.or_else(|| a.get("base_stack"))
.and_then(|v| v.as_str())
.map(protocol_owns_stdout)
.unwrap_or(false)
})
}
pub(crate) fn emit_status_line(line: &str, to_stderr: bool) {
use std::io::Write;
let line = crate::utils::sanitize::multiline(line);
if to_stderr {
let mut sink = std::io::stderr().lock();
let _ = writeln!(sink, "{line}");
let _ = sink.flush();
} else {
let mut sink = std::io::stdout().lock();
let _ = writeln!(sink, "{line}");
let _ = sink.flush();
}
}
#[derive(Debug, Clone)]
pub(crate) struct RunLimits {
exit_after_events: Option<u64>,
run_for: Option<Duration>,
started: crate::utils::clock::Instant,
}
impl RunLimits {
pub(crate) fn from_args(args: &super::Args) -> Self {
Self {
exit_after_events: args.exit_after_events,
run_for: args.run_for.map(Duration::from_secs),
started: crate::utils::clock::Instant::now(),
}
}
pub(crate) async fn reached(&self, state: &AppState) -> Option<String> {
if let Some(run_for) = self.run_for {
if self.started.elapsed() >= run_for {
return Some(format!("--run-for {}s elapsed", run_for.as_secs()));
}
}
if let Some(limit) = self.exit_after_events {
let handled = state.access_log_total().await;
if handled >= limit {
return Some(format!(
"--exit-after-events {limit}: {handled} network event(s) handled"
));
}
}
None
}
}
fn ctrl_c_flag() -> Arc<Mutex<bool>> {
let shutdown = Arc::new(Mutex::new(false));
let shutdown_clone = shutdown.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
*shutdown_clone.lock().await = true;
});
shutdown
}
pub async fn run_non_interactive(
prompt: String,
args: &super::Args,
settings: Settings,
) -> Result<()> {
info!("Starting NetGet in non-interactive mode");
debug!("Prompt: {}", prompt);
let limits = RunLimits::from_args(args);
let base_url = args
.openai_url
.clone()
.or_else(|| args.ollama_url.clone())
.unwrap_or_else(|| "http://localhost:11434".to_string());
let state = AppState::new_with_options(args.include_disabled_protocols, base_url);
state.set_min_stability(args.parse_min_stability()?).await;
let rate_limiter_config = args.build_rate_limiter_config();
state.configure_rate_limiter(rate_limiter_config).await?;
let configured_model = args.model.clone().or(settings.model.clone());
let is_openai = args.openai_url.is_some();
let selected_model = if is_openai {
configured_model
.ok_or_else(|| anyhow::anyhow!("--model is required when using --openai-url"))?
} else {
let ollama_url_for_model = args
.ollama_url
.as_deref()
.unwrap_or("http://localhost:11434");
crate::llm::select_or_validate_model(configured_model, false, ollama_url_for_model)
.await?
.ok_or_else(|| anyhow::anyhow!("No model available"))?
};
info!("✓ Using model: {}", selected_model);
state.set_ollama_model(Some(selected_model)).await;
let mode_to_set = if let Some(mode) = args.parse_scripting_mode()? {
Some(mode)
} else {
settings.parse_scripting_mode()
};
if let Some(mode) = mode_to_set {
let scripting_env = state.get_scripting_env().await;
let available = match mode {
crate::state::app_state::ScriptingMode::On => true, crate::state::app_state::ScriptingMode::Off => true, crate::state::app_state::ScriptingMode::Python => scripting_env.python.is_some(),
crate::state::app_state::ScriptingMode::JavaScript => {
scripting_env.javascript.is_some()
}
crate::state::app_state::ScriptingMode::Go => scripting_env.go.is_some(),
crate::state::app_state::ScriptingMode::Perl => scripting_env.perl.is_some(),
};
if !available {
anyhow::bail!(
"{} environment is not available on this system. Please install it or choose a different environment.",
mode
);
}
state.set_selected_scripting_mode(mode).await;
debug!("Using scripting mode: {}", mode);
}
if let Some(handler_mode) = args.parse_event_handler_mode()? {
state.set_event_handler_mode(handler_mode).await;
debug!("Using event handler mode: {}", handler_mode);
}
let mut web_search_mode = settings.get_web_search_mode();
if web_search_mode == crate::state::app_state::WebSearchMode::Ask {
debug!("Web search mode ASK is not supported in non-interactive mode, using OFF instead");
web_search_mode = crate::state::app_state::WebSearchMode::Off;
}
state.set_web_search_mode(web_search_mode).await;
debug!("Web search mode: {:?}", web_search_mode);
let llm = super::create_llm_client(args)?.with_mock_config_file(args.mock_config_file.clone());
state.set_llm_client(llm.clone()).await;
let mut event_handler = EventHandler::new(state.clone(), llm.clone());
let (status_tx, mut status_rx) = mpsc::unbounded_channel::<String>();
let status_state = state.clone();
let _status_forwarder = tokio::spawn(async move {
while let Some(msg) = status_rx.recv().await {
if !msg.starts_with("__") {
let clean_msg = msg
.strip_prefix("[INFO] ")
.unwrap_or(&msg)
.strip_prefix("[ERROR] ")
.unwrap_or(&msg)
.strip_prefix("[WARN] ")
.unwrap_or(&msg)
.strip_prefix("[DEBUG] ")
.unwrap_or(&msg);
emit_status_line(clean_msg, server_owns_stdout(&status_state).await);
}
}
});
tokio::task::yield_now().await;
event_handler
.handle_interpret_with_actions(prompt, status_tx.clone(), None)
.await?;
tokio::time::sleep(Duration::from_millis(500)).await;
{
use std::io::Write;
std::io::stdout().flush().ok();
}
if state.get_mode().await == Mode::Server || !state.get_all_servers().await.is_empty() {
let (_new_status_tx, new_status_rx) = mpsc::unbounded_channel::<String>();
return run_server(&state, llm, new_status_rx, limits).await;
}
run_clients(&state, &llm, &limits, false).await;
Ok(())
}
pub(crate) async fn run_clients(
state: &AppState,
llm: &OllamaClient,
limits: &RunLimits,
to_stderr: bool,
) {
let shutdown = ctrl_c_flag();
let mut ticker = tokio::time::interval(Duration::from_secs(1));
let (status_tx, mut status_rx) = mpsc::unbounded_channel::<String>();
let owner = tokio_util::sync::CancellationToken::new();
let _cancel_on_exit = owner.clone().drop_guard();
loop {
while let Ok(message) = status_rx.try_recv() {
if !message.starts_with("__") {
emit_status_line(&message, to_stderr);
}
}
if !has_live_background_work(state).await {
return;
}
if *shutdown.lock().await {
emit_status_line("\nShutting down background work...", to_stderr);
return;
}
if let Some(reason) = limits.reached(state).await {
emit_status_line(&format!("Stopping: {reason}."), to_stderr);
return;
}
tokio::select! {
_ = ticker.tick() => {
crate::cli::execute_due_tasks_owned_public(state, llm, &status_tx, owner.clone()).await;
crate::llm::feedback::execute_due_feedback(state, llm, &status_tx).await;
}
Some(message) = status_rx.recv() => {
if !message.starts_with("__") {
emit_status_line(&message, to_stderr);
}
}
_ = tokio::time::sleep(Duration::from_millis(100)) => {}
}
}
}
async fn has_live_background_work(state: &AppState) -> bool {
state.get_all_clients().await.iter().any(|client| {
matches!(
client.status,
crate::state::ClientStatus::Connecting | crate::state::ClientStatus::Connected
)
}) || state.get_all_servers().await.iter().any(|server| {
matches!(
server.status,
crate::state::server::ServerStatus::Starting
| crate::state::server::ServerStatus::Running
)
}) || state.get_all_tasks().await.iter().any(|task| {
matches!(
task.status,
crate::state::task::TaskStatus::Scheduled | crate::state::task::TaskStatus::Executing
)
})
}
pub(crate) async fn run_server(
state: &AppState,
llm: OllamaClient,
mut status_rx: mpsc::UnboundedReceiver<String>,
limits: RunLimits,
) -> Result<()> {
let (status_tx, mut server_status_rx) = mpsc::unbounded_channel::<String>();
let to_stderr = server_owns_stdout(state).await;
let mut servers = state.get_all_servers().await;
servers.sort_by_key(|s| s.id.as_u32());
if servers.is_empty() {
return Err(anyhow::anyhow!(
"No server configured. Use a command like 'listen on port 8080 via http'"
));
}
let failed: Vec<String> = servers
.iter()
.filter_map(|s| match &s.status {
crate::state::server::ServerStatus::Error(e) => Some(format!(
"server #{} ({}): {}",
s.id.as_u32(),
s.protocol_name,
e
)),
_ => None,
})
.collect();
if !failed.is_empty() {
return Err(anyhow::anyhow!(
"{} server(s) failed to start: {}",
failed.len(),
failed.join("; ")
));
}
for server in &servers {
let at = server
.local_addr
.map(|a| format!(" on {a}"))
.unwrap_or_default();
emit_status_line(
&format!(
"Server #{} ({}) is running{at}.",
server.id.as_u32(),
server.protocol_name
),
to_stderr,
);
}
emit_status_line("Press Ctrl+C to stop.", to_stderr);
emit_status_line("Waiting for connections...\n", to_stderr);
let shutdown = ctrl_c_flag();
use tokio::time::interval;
let mut task_execution_interval = interval(Duration::from_secs(1));
let owner = tokio_util::sync::CancellationToken::new();
let _cancel_on_exit = owner.clone().drop_guard();
loop {
tokio::select! {
_ = tokio::time::sleep(Duration::from_millis(100)) => {
if *shutdown.lock().await {
emit_status_line("\nShutting down server...", to_stderr);
break;
}
if let Some(reason) = limits.reached(state).await {
emit_status_line(&format!("Stopping: {reason}."), to_stderr);
break;
}
let live = state
.get_all_servers()
.await
.iter()
.filter(|s| {
matches!(
s.status,
crate::state::server::ServerStatus::Starting
| crate::state::server::ServerStatus::Running
)
})
.count();
if live == 0 && !has_live_background_work(state).await {
emit_status_line("All servers have stopped.", to_stderr);
break;
}
while let Ok(msg) = status_rx.try_recv() {
if !msg.starts_with("__") {
emit_status_line(&format!("[STATUS] {msg}"), to_stderr);
}
}
while let Ok(msg) = server_status_rx.try_recv() {
emit_status_line(&format!("[STATUS] {msg}"), to_stderr);
}
}
_ = task_execution_interval.tick() => {
crate::cli::execute_due_tasks_owned_public(state, &llm, &status_tx, owner.clone()).await;
crate::llm::feedback::execute_due_feedback(state, &llm, &status_tx).await;
}
}
}
emit_status_line("Server stopped.", to_stderr);
Ok(())
}
pub async fn run_with_actions(
actions: Vec<serde_json::Value>,
args: &super::Args,
settings: Settings,
) -> Result<()> {
info!("Starting NetGet in non-interactive mode (actions JSON)");
debug!("Loading {} actions", actions.len());
let limits = RunLimits::from_args(args);
let base_url = args
.openai_url
.clone()
.or_else(|| args.ollama_url.clone())
.unwrap_or_else(|| "http://localhost:11434".to_string());
let state = AppState::new_with_options(args.include_disabled_protocols, base_url);
state.set_min_stability(args.parse_min_stability()?).await;
let rate_limiter_config = args.build_rate_limiter_config();
state.configure_rate_limiter(rate_limiter_config).await?;
let mode_to_set = if let Some(mode) = args.parse_scripting_mode()? {
Some(mode)
} else {
settings.parse_scripting_mode()
};
if let Some(mode) = mode_to_set {
state.set_selected_scripting_mode(mode).await;
}
if let Some(handler_mode) = args.parse_event_handler_mode()? {
state.set_event_handler_mode(handler_mode).await;
}
let mut web_search_mode = settings.get_web_search_mode();
if web_search_mode == crate::state::app_state::WebSearchMode::Ask {
web_search_mode = crate::state::app_state::WebSearchMode::Off;
}
state.set_web_search_mode(web_search_mode).await;
let llm = super::create_llm_client(args)?.with_mock_config_file(args.mock_config_file.clone());
state.set_llm_client(llm.clone()).await;
let to_stderr = actions_launch_stdout_owner(&actions);
let (status_tx, mut status_rx) = mpsc::unbounded_channel::<String>();
let status_printer = tokio::spawn(async move {
while let Some(msg) = status_rx.recv().await {
if !msg.starts_with("__") {
emit_status_line(&msg, to_stderr);
}
}
});
emit_status_line(
&format!("Loading {} action(s)...\n", actions.len()),
to_stderr,
);
let mut failures: Vec<String> = Vec::new();
for (i, action) in actions.iter().enumerate() {
if action.get("type").and_then(serde_json::Value::as_str) == Some("restore_session") {
if let Err(error) =
crate::utils::save_load::restore_session(&state, &llm, &action["session"]).await
{
failures.push(format!("action {} (session): {error:#}", i + 1));
}
continue;
}
if let Ok(common_action) = crate::llm::actions::common::CommonAction::from_json(action) {
use crate::cli::{client_startup, server_startup};
use crate::llm::actions::common::CommonAction;
match common_action {
CommonAction::OpenServer {
mac_address,
interface,
host,
port,
protocol,
send_first,
initial_memory,
instruction,
startup_params,
event_handlers,
scheduled_tasks,
feedback_instructions,
} => {
match server_startup::start_server_from_action(
&state,
mac_address,
interface.clone(),
host,
port,
&protocol,
send_first,
initial_memory,
instruction.clone(),
startup_params,
event_handlers,
scheduled_tasks,
feedback_instructions,
status_tx.clone(),
)
.await
{
Ok(server_id) => {
let binding_desc = if let Some(iface) = &interface {
format!("interface {} ({})", iface, protocol)
} else if let Some(p) = port {
format!("port {} ({})", p, protocol)
} else {
format!("({})", protocol)
};
emit_status_line(
&format!(
"[{}] Opened server #{} on {}",
i + 1,
server_id.as_u32(),
binding_desc
),
to_stderr,
);
}
Err(e) => {
eprintln!("[{}] Failed to open server: {}", i + 1, e);
failures.push(format!("action {} ({} server): {}", i + 1, protocol, e));
}
}
}
CommonAction::OpenClient {
protocol,
remote_addr,
instruction,
startup_params,
initial_memory,
event_handlers,
scheduled_tasks,
feedback_instructions,
} => {
match client_startup::start_client_from_action(
&state,
&protocol,
&remote_addr,
instruction.clone(),
startup_params,
initial_memory,
event_handlers,
scheduled_tasks,
feedback_instructions,
llm.clone(),
None, )
.await
{
Ok(client_id) => {
emit_status_line(
&format!(
"[{}] Opened client #{} to {} ({})",
i + 1,
client_id.as_u32(),
remote_addr,
protocol
),
to_stderr,
);
}
Err(e) => {
eprintln!("[{}] Failed to open client: {}", i + 1, e);
failures.push(format!("action {} ({} client): {}", i + 1, protocol, e));
}
}
}
CommonAction::ShowMessage { message } => {
emit_status_line(&format!("[{}] {}", i + 1, message), to_stderr);
}
_ => {
emit_status_line(
&format!("[{}] Skipping unsupported action type", i + 1),
to_stderr,
);
}
}
} else {
eprintln!("[{}] Skipping invalid action", i + 1);
}
}
drop(status_tx);
for _ in 0..8 {
tokio::task::yield_now().await;
}
drop(status_printer);
if !failures.is_empty() {
return Err(anyhow::anyhow!(
"{} of {} action(s) failed to start: {}",
failures.len(),
actions.len(),
failures.join("; ")
));
}
emit_status_line("\nConfiguration loaded successfully.", to_stderr);
if !state.get_all_servers().await.is_empty() {
let (_status_tx, status_rx) = mpsc::unbounded_channel::<String>();
return run_server(&state, llm, status_rx, limits).await;
}
run_clients(&state, &llm, &limits, to_stderr).await;
Ok(())
}