#[allow(unused_imports)]
use crate::sync_util::LockExt;
use compact_str::CompactString;
use crossterm::style::Color;
use tokio::sync::mpsc;
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;
use crate::ui::tool_display::{chamber_bottom, chamber_widths};
#[cfg(feature = "loop")]
pub(crate) struct LoopBits<'a> {
pub state: &'a mut Option<crate::extras::r#loop::LoopState>,
pub label: &'a mut Option<String>,
}
#[allow(clippy::too_many_arguments, clippy::await_holding_lock, unused_mut)]
pub(crate) async fn handle_done(
ctx: &mut RunCtx<'_>,
mut response: CompactString,
tokens: u64,
cost: f64,
was_reasoning: &mut bool,
is_running: &mut bool,
agent: &mut AnyAgent,
#[cfg_attr(not(feature = "plugin"), allow(unused_variables))] context: &mut ContextFiles,
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 = "loop")] loop_bits: LoopBits<'_>,
) -> anyhow::Result<()> {
#[cfg(feature = "plugin")]
let client = deps.client;
#[cfg(feature = "plugin")]
let permission = deps.permission;
#[cfg(feature = "plugin")]
let ask_tx = deps.ask_tx;
#[cfg(feature = "plugin")]
let question_tx = deps.question_tx;
#[cfg(feature = "plugin")]
let plan_tx = deps.plan_tx;
let bg_store = deps.bg_store;
#[cfg(feature = "plugin")]
let sandbox = deps.sandbox;
#[cfg(all(feature = "mcp", feature = "plugin"))]
let mcp_manager = deps.mcp_manager;
#[cfg(all(feature = "semantic", feature = "plugin"))]
let semantic_manager = deps.semantic_manager;
#[cfg(all(feature = "lsp", feature = "plugin"))]
let lsp_manager = deps.lsp_manager;
*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 {
let (frame_w, _) = chamber_widths(ctx.renderer);
ctx.renderer
.write_line_raw(&chamber_bottom(frame_w), 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("");
#[allow(unused_mut, unused_variables)]
let mut plugin_followup: Option<String> = None;
#[cfg(feature = "plugin")]
if let Some(pm) = plugin_manager {
let mut mgr = pm.lock_ignore_poison();
match mgr.dispatch(
"on-response",
&format!(
"@{{:response \"{}\"}}",
crate::plugin::escape_janet_string(&response)
),
) {
Ok(results) if !results.is_empty() => {
for line in &results {
let safe = crate::ui::events::sanitize_output(line);
ctx.renderer
.write_line(&format!("[plugin] {}", safe), theme::dim())?;
}
plugin_followup = Some(results.join("\n"));
}
Ok(_) => {}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] on-response error: {e}"), c_error())?;
}
}
if let Some(pending) = mgr.take_pending_prompt() {
plugin_followup = Some(pending);
}
match mgr.dispatch(
"message-end",
&format!(
"@{{:message \"{}\"}}",
crate::plugin::escape_janet_string(&response)
),
) {
Ok(_) => {
if let Some(rewritten) = mgr.take_message_rewrite() {
response = compact_str::CompactString::new(&rewritten);
}
}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] message-end error: {e}"), c_error())?;
}
}
mgr.store_response(&response);
match mgr.dispatch("on-complete", "@{}") {
Ok(_) => {}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] on-complete error: {e}"), c_error())?;
}
}
match mgr.dispatch("prepare-next-run", "@{}") {
Ok(_) => {}
Err(e) => {
ctx.renderer
.write_line(&format!("[plugin] prepare-next-run error: {e}"), c_error())?;
}
}
let pending_next_model = mgr.take_pending_next_model();
drop(mgr);
if let Some(next_model) = pending_next_model {
let trimmed = next_model.trim();
if !trimmed.is_empty() && trimmed != ctx.session.model.as_str() {
let new_model_compact = CompactString::new(trimmed);
let model_obj = client.completion_model(new_model_compact.to_string());
*agent = crate::provider::build_agent(
model_obj,
ctx.cli,
ctx.cfg,
context,
permission.clone(),
ask_tx.clone(),
question_tx.clone(),
plan_tx.clone(),
bg_store.clone(),
#[cfg(feature = "lsp")]
lsp_manager.cloned(),
sandbox.clone(),
#[cfg(feature = "mcp")]
mcp_manager,
#[cfg(feature = "semantic")]
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(),
)?;
}
}
{
let mut mgr = pm.lock_ignore_poison();
let _ = mgr.eval("(set harness-response nil)");
}
}
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 =
format!("dirge-{}", crate::text::short_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 = format!("dirge-{}", crate::text::short_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(())
}