Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "nomi-cli"
|
||||
description = "CLI binary for Nomi — a multi-provider AI agent with tool orchestration"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
keywords = ["ai", "agent", "cli", "llm", "anthropic"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
[[bin]]
|
||||
name = "nomi"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
nomi-agent = { workspace = true, features = ["computer-use"] }
|
||||
nomi-compact.workspace = true
|
||||
nomi-config.workspace = true
|
||||
nomi-memory.workspace = true
|
||||
nomi-providers.workspace = true
|
||||
nomi-tools.workspace = true
|
||||
nomi-mcp.workspace = true
|
||||
nomi-protocol.workspace = true
|
||||
nomi-skills.workspace = true
|
||||
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
@@ -0,0 +1,737 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use nomi_agent::bootstrap::AgentBootstrap;
|
||||
use nomi_agent::output::OutputSink;
|
||||
use nomi_agent::output::protocol_sink::ProtocolSink;
|
||||
use nomi_agent::output::terminal::TerminalSink;
|
||||
use nomi_agent::session;
|
||||
use nomi_config::auth;
|
||||
use nomi_config::config::{self, CliArgs, Config, McpServerConfig, TransportType};
|
||||
use nomi_mcp::manager::McpManager;
|
||||
use nomi_mcp::tool_proxy::register_single_server_tools;
|
||||
use nomi_protocol::commands::{ApprovalScope, ProtocolCommand};
|
||||
use nomi_protocol::events::ProtocolEvent;
|
||||
use nomi_protocol::reader::spawn_stdin_reader;
|
||||
use nomi_protocol::writer::{ProtocolEmitter, ProtocolWriter};
|
||||
use nomi_protocol::{ToolApprovalManager, ToolApprovalResult};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "nomi",
|
||||
about = "Nomi agent CLI — multi-provider AI agent with tool orchestration",
|
||||
version
|
||||
)]
|
||||
struct Cli {
|
||||
/// Provider: "anthropic" or "openai"
|
||||
#[arg(short, long, env = "PROVIDER")]
|
||||
provider: Option<String>,
|
||||
|
||||
/// API key
|
||||
#[arg(short = 'k', long, env = "API_KEY")]
|
||||
api_key: Option<String>,
|
||||
|
||||
/// Base URL for the API
|
||||
#[arg(short, long, env = "BASE_URL")]
|
||||
base_url: Option<String>,
|
||||
|
||||
/// Model name
|
||||
#[arg(short, long, env = "MODEL")]
|
||||
model: Option<String>,
|
||||
|
||||
/// Max output tokens per response
|
||||
#[arg(long)]
|
||||
max_tokens: Option<u32>,
|
||||
|
||||
/// Max agent loop turns
|
||||
#[arg(long)]
|
||||
max_turns: Option<usize>,
|
||||
|
||||
/// Custom system prompt
|
||||
#[arg(long)]
|
||||
system_prompt: Option<String>,
|
||||
|
||||
/// Named profile from config file
|
||||
#[arg(long)]
|
||||
profile: Option<String>,
|
||||
|
||||
/// Auto-approve all tool executions (skip confirmation)
|
||||
#[arg(long)]
|
||||
auto_approve: bool,
|
||||
|
||||
/// Project directory to load .nomi.toml from (defaults to CWD)
|
||||
#[arg(long)]
|
||||
project_dir: Option<std::path::PathBuf>,
|
||||
|
||||
/// Resume a previous session
|
||||
#[arg(long)]
|
||||
resume: Option<String>,
|
||||
|
||||
/// Use a specific session ID (instead of auto-generating one)
|
||||
#[arg(long)]
|
||||
session_id: Option<String>,
|
||||
|
||||
/// List saved sessions
|
||||
#[arg(long)]
|
||||
list_sessions: bool,
|
||||
|
||||
/// Disable colored output
|
||||
#[arg(long)]
|
||||
no_color: bool,
|
||||
|
||||
/// Enable JSON streaming mode for host client integration
|
||||
#[arg(long)]
|
||||
json_stream: bool,
|
||||
|
||||
/// Generate a default config file
|
||||
#[arg(long)]
|
||||
init_config: bool,
|
||||
|
||||
/// Print config file path and exit
|
||||
#[arg(long)]
|
||||
config_path: bool,
|
||||
|
||||
/// Print skill directory paths and exit
|
||||
#[arg(long)]
|
||||
skills_path: bool,
|
||||
|
||||
/// Login with Anthropic account (OAuth device flow)
|
||||
#[arg(long)]
|
||||
login: bool,
|
||||
|
||||
/// Logout (remove saved OAuth credentials)
|
||||
#[arg(long)]
|
||||
logout: bool,
|
||||
|
||||
/// Output compaction level: off, safe (default), full
|
||||
#[arg(long)]
|
||||
compaction: Option<String>,
|
||||
|
||||
/// Enable TOON encoding for JSON arrays (session-level, cannot change mid-conversation)
|
||||
#[arg(long)]
|
||||
toon: bool,
|
||||
|
||||
/// Log directory (enables file logging)
|
||||
#[arg(long)]
|
||||
log_dir: Option<String>,
|
||||
|
||||
/// Log level filter (e.g. "info", "debug", "info,nomi_providers=debug")
|
||||
#[arg(long)]
|
||||
log_level: Option<String>,
|
||||
|
||||
/// Initial prompt (if omitted, enters interactive REPL mode)
|
||||
#[arg(trailing_var_arg = true)]
|
||||
prompt: Vec<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
if cli.resume.is_some() && cli.session_id.is_some() {
|
||||
anyhow::bail!("Cannot use --resume and --session-id together");
|
||||
}
|
||||
|
||||
// Handle --config-path
|
||||
if cli.config_path {
|
||||
println!("{}", config::global_config_path().display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle --skills-path
|
||||
if cli.skills_path {
|
||||
print_skills_paths();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Handle --init-config
|
||||
if cli.init_config {
|
||||
return config::init_config();
|
||||
}
|
||||
|
||||
// Handle --login / --logout
|
||||
if cli.login || cli.logout {
|
||||
let oauth = auth::OAuthManager::new(auth::AuthConfig::default());
|
||||
if cli.login {
|
||||
oauth.login().await?;
|
||||
eprintln!("Login successful! You can now use nomi without --api-key.");
|
||||
} else {
|
||||
oauth.logout()?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let terminal = Arc::new(TerminalSink::new(cli.no_color));
|
||||
let output: Arc<dyn OutputSink> = terminal.clone();
|
||||
|
||||
// Resolve config from files + CLI args + env vars
|
||||
let cli_args = CliArgs {
|
||||
provider: cli.provider,
|
||||
api_key: cli.api_key,
|
||||
base_url: cli.base_url,
|
||||
model: cli.model,
|
||||
max_tokens: cli.max_tokens,
|
||||
max_turns: cli.max_turns,
|
||||
system_prompt: cli.system_prompt,
|
||||
profile: cli.profile,
|
||||
auto_approve: cli.auto_approve,
|
||||
project_dir: cli.project_dir,
|
||||
};
|
||||
|
||||
let mut config = Config::resolve(&cli_args)?;
|
||||
|
||||
if let Some(ref level_str) = cli.compaction {
|
||||
match level_str.parse::<nomi_compact::CompactionLevel>() {
|
||||
Ok(level) => config.compact.compaction = level,
|
||||
Err(e) => anyhow::bail!("Invalid --compaction value: {e}"),
|
||||
}
|
||||
}
|
||||
if cli.toon {
|
||||
config.compact.toon = true;
|
||||
}
|
||||
|
||||
let _log_guard = {
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
let resolved = config
|
||||
.logging
|
||||
.resolve(cli.log_dir.as_deref(), cli.log_level.as_deref());
|
||||
if resolved.enabled {
|
||||
match nomi_config::logging::create_file_layer(&resolved) {
|
||||
Ok((layer, guard)) => {
|
||||
tracing_subscriber::registry().with(layer).init();
|
||||
Some(guard)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: failed to initialize logging: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let cwd = std::env::current_dir()?.to_string_lossy().to_string();
|
||||
|
||||
// Handle --list-sessions
|
||||
if cli.list_sessions {
|
||||
let session_mgr = session::SessionManager::new(
|
||||
config.session.directory.clone().into(),
|
||||
config.session.max_sessions,
|
||||
);
|
||||
let sessions = session_mgr.list()?;
|
||||
if sessions.is_empty() {
|
||||
eprintln!("No saved sessions.");
|
||||
} else {
|
||||
eprintln!(
|
||||
"{:<8} {:<12} {:<30} {:>5} Summary",
|
||||
"ID", "Date", "Model", "Msgs"
|
||||
);
|
||||
for s in &sessions {
|
||||
eprintln!(
|
||||
"{:<8} {:<12} {:<30} {:>5} {}",
|
||||
s.id,
|
||||
s.created_at.format("%Y-%m-%d"),
|
||||
s.model,
|
||||
s.message_count,
|
||||
s.summary
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Branch to JSON stream mode
|
||||
if cli.json_stream {
|
||||
return run_json_stream_mode(config, &cwd, cli.resume, cli.session_id).await;
|
||||
}
|
||||
|
||||
let provider_name = config.provider_label.clone();
|
||||
|
||||
// Bootstrap engine with full feature initialization
|
||||
let mut bootstrap = AgentBootstrap::new(config, &cwd, output.clone());
|
||||
|
||||
if let Some(resume_id) = &cli.resume {
|
||||
let cfg = bootstrap.config();
|
||||
let session_mgr = session::SessionManager::new(
|
||||
cfg.session.directory.clone().into(),
|
||||
cfg.session.max_sessions,
|
||||
);
|
||||
let session = session_mgr.load(resume_id)?;
|
||||
terminal.formatter().session_info(&format!(
|
||||
"Resumed session {} ({} messages, {} model)",
|
||||
session.id,
|
||||
session.messages.len(),
|
||||
session.model
|
||||
));
|
||||
bootstrap = bootstrap.resume(session);
|
||||
}
|
||||
|
||||
let result = bootstrap.build().await?;
|
||||
let mut engine = result.engine;
|
||||
|
||||
if cli.resume.is_none() {
|
||||
engine.init_session(&provider_name, &cwd, cli.session_id.as_deref())?;
|
||||
}
|
||||
|
||||
let prompt = cli.prompt.join(" ");
|
||||
if prompt.is_empty() {
|
||||
repl_loop(&mut engine, &terminal, &output).await?;
|
||||
} else {
|
||||
let run_result = engine.run(&prompt, "").await?;
|
||||
output.emit_stream_end(
|
||||
"",
|
||||
run_result.turns,
|
||||
run_result.usage.input_tokens,
|
||||
run_result.usage.output_tokens,
|
||||
run_result.usage.cache_creation_tokens,
|
||||
run_result.usage.cache_read_tokens,
|
||||
);
|
||||
}
|
||||
|
||||
engine.run_stop_hooks().await;
|
||||
|
||||
for mgr in &result.mcp_managers {
|
||||
mgr.shutdown().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn repl_loop(
|
||||
engine: &mut nomi_agent::engine::AgentEngine,
|
||||
terminal: &Arc<TerminalSink>,
|
||||
output: &Arc<dyn OutputSink>,
|
||||
) -> anyhow::Result<()> {
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
loop {
|
||||
terminal.formatter().repl_prompt();
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().lock().read_line(&mut input)?;
|
||||
let input = input.trim();
|
||||
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
match engine.run(input, "").await {
|
||||
Ok(result) => {
|
||||
if result.turns > 0 {
|
||||
output.emit_stream_end(
|
||||
"",
|
||||
result.turns,
|
||||
result.usage.input_tokens,
|
||||
result.usage.output_tokens,
|
||||
result.usage.cache_creation_tokens,
|
||||
result.usage.cache_read_tokens,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(nomi_agent::engine::AgentError::UserAborted) => break,
|
||||
Err(e) => {
|
||||
output.emit_error(&e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_skills_paths() {
|
||||
use nomi_skills::paths::{
|
||||
project_commands_dirs, project_skills_dirs, user_commands_dir, user_skills_dir,
|
||||
};
|
||||
|
||||
fn status(p: &Path) -> &'static str {
|
||||
if p.is_dir() { "exists" } else { "not found" }
|
||||
}
|
||||
|
||||
// User-level
|
||||
match user_skills_dir() {
|
||||
Some(dir) => println!("User: {} ({})", dir.display(), status(&dir)),
|
||||
None => println!("User: <unable to determine config directory>"),
|
||||
}
|
||||
|
||||
// Project-level
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let project_dirs = project_skills_dirs(&cwd);
|
||||
if project_dirs.is_empty() {
|
||||
println!("Project: <none found>");
|
||||
} else {
|
||||
for dir in &project_dirs {
|
||||
println!("Project: {} ({})", dir.display(), status(dir));
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy commands
|
||||
let mut has_legacy = false;
|
||||
if let Some(dir) = user_commands_dir()
|
||||
&& dir.is_dir()
|
||||
{
|
||||
println!("Legacy: {} ({})", dir.display(), status(&dir));
|
||||
has_legacy = true;
|
||||
}
|
||||
for dir in project_commands_dirs(&cwd) {
|
||||
println!("Legacy: {} ({})", dir.display(), status(&dir));
|
||||
has_legacy = true;
|
||||
}
|
||||
if !has_legacy {
|
||||
println!("Legacy: <none found>");
|
||||
}
|
||||
}
|
||||
|
||||
fn to_mcp_server_config(
|
||||
transport: &str,
|
||||
command: Option<String>,
|
||||
args: Option<Vec<String>>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
url: Option<String>,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
) -> Result<McpServerConfig, String> {
|
||||
let transport_type = match transport {
|
||||
"stdio" => TransportType::Stdio,
|
||||
"sse" => TransportType::Sse,
|
||||
"streamable-http" | "streamable_http" => TransportType::StreamableHttp,
|
||||
other => return Err(format!("unknown transport: {other}")),
|
||||
};
|
||||
Ok(McpServerConfig {
|
||||
transport: transport_type,
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
url,
|
||||
headers,
|
||||
deferred: Some(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Pending config fields: (model, thinking, thinking_budget, effort)
|
||||
type PendingConfig = (
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
Option<u32>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
async fn run_json_stream_mode(
|
||||
config: Config,
|
||||
cwd: &str,
|
||||
resume: Option<String>,
|
||||
session_id: Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let writer = Arc::new(ProtocolWriter::new());
|
||||
let protocol_sink = Arc::new(ProtocolSink::new(writer.clone()));
|
||||
let approval_manager = Arc::new(ToolApprovalManager::new());
|
||||
let output: Arc<dyn OutputSink> = protocol_sink.clone();
|
||||
|
||||
let provider_name = config.provider_label.clone();
|
||||
|
||||
// Bootstrap engine with full feature initialization
|
||||
// P3-X1: pass the same approval manager into bootstrap so the native BrowserTool's redline
|
||||
// gate reads the LIVE runtime session mode (a SetMode command flips it immediately) instead
|
||||
// of the construction-time auto_approve snapshot.
|
||||
let mut bootstrap =
|
||||
AgentBootstrap::new(config, cwd, output.clone()).approval_manager(approval_manager.clone());
|
||||
|
||||
if let Some(resume_id) = &resume {
|
||||
let cfg = bootstrap.config();
|
||||
let session_mgr = session::SessionManager::new(
|
||||
cfg.session.directory.clone().into(),
|
||||
cfg.session.max_sessions,
|
||||
);
|
||||
let session = session_mgr.load(resume_id)?;
|
||||
bootstrap = bootstrap.resume(session);
|
||||
}
|
||||
|
||||
let result = bootstrap.build().await?;
|
||||
let mut engine = result.engine;
|
||||
let initial_has_mcp = result.has_mcp;
|
||||
|
||||
if resume.is_none() {
|
||||
engine.init_session(&provider_name, cwd, session_id.as_deref())?;
|
||||
}
|
||||
|
||||
let sid = engine.current_session_id();
|
||||
protocol_sink.emit_ready(
|
||||
engine.compat(),
|
||||
initial_has_mcp,
|
||||
sid,
|
||||
&approval_manager.current_mode(),
|
||||
);
|
||||
|
||||
engine.set_approval_manager(approval_manager.clone());
|
||||
engine.set_protocol_writer(writer.clone());
|
||||
|
||||
let mut cmd_rx = spawn_stdin_reader();
|
||||
|
||||
// --- Pre-message phase: accept AddMcpServer commands ---
|
||||
let mut dynamic_managers: Vec<Arc<McpManager>> = Vec::new();
|
||||
let mut first_cmd: Option<ProtocolCommand> = None;
|
||||
|
||||
while let Some(cmd) = cmd_rx.recv().await {
|
||||
match cmd {
|
||||
ProtocolCommand::AddMcpServer {
|
||||
name,
|
||||
transport,
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
url,
|
||||
headers,
|
||||
} => {
|
||||
tracing::info!(target: "nomi_mcp", %name, %transport, ?command, "AddMcpServer received");
|
||||
let config =
|
||||
match to_mcp_server_config(&transport, command, args, env, url, headers) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
output.emit_error(&format!("AddMcpServer '{name}': {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut single_configs = HashMap::new();
|
||||
single_configs.insert(name.clone(), config.clone());
|
||||
tracing::info!(target: "nomi_mcp", %name, "connecting to mcp server");
|
||||
match McpManager::connect_all(&single_configs).await {
|
||||
Ok(mgr) => {
|
||||
let tool_names: Vec<String> = mgr
|
||||
.all_tools()
|
||||
.iter()
|
||||
.map(|(_, t)| t.name.clone())
|
||||
.collect();
|
||||
tracing::info!(target: "nomi_mcp", %name, tools = tool_names.len(), "mcp server connected");
|
||||
let mgr_arc = Arc::new(mgr);
|
||||
let builtin_names = engine.tool_names();
|
||||
register_single_server_tools(
|
||||
engine.registry_mut(),
|
||||
&mgr_arc,
|
||||
&name,
|
||||
&builtin_names,
|
||||
config.deferred.unwrap_or(true),
|
||||
);
|
||||
dynamic_managers.push(mgr_arc);
|
||||
let _ = writer.emit(&ProtocolEvent::McpReady {
|
||||
name,
|
||||
tools: tool_names,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(target: "nomi_mcp", %name, error = %e, "mcp server connection failed");
|
||||
output.emit_error(&format!("AddMcpServer '{name}' failed: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
ProtocolCommand::Stop => return Ok(()),
|
||||
other => {
|
||||
first_cmd = Some(other);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_mcp = initial_has_mcp || !dynamic_managers.is_empty();
|
||||
let mut pending_cmd = first_cmd;
|
||||
|
||||
loop {
|
||||
let cmd = if let Some(c) = pending_cmd.take() {
|
||||
c
|
||||
} else {
|
||||
match cmd_rx.recv().await {
|
||||
Some(c) => c,
|
||||
None => break,
|
||||
}
|
||||
};
|
||||
|
||||
match cmd {
|
||||
ProtocolCommand::Message {
|
||||
msg_id,
|
||||
content,
|
||||
files: _,
|
||||
} => {
|
||||
let mut stopped = false;
|
||||
let mut pending_config: Option<PendingConfig> = None;
|
||||
let mut mode_changed = false;
|
||||
|
||||
{
|
||||
let engine_fut = engine.run(&content, &msg_id);
|
||||
tokio::pin!(engine_fut);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = &mut engine_fut => {
|
||||
match result {
|
||||
Ok(result) => {
|
||||
output.emit_stream_end(
|
||||
&msg_id,
|
||||
result.turns,
|
||||
result.usage.input_tokens,
|
||||
result.usage.output_tokens,
|
||||
result.usage.cache_creation_tokens,
|
||||
result.usage.cache_read_tokens,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
output.emit_error(&e.to_string());
|
||||
output.emit_stream_end(&msg_id, 0, 0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
Some(sub_cmd) = cmd_rx.recv() => {
|
||||
match sub_cmd {
|
||||
ProtocolCommand::ToolApprove { call_id, scope: _ } => {
|
||||
approval_manager.resolve(&call_id, ToolApprovalResult::Approved);
|
||||
}
|
||||
ProtocolCommand::ToolDeny { call_id, reason } => {
|
||||
approval_manager.resolve(&call_id, ToolApprovalResult::Denied { reason });
|
||||
}
|
||||
ProtocolCommand::Stop => {
|
||||
stopped = true;
|
||||
break;
|
||||
}
|
||||
ProtocolCommand::SetConfig { model, thinking, thinking_budget, effort, compaction } => {
|
||||
pending_config = Some((model, thinking, thinking_budget, effort, compaction));
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Info {
|
||||
msg_id: String::new(),
|
||||
message: "set_config: queued, will apply after current response".to_string(),
|
||||
});
|
||||
}
|
||||
ProtocolCommand::SetMode { mode } => {
|
||||
approval_manager.set_mode(mode);
|
||||
mode_changed = true;
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Info {
|
||||
msg_id: String::new(),
|
||||
message: format!("mode updated: {}", approval_manager.current_mode()),
|
||||
});
|
||||
}
|
||||
ProtocolCommand::Ping => {
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Pong);
|
||||
}
|
||||
_ => {
|
||||
tracing::debug!(target: "nomi_protocol", "ignoring command during active message processing");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((model, thinking, thinking_budget, effort, compaction)) =
|
||||
pending_config.take()
|
||||
{
|
||||
let changes = engine.apply_config_update(
|
||||
model,
|
||||
thinking,
|
||||
thinking_budget,
|
||||
effort,
|
||||
compaction,
|
||||
);
|
||||
if !changes.is_empty() {
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Info {
|
||||
msg_id: String::new(),
|
||||
message: format!("config applied: {}", changes.join(", ")),
|
||||
});
|
||||
}
|
||||
protocol_sink.emit_config_changed(
|
||||
engine.compat(),
|
||||
has_mcp,
|
||||
&approval_manager.current_mode(),
|
||||
);
|
||||
} else if mode_changed {
|
||||
protocol_sink.emit_config_changed(
|
||||
engine.compat(),
|
||||
has_mcp,
|
||||
&approval_manager.current_mode(),
|
||||
);
|
||||
}
|
||||
if stopped {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ProtocolCommand::Stop => {
|
||||
break;
|
||||
}
|
||||
ProtocolCommand::ToolApprove { call_id, scope } => {
|
||||
if matches!(scope, ApprovalScope::Always) {
|
||||
// Auto-approve all future calls of this tool's category
|
||||
}
|
||||
approval_manager.resolve(&call_id, ToolApprovalResult::Approved);
|
||||
}
|
||||
ProtocolCommand::ToolDeny { call_id, reason } => {
|
||||
approval_manager.resolve(&call_id, ToolApprovalResult::Denied { reason });
|
||||
}
|
||||
ProtocolCommand::InitHistory { text } => {
|
||||
tracing::debug!(target: "nomi_protocol", chars = text.len(), "InitHistory received");
|
||||
}
|
||||
ProtocolCommand::SetMode { mode } => {
|
||||
let mode_str = format!("{mode:?}").to_lowercase();
|
||||
approval_manager.set_mode(mode);
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Info {
|
||||
msg_id: String::new(),
|
||||
message: format!("mode updated: {}", approval_manager.current_mode()),
|
||||
});
|
||||
protocol_sink.emit_config_changed(
|
||||
engine.compat(),
|
||||
has_mcp,
|
||||
&approval_manager.current_mode(),
|
||||
);
|
||||
tracing::debug!(target: "nomi_protocol", mode = %mode_str, "SetMode applied");
|
||||
}
|
||||
ProtocolCommand::SetConfig {
|
||||
model,
|
||||
thinking,
|
||||
thinking_budget,
|
||||
effort,
|
||||
compaction,
|
||||
} => {
|
||||
let changes = engine.apply_config_update(
|
||||
model,
|
||||
thinking,
|
||||
thinking_budget,
|
||||
effort,
|
||||
compaction,
|
||||
);
|
||||
let message = if changes.is_empty() {
|
||||
"set_config: no changes".to_string()
|
||||
} else {
|
||||
format!("config updated: {}", changes.join(", "))
|
||||
};
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Info {
|
||||
msg_id: String::new(),
|
||||
message,
|
||||
});
|
||||
protocol_sink.emit_config_changed(
|
||||
engine.compat(),
|
||||
has_mcp,
|
||||
&approval_manager.current_mode(),
|
||||
);
|
||||
}
|
||||
ProtocolCommand::AddMcpServer { name, .. } => {
|
||||
output.emit_error(&format!(
|
||||
"AddMcpServer '{name}': rejected — only allowed before first Message"
|
||||
));
|
||||
}
|
||||
ProtocolCommand::Ping => {
|
||||
let _ = writer.emit(&nomi_protocol::events::ProtocolEvent::Pong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.run_stop_hooks().await;
|
||||
for mgr in &result.mcp_managers {
|
||||
mgr.shutdown().await;
|
||||
}
|
||||
for mgr in &dynamic_managers {
|
||||
mgr.shutdown().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user