266
/// context window, so the cap can afford to be generous
267
const MAX_TOOL_ROUNDS: usize = 24;
268
269
+/// What the user sees when a turn that ran tools ends with no closing text.
270
+fn silent_stop_message(repeated_tool: Option<&str>, rounds: usize) -> String {
271
+ match repeated_tool {
272
+ Some(tool) => format!(
273
+ "I stopped because I kept making the same `{tool}` call without getting \
274
+ anywhere, and I had nothing to add without it. Reply \"continue\" to pick \
275
+ it back up, or tell me what to try instead."
276
+ ),
277
+ None => format!(
278
+ "I stopped after {rounds} tool round(s) without writing a final answer. \
279
+ Reply \"continue\" to pick it back up, or tell me what to try instead."
280
+ ),
281
+ }
282
+}
283
+
284
/// Separator between two stretches of assistant prose that a tool round split
285
/// apart. ACP clients concatenate consecutive agent-message chunks into a
286
/// single block, so a plain newline (or nothing at all) leaves the rounds
2246
};
2247
2248
let mut round = 0;
2249
+ // Set when the repetition guard blocked a call; names the tool.
2250
+ let mut stopped_repeating: Option<String> = None;
2251
+ // Visible text already sent before the latest inference round, so the
2252
+ // end of the turn can tell whether that round said anything at all.
2253
+ let mut sent_before_last_round = 0;
2254
2255
while !result.tool_calls.is_empty() && round < MAX_TOOL_ROUNDS {
2256
if cancellation.cancelled.load(Ordering::Acquire) {
2359
let repeated = *repeat_count >= 3 && tc.name != "command_output";
2360
if repeated {
2361
force_text = true;
2362
+ stopped_repeating = Some(tc.name.clone());
2363
log::warn!(
2364
"prompt({}) stopping repeated tool call `{}` after {} attempts",
2365
session_id,
2493
// Whatever this round says starts a new paragraph rather than
2494
// continuing the sentence the tool calls interrupted.
2495
reply.interrupt();
2496
+ sent_before_last_round = reply.sent.len();
2497
2498
let cancelled_results = tool_results.clone();
2499
result = match self
2536
session_id,
2537
round
2538
);
2517
- "Something went wrong — the edits didn't go through. Try rephrasing what you need, or point me at the specific lines.".to_string()
2539
+ silent_stop_message(stopped_repeating.as_deref(), round)
2540
} else {
2541
log::warn!(
2542
"prompt({}) — model returned empty reply (no tool rounds)",
2554
self.send_assistant_message(cx, session_id.clone(), final_text)
2555
.ok();
2556
}
2557
+ } else if round > 0 && reply.sent.len() == sent_before_last_round {
2558
+ // Earlier rounds streamed text, but the last one (typically the
2559
+ // forced no-tools round) said nothing. Without a closing message
2560
+ // the turn just stops, and the user can't tell whether it ended,
2561
+ // stalled, or is waiting on them.
2562
+ log::warn!(
2563
+ "prompt({}) — final round after {} tool round(s) produced no text",
2564
+ session_id,
2565
+ round
2566
+ );
2567
+ self.send_assistant_message(
2568
+ cx,
2569
+ session_id.clone(),
2570
+ format!(
2571
+ "{PARAGRAPH_BREAK}{}",
2572
+ silent_stop_message(stopped_repeating.as_deref(), round)
2573
+ ),
2574
+ )
2575
+ .ok();
2576
}
2577
2578
// Persist the completed turn so a restart (or session/load) can pick