#[allow(unused_imports)]
use crate::sync_util::LockExt;
use compact_str::CompactString;
use crossterm::style::Color;
use tokio::sync::mpsc;
#[cfg(feature = "plugin")]
use crate::context::ContextFiles;
use crate::event::AgentEvent;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
use crate::provider::AnyAgent;
use crate::session::MessageRole;
use crate::ui::agent_io::persist_turn_to_db;
use crate::ui::avatar;
use crate::ui::colors::{c_agent, c_error};
use crate::ui::run_handlers::{AgentBuildDeps, RunCtx};
use crate::ui::theme;
#[cfg(feature = "loop")]
pub(crate) struct LoopBits<'a> {
pub state: &'a mut Option<crate::extras::r#loop::LoopState>,
pub label: &'a mut Option<String>,
}
#[cfg(feature = "plugin")]
pub(crate) async fn apply_next_model(
ctx: &mut RunCtx<'_>,
agent: &mut AnyAgent,
context: &mut ContextFiles,
deps: &AgentBuildDeps<'_>,
next_model: &str,
) -> anyhow::Result<()> {
let trimmed = next_model.trim();
if trimmed.is_empty() || trimmed == ctx.session.model.as_str() {
return Ok(());
}
let new_model_compact = CompactString::new(trimmed);
let model_obj = deps.client.completion_model(new_model_compact.to_string());
*agent = crate::provider::build_agent(
model_obj,
ctx.cli,
ctx.cfg,
context,
deps.permission.clone(),
deps.ask_tx.clone(),
deps.question_tx.clone(),
deps.plan_tx.clone(),
deps.bg_store.clone(),
#[cfg(feature = "lsp")]
deps.lsp_manager.cloned(),
deps.sandbox.clone(),
#[cfg(feature = "mcp")]
deps.mcp_manager,
#[cfg(feature = "semantic")]
deps.semantic_manager,
Some(ctx.session.id.to_string()),
)
.await;
let old_model = ctx.session.model.clone();
ctx.session.model = new_model_compact.clone();
ctx.session.provider = ctx.cli.resolve_provider(ctx.cfg);
let new_ctx = ctx.cfg.resolve_context_window(new_model_compact.as_str());
if new_ctx != ctx.session.context_window {
ctx.session.context_window = new_ctx;
}
ctx.renderer.write_line(
&format!("[plugin] swapped model: {old_model} → {new_model_compact}"),
c_agent(),
)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_done(
ctx: &mut RunCtx<'_>,
response: CompactString,
tokens: u64,
cost: f64,
was_reasoning: &mut bool,
is_running: &mut bool,
agent: &mut AnyAgent,
deps: &AgentBuildDeps<'_>,
agent_rx: &mut Option<mpsc::Receiver<AgentEvent>>,
agent_abort: &mut Option<tokio::task::JoinHandle<()>>,
agent_interject: &mut Option<mpsc::Sender<()>>,
agent_cancel: &mut Option<mpsc::Sender<()>>,
interjection_queue: &std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
review_phase: &mut Option<crate::agent::plan::runtime::ReviewPhaseHandle>,
#[cfg(feature = "plugin")] plugin_manager: Option<
&std::sync::Arc<std::sync::Mutex<PluginManager>>,
>,
#[cfg(feature = "plugin")] done_phase: &mut Option<crate::ui::done_phase::DonePhaseHandle>,
#[cfg(feature = "loop")] loop_bits: LoopBits<'_>,
) -> anyhow::Result<()> {
*was_reasoning = false;
if *ctx.tool_chamber_open {
let drop_chamber = match (*ctx.chamber_top_start, *ctx.chamber_top_end) {
(Some(_), Some(end)) => ctx.renderer.buffer_len() == end,
_ => false,
};
if drop_chamber {
if let Some(start) = *ctx.chamber_top_start {
ctx.renderer.replace_from(start, Vec::new());
}
} else {
ctx.renderer.write_chamber_bottom(theme::dim())?;
}
*ctx.tool_chamber_open = false;
*ctx.chamber_top_start = None;
*ctx.chamber_top_end = None;
}
*ctx.last_tool_name = None;
ctx.renderer.set_avatar_state(avatar::AvatarState::Done);
#[cfg(feature = "experimental-ui-terminal-tab")]
ctx.renderer.set_last_tool_name("");
#[cfg(feature = "plugin")]
if let Some(pm) = plugin_manager {
if let Some(h) = agent_abort.take() {
h.abort();
}
*agent_rx = None;
*agent_interject = None;
*agent_cancel = None;
*done_phase = Some(crate::ui::done_phase::spawn(
pm.clone(),
response,
tokens,
cost,
));
return Ok(());
}
finish_done(
ctx,
response,
tokens,
cost,
agent,
is_running,
deps,
None,
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
interjection_queue,
review_phase,
#[cfg(feature = "plugin")]
plugin_manager,
#[cfg(feature = "loop")]
loop_bits,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn finish_done(
ctx: &mut RunCtx<'_>,
response: CompactString,
tokens: u64,
cost: f64,
agent: &mut AnyAgent,
is_running: &mut bool,
deps: &AgentBuildDeps<'_>,
#[cfg_attr(not(feature = "plugin"), allow(unused_variables))] plugin_followup: Option<String>,
agent_rx: &mut Option<mpsc::Receiver<AgentEvent>>,
agent_abort: &mut Option<tokio::task::JoinHandle<()>>,
agent_interject: &mut Option<mpsc::Sender<()>>,
agent_cancel: &mut Option<mpsc::Sender<()>>,
interjection_queue: &std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
review_phase: &mut Option<crate::agent::plan::runtime::ReviewPhaseHandle>,
#[cfg(feature = "plugin")]
#[cfg_attr(not(feature = "experimental-graph-search"), allow(unused_variables))]
plugin_manager: Option<&std::sync::Arc<std::sync::Mutex<PluginManager>>>,
#[cfg(feature = "loop")] loop_bits: LoopBits<'_>,
) -> anyhow::Result<()> {
let bg_store = deps.bg_store;
if !ctx.response_buf.is_empty() {
ctx.renderer.stream(ctx.response_buf, c_agent(), true);
ctx.renderer.render_viewport()?;
} else if !*ctx.agent_line_started {
ctx.renderer.write("<dirge> ", c_agent())?;
}
ctx.renderer.commit_stream();
ctx.renderer.write_line("", Color::White)?;
ctx.renderer.write_line("", Color::White)?;
ctx.session.add_message_with_tool_calls(
MessageRole::Assistant,
&response,
std::mem::take(ctx.tool_calls_buf),
);
ctx.session.total_tokens = ctx.session.total_tokens.saturating_add(tokens);
ctx.session.total_cost += cost;
*ctx.tool_calls_this_run = 0;
*ctx.agent_line_started = false;
ctx.response_buf.clear();
*ctx.response_start_line = None;
ctx.end_reasoning();
*ctx.reasoning_start_line = None;
if !ctx.cli.no_session
&& let Err(e) = crate::session::storage::save_session(ctx.session)
{
ctx.renderer.write_line(
&format!("warning: failed to save session: {}", e),
c_error(),
)?;
}
*is_running = false;
if let Some(h) = agent_abort.take() {
h.abort();
}
*agent_rx = None;
*agent_interject = None;
*agent_cancel = None;
#[cfg(feature = "plugin")]
let followup_for_decision = plugin_followup.clone();
#[cfg(not(feature = "plugin"))]
let followup_for_decision: Option<String> = None;
#[cfg(feature = "loop")]
let (loop_active, loop_should_stop) = loop_bits
.state
.as_ref()
.map(|ls| (ls.active, ls.active && ls.should_stop()))
.unwrap_or((false, false));
#[cfg(not(feature = "loop"))]
let (loop_active, loop_should_stop) = (false, false);
let action = crate::plugin::decide_post_done_action(
followup_for_decision,
loop_active,
loop_should_stop,
);
match action {
crate::plugin::PostDoneAction::Followup(text) => {
let followup_prompt = text + "\n\nContinue.";
ctx.last_user_prompt.clone_from(&followup_prompt);
let runner = agent.clone().spawn_runner(
crate::agent::tools::background::prepend_pending_notifications(
&followup_prompt,
bg_store.as_ref(),
),
crate::agent::runner::convert_history(ctx.session),
Some(interjection_queue.clone()),
);
runner.install_into(
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
);
}
crate::plugin::PostDoneAction::LoopStop =>
{
#[cfg(feature = "loop")]
if let Some(ls) = loop_bits.state.as_mut() {
ctx.renderer.write_line(
&format!("[loop] max iterations ({}) reached, stopping", ls.iteration),
c_agent(),
)?;
ls.active = false;
*loop_bits.label = None;
}
}
crate::plugin::PostDoneAction::LoopIter =>
{
#[cfg(feature = "loop")]
if let Some(ls) = loop_bits.state.as_mut() {
let summary: String = response.chars().take(200).collect();
ls.last_summary = Some(summary);
ls.iteration += 1;
let prompt = ls.build_prompt();
ctx.last_user_prompt.clone_from(&prompt);
let runner = agent.clone().spawn_runner(
crate::agent::tools::background::prepend_pending_notifications(
&prompt,
bg_store.as_ref(),
),
Vec::new(),
Some(interjection_queue.clone()),
);
runner.install_into(
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
);
*loop_bits.label = Some(ls.iteration_label());
ctx.renderer.write_line(
&format!("[loop] launching {}", ls.iteration_label()),
c_agent(),
)?;
}
}
crate::plugin::PostDoneAction::Idle => {}
}
let plan_review_tool_calls = ctx.tool_calls_buf.clone();
super::plan_review::drive_plan_review(
ctx,
agent,
&response,
&plan_review_tool_calls,
review_phase,
is_running,
)?;
if !*is_running {
#[cfg(all(feature = "experimental-graph-search", feature = "plugin"))]
{
let paths = crate::extras::dirge_paths::ProjectPaths::new(
&std::env::current_dir().unwrap_or_else(|_| ".".into()),
);
if let Some(pm) = plugin_manager {
let mut mgr = pm.lock_ignore_poison();
let entities = mgr.drain_entity_records();
let relations = mgr.drain_relation_records();
if !entities.is_empty() || !relations.is_empty() {
if let Ok(db) =
crate::extras::session_db::SessionDb::open(&paths.session_db_path())
{
use crate::extras::entity_db;
let sid = crate::text::db_session_id(ctx.session.id.as_str());
for ent in &entities {
let _ = entity_db::upsert_entity(
&db.conn,
&sid,
None,
&ent.kind,
&ent.name,
ent.extra.as_deref(),
);
}
for rel in &relations {
let source_id = entity_db::resolve_entity(
&db.conn,
&rel.source_kind,
&rel.source_name,
);
let target_id = entity_db::resolve_entity(
&db.conn,
&rel.target_kind,
&rel.target_name,
);
if let (Ok(Some(src_eid)), Ok(Some(tgt_eid))) = (source_id, target_id) {
let _ = entity_db::insert_relation(
&db.conn,
src_eid,
tgt_eid,
&rel.rel_type,
&sid,
);
}
}
}
}
}
if let Some(pm) = plugin_manager {
if let Ok(db) = crate::extras::session_db::SessionDb::open(&paths.session_db_path())
{
let sid = crate::text::db_session_id(ctx.session.id.as_str());
if let Ok(context) =
crate::extras::entity_compress::build_graph_context(&db.conn, &sid)
{
if !context.is_empty() {
let mut mgr = pm.lock_ignore_poison();
let escaped = crate::plugin::escape_janet_string(&context);
let _ =
mgr.eval(&format!("(harness/append-system-prompt {})", escaped));
}
}
}
}
}
finalize_idle_turn(
ctx.session,
ctx.last_user_prompt,
&response,
ctx.tool_calls_buf,
agent,
bg_store,
interjection_queue,
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn finalize_idle_turn(
session: &mut crate::session::Session,
last_user_prompt: &mut String,
response: &str,
tool_calls: &[crate::session::ToolCallEntry],
agent: &AnyAgent,
bg_store: &Option<crate::agent::tools::background::BackgroundStore>,
interjection_queue: &std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
agent_rx: &mut Option<mpsc::Receiver<AgentEvent>>,
agent_abort: &mut Option<tokio::task::JoinHandle<()>>,
agent_interject: &mut Option<mpsc::Sender<()>>,
agent_cancel: &mut Option<mpsc::Sender<()>>,
is_running: &mut bool,
) -> anyhow::Result<()> {
let cwd = std::env::current_dir().unwrap_or_else(|_| ".".into());
let paths = crate::extras::dirge_paths::ProjectPaths::new(&cwd);
persist_turn_to_db(session, last_user_prompt, response, tool_calls);
let base = crate::agent::review::build_transcript(session);
let digest = crate::agent::session_digest::SessionDigest::from_session(session);
crate::agent::post_session::spawn_post_session(agent.clone(), paths, digest, base);
if !interjection_queue.lock().unwrap().is_empty() {
let queued: Vec<String> = interjection_queue.lock().unwrap().drain(..).collect();
let combined = queued.join("\n\n");
last_user_prompt.clone_from(&combined);
let history = crate::agent::runner::convert_history(session);
session.add_message(MessageRole::User, &combined);
let runner = agent.clone().spawn_runner(
crate::agent::tools::background::prepend_pending_notifications(
&combined,
bg_store.as_ref(),
),
history,
Some(interjection_queue.clone()),
);
runner.install_into(
agent_rx,
agent_abort,
agent_interject,
agent_cancel,
is_running,
);
}
Ok(())
}