use crate::command::chat::agent::config::{AgentLoopConfig, AgentLoopSharedState};
use crate::command::chat::app::AskRequest;
use crate::command::chat::app::types::StreamMsg;
use crate::command::chat::app::{MainAgentHandle, build_system_prompt_fn};
use crate::command::chat::context::compact::new_invoked_skills_map;
use crate::command::chat::context::window::select_messages;
use crate::command::chat::infra::hook::{HookContext, HookEvent, HookManager};
use crate::command::chat::infra::skill;
use crate::command::chat::oneshot::animation::{start_thinking_animation, stop_thinking_animation};
use crate::command::chat::oneshot::ask_ui::spawn_ask_handler;
use crate::command::chat::oneshot::display::{redraw_streaming_as_markdown, term_width};
use crate::command::chat::oneshot::session::{fire_session_end, persist_messages};
use crate::command::chat::oneshot::tool_exec::handle_tool_call;
use crate::command::chat::permission::JcliConfig;
use crate::command::chat::storage::{AgentConfig, ChatMessage, MessageRole, ModelProvider};
use crate::command::chat::tools::ToolRegistry;
use crate::command::chat::tools::background::BackgroundManager;
use crate::command::chat::tools::task::TaskManager;
use crate::command::chat::tools::todo::TodoManager;
use crate::error;
use crate::theme::Theme;
use colored::Colorize;
use std::io::{self, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
const ONESHOT_POLL_MS: u64 = 30;
pub(crate) fn run_oneshot_no_tools(
provider: &ModelProvider,
agent_config: &AgentConfig,
message: String,
prior_messages: Vec<ChatMessage>,
session_id: &str,
) {
use crate::command::chat::agent::api::call_llm_stream;
use crate::command::chat::oneshot::animation::start_thinking_animation;
use crate::command::chat::oneshot::display::redraw_markdown;
use crate::command::chat::oneshot::session::persist_messages;
let user_msg = ChatMessage::text(MessageRole::User, message.clone());
let mut messages = prior_messages.clone();
messages.push(user_msg.clone());
let thinking_style = agent_config.thinking_style;
let _stop_anim = start_thinking_animation(thinking_style);
let tw = term_width();
let mut cur_col: usize = 0;
let mut raw_lines: usize = 0;
let interrupted = Arc::new(AtomicBool::new(false));
let interrupted2 = Arc::clone(&interrupted);
let _ = ctrlc::set_handler(move || {
interrupted2.store(true, Ordering::Relaxed);
});
let send_messages = select_messages(
&messages,
agent_config.max_history_messages,
agent_config.max_context_tokens,
agent_config.compact.keep_recent,
&agent_config.compact.micro_compact_exempt_tools,
);
match call_llm_stream(
provider,
&send_messages,
crate::command::chat::storage::load_system_prompt().as_deref(),
&mut |chunk| {
if interrupted.load(Ordering::Relaxed) {
return;
}
print!("{}", chunk);
let _ = io::stdout().flush();
for ch in chunk.chars() {
if ch == '\n' {
raw_lines += 1;
cur_col = 0;
} else {
cur_col += 1;
if cur_col >= tw {
raw_lines += 1;
cur_col = 0;
}
}
}
},
) {
Ok(full_text) => {
if !full_text.is_empty() {
redraw_markdown(raw_lines, cur_col, &full_text);
persist_messages(session_id, &[user_msg], 0);
persist_messages(
session_id,
&[ChatMessage::text(MessageRole::Assistant, &full_text)],
0,
);
eprintln!("{} {}", "会话 ID:".dimmed(), session_id.dimmed());
}
}
Err(e) => {
error!("\n{}", e.display_message());
}
}
}
pub(crate) fn run_oneshot_agent(
provider: &ModelProvider,
agent_config: &AgentConfig,
message: String,
prior_messages: Vec<ChatMessage>,
session_id: &str,
bypass: bool,
) {
let thinking_style = agent_config.thinking_style;
let hook_manager_loaded = HookManager::load();
let hook_manager_for_end = hook_manager_loaded.clone();
let disabled_hooks: Vec<String> = vec![];
{
if hook_manager_loaded.has_hooks_for(HookEvent::SessionStart) {
let ctx = HookContext {
event: HookEvent::SessionStart,
messages: Some(prior_messages.clone()),
model: Some(provider.model.clone()),
session_id: Some(session_id.to_string()),
..Default::default()
};
hook_manager_loaded.execute(HookEvent::SessionStart, ctx, &disabled_hooks);
}
}
let (ask_tx, ask_rx) = std::sync::mpsc::channel::<AskRequest>();
let background_manager = Arc::new(BackgroundManager::new());
let task_manager = Arc::new(TaskManager::new_with_session(session_id));
let todo_manager = Arc::new(TodoManager::new());
let hook_manager_for_registry = hook_manager_loaded.clone();
let invoked_skills = new_invoked_skills_map();
let tool_registry = Arc::new(ToolRegistry::new(
vec![],
ask_tx,
Arc::clone(&background_manager),
Arc::clone(&task_manager),
Arc::new(Mutex::new(hook_manager_for_registry)),
invoked_skills.clone(),
crate::command::chat::storage::SessionPaths::new(session_id).todos_file(),
));
spawn_ask_handler(ask_rx);
let user_msg = ChatMessage::text(MessageRole::User, &message);
let prior_len = prior_messages.len();
let mut messages = prior_messages;
messages.push(user_msg);
let loaded_skills = skill::load_all_skills();
let system_prompt_fn = build_system_prompt_fn(
loaded_skills,
agent_config.disabled_skills.clone(),
agent_config.disabled_tools.clone(),
Arc::clone(&tool_registry),
);
let api_messages = select_messages(
&messages,
agent_config.max_history_messages,
agent_config.max_context_tokens,
agent_config.compact.keep_recent,
&agent_config.compact.micro_compact_exempt_tools,
);
let cancel_token = tokio_util::sync::CancellationToken::new();
let streaming_content: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let streaming_reasoning_content: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let pending_user_messages: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(vec![]));
let display_messages: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(vec![]));
let context_messages: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(vec![]));
let estimated_context_tokens: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
let derived_system_prompt: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let agent_config_struct = AgentLoopConfig {
provider: provider.clone(),
max_llm_rounds: agent_config.max_tool_rounds,
compact_config: agent_config.compact.clone(),
hook_manager: hook_manager_loaded,
disabled_hooks: agent_config.disabled_hooks.clone(),
cancel_token: cancel_token.clone(),
};
let agent_shared = AgentLoopSharedState {
streaming_content: Arc::clone(&streaming_content),
streaming_reasoning_content: Arc::clone(&streaming_reasoning_content),
pending_user_messages,
background_manager,
todo_manager,
display_messages: Arc::clone(&display_messages),
context_messages: Arc::clone(&context_messages),
estimated_context_tokens,
invoked_skills,
session_id: session_id.to_string(),
derived_system_prompt,
tool_registry: Arc::clone(&tool_registry),
disabled_tools: agent_config.disabled_tools.clone(),
tools_enabled: agent_config.tools_enabled,
};
let cancel_for_ctrlc = cancel_token.clone();
let _ = ctrlc::set_handler(move || {
cancel_for_ctrlc.cancel();
});
let (handle, tool_result_tx) = MainAgentHandle::spawn(
agent_config_struct,
agent_shared,
api_messages,
system_prompt_fn,
);
let tool_result_tx: std::sync::mpsc::SyncSender<
crate::command::chat::app::types::ToolResultMsg,
> = tool_result_tx;
let anim_stop = start_thinking_animation(thinking_style);
let mut anim_running = true;
let mut last_streaming_len: usize = 0;
let mut raw_lines: usize = 0;
let mut cur_col: usize = 0;
let tw = term_width();
let jcli_config = JcliConfig::load();
let cancelled = Arc::new(AtomicBool::new(false));
let mut round: usize = 0;
let mut first_content = true;
loop {
let msgs = handle.poll();
if msgs.is_empty() {
std::thread::sleep(Duration::from_millis(ONESHOT_POLL_MS));
continue;
}
for msg in msgs {
match msg {
StreamMsg::Chunk => {
let content = streaming_content.lock().unwrap();
if content.len() > last_streaming_len {
if anim_running {
stop_thinking_animation(&anim_stop);
anim_running = false;
}
if first_content {
let theme = Theme::terminal();
eprintln!(
" {}",
crate::util::color_adapt::apply_fg("Sprite", theme.label_ai).bold()
);
first_content = false;
}
let delta = &content[last_streaming_len..];
print!("{}", delta);
let _ = io::stdout().flush();
for ch in delta.chars() {
if ch == '\n' {
raw_lines += 1;
cur_col = 0;
} else {
cur_col += 1;
if cur_col >= tw {
raw_lines += 1;
cur_col = 0;
}
}
}
last_streaming_len = content.len();
}
}
StreamMsg::ToolCallRequest(items) => {
if anim_running {
stop_thinking_animation(&anim_stop);
anim_running = false;
}
if last_streaming_len > 0 {
redraw_streaming_as_markdown(
&streaming_content,
&mut raw_lines,
&mut cur_col,
);
last_streaming_len = streaming_content.lock().unwrap().len();
}
round += 1;
eprintln!();
eprintln!(" {} R{} · {} 工具", "⚙".dimmed(), round, items.len());
for item in items.iter() {
let tool_result = handle_tool_call(
item,
tool_registry.as_ref(),
&jcli_config,
&cancelled,
bypass,
);
let _ = tool_result_tx.send(tool_result);
}
first_content = true;
}
StreamMsg::Done => {
if anim_running {
stop_thinking_animation(&anim_stop);
}
if last_streaming_len > 0 {
redraw_streaming_as_markdown(
&streaming_content,
&mut raw_lines,
&mut cur_col,
);
}
let ctx_msgs = context_messages.lock().unwrap();
let persist_from = if prior_len < ctx_msgs.len() {
prior_len
} else {
0
};
persist_messages(session_id, &ctx_msgs, persist_from);
if round > 0 {
eprintln!();
}
eprintln!("{} {}", "会话 ID:".dimmed(), session_id.dimmed());
fire_session_end(
&hook_manager_for_end,
&disabled_hooks,
&ctx_msgs,
session_id,
&provider.model,
);
return;
}
StreamMsg::Error(e) => {
if anim_running {
stop_thinking_animation(&anim_stop);
}
error!("\n{}", e.display_message());
let ctx_msgs = context_messages.lock().unwrap();
let persist_from = if prior_len < ctx_msgs.len() {
prior_len
} else {
0
};
persist_messages(session_id, &ctx_msgs, persist_from);
fire_session_end(
&hook_manager_for_end,
&disabled_hooks,
&ctx_msgs,
session_id,
&provider.model,
);
return;
}
StreamMsg::Cancelled => {
if anim_running {
stop_thinking_animation(&anim_stop);
}
println!();
let ctx_msgs = context_messages.lock().unwrap();
let persist_from = if prior_len < ctx_msgs.len() {
prior_len
} else {
0
};
persist_messages(session_id, &ctx_msgs, persist_from);
eprintln!("\n {}", "⏹ 已中断".dimmed());
eprintln!(" {} {}", "会话 ID:".dimmed(), session_id.dimmed());
fire_session_end(
&hook_manager_for_end,
&disabled_hooks,
&ctx_msgs,
session_id,
&provider.model,
);
return;
}
StreamMsg::Retrying {
attempt,
max_attempts,
delay_ms,
error,
} => {
if anim_running {
stop_thinking_animation(&anim_stop);
anim_running = false;
}
eprintln!(
" {} 重试中 ({}/{}, {}ms) — {}",
"⟳".yellow(),
attempt,
max_attempts,
delay_ms,
error.dimmed()
);
}
StreamMsg::Compacting => {
eprintln!(" {} 压缩上下文中...", "📦".dimmed());
}
StreamMsg::Compacted { messages_before } => {
eprintln!(" {} 已压缩 {} 条消息", "📦".dimmed(), messages_before);
}
}
}
}
}