use super::phantom::looks_truncated_mid_sentence;
use super::types::{ProgressCallback, ProgressEvent};
use crate::brain::agent::context::AgentContext;
use crate::brain::provider::Message;
use uuid::Uuid;
pub(crate) fn has_usage_gap(text: &str, usage: &crate::brain::provider::TokenUsage) -> bool {
if usage.output_tokens == 0 {
return false;
}
let visible_tokens = usage.output_tokens.saturating_sub(usage.reasoning_tokens) as u64;
if visible_tokens == 0 {
return false;
}
let chars = text.trim_end().chars().count() as u64;
chars < visible_tokens * 2
}
pub(crate) const MAX_CONTINUATION_ATTEMPTS: u32 = 2;
pub(crate) const TAIL_ANCHOR_CHARS: usize = 120;
pub(crate) fn continuation_nudge(partial: &str, degenerate: Option<&str>) -> String {
let tail: String = partial
.trim_end()
.chars()
.rev()
.take(TAIL_ANCHOR_CHARS)
.collect::<String>()
.chars()
.rev()
.collect();
let head = match degenerate {
Some(d) => format!(
"[System: Your previous continuation attempt returned only {d:?} — it added \
no content, so it was discarded. "
),
None => "[System: Your previous reply was cut off mid-sentence. ".to_string(),
};
format!(
"{head}Your reply so far ends with these exact characters:\n---\n{tail}\n---\n\
Continue from that exact point onward: the very next characters must be NEW \
content that carries the answer forward. Do NOT repeat any earlier text, do \
NOT restart the answer, do NOT re-plan. If the tail ends inside unclosed \
markup (a code span, fence, bracket or quote), closing it alone does NOT \
count as continuing — carry the content forward and close the markup only \
when the content is actually done. Just keep writing.]"
)
}
pub(super) fn try_emit_truncation_continue(
iteration_text: &str,
reasoning_text: Option<&String>,
usage: &crate::brain::provider::TokenUsage,
context: &mut AgentContext,
session_id: Uuid,
attempt: u32,
progress_callback: &Option<ProgressCallback>,
) -> bool {
let structural = looks_truncated_mid_sentence(iteration_text.trim_end());
let usage_gap = has_usage_gap(iteration_text, usage);
if !structural {
if usage_gap {
tracing::info!(
"[TRUNCATION] verdict=usage-gap-watch text_chars={}, usage_output={}, usage_reasoning={} — complete-looking text with a token deficit; no retry",
iteration_text.trim_end().chars().count(),
usage.output_tokens,
usage.reasoning_tokens
);
}
return false;
}
let preview: String = iteration_text
.chars()
.rev()
.take(60)
.collect::<String>()
.chars()
.rev()
.collect();
tracing::warn!(
"[TRUNCATION] verdict=retry signals=structural{} tail={:?} text_chars={}, usage_output={}, usage_reasoning={}, usage_gap={}, attempt={}/{} — asking model to continue",
if usage_gap { "+usage_gap" } else { "" },
preview,
iteration_text.trim_end().chars().count(),
usage.output_tokens,
usage.reasoning_tokens,
usage_gap,
attempt,
MAX_CONTINUATION_ATTEMPTS
);
if let Some(cb) = progress_callback {
cb(
session_id,
ProgressEvent::SelfHealingAlert {
message: CONTINUATION_NUDGE_ALERT.into(),
},
);
}
if !iteration_text.is_empty()
&& let Some(cb) = progress_callback
{
cb(
session_id,
ProgressEvent::IntermediateText {
text: iteration_text.to_string(),
reasoning: reasoning_text.cloned(),
},
);
}
context.add_message(Message::assistant(iteration_text.to_string()));
context.add_message(Message::user(continuation_nudge(iteration_text, None)));
true
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Continuation {
Extended(String),
Echoed(String),
}
pub(crate) const CONTINUATION_NUDGE_ALERT: &str =
"Response was cut off mid-sentence: asking model to continue";
pub(crate) const INCOMPLETE_MARKER: &str =
"\n\n_(cut off here: the model did not continue. Ask it to finish this.)_";
pub(crate) fn join_continuation(partial: &str, continuation: &str) -> Continuation {
let p = partial.trim_end();
let c = continuation.trim();
if p.is_empty() {
return Continuation::Extended(c.to_string());
}
if c.is_empty() {
return Continuation::Echoed(p.to_string());
}
if p.contains(c) {
return Continuation::Echoed(p.to_string());
}
if c.contains(p) {
return Continuation::Extended(c.to_string());
}
let needs_space = !p.ends_with(char::is_whitespace)
&& !c.starts_with(char::is_whitespace)
&& !p.ends_with(char::is_alphanumeric);
Continuation::Extended(if needs_space {
format!("{p} {c}")
} else {
format!("{p}{c}")
})
}