1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//! Heuristic check for whether an LLM response text "looks complete".
//!
//! Used to distinguish two situations that look identical on the
//! wire when a stream ends without `[DONE]` / `MessageStop`:
//!
//! 1. **Connection truly dropped mid-response** — the text ends in
//! the middle of a sentence, mid-word, with an unclosed code
//! fence, etc. The right reaction is to retry the request.
//!
//! 2. **Provider doesn't honour `[DONE]`** — the stream delivered a
//! full coherent response, then the TCP connection closed
//! without an explicit termination marker. Observed on
//! `dialagram` + `qwen-3.7-max-thinking` (2026-05-30). Retrying
//! here regenerates the same content and pegs the
//! "is responding..." indicator for minutes.
//!
//! Returning `true` from `text_looks_complete` lets the caller
//! synthesise a `StopReason::EndTurn` and proceed; `false` keeps
//! the existing retry path for genuinely truncated streams.
//!
//! Conservative by design: we'd rather over-retry a slightly
//! awkward complete response than under-retry a real truncation
//! that needs another shot.
/// Minimum char count below which we won't claim "complete" no
/// matter how the text ends. Short responses are usually preambles
/// like "Let me check the README:" that genuinely ARE truncated.
const MIN_COMPLETE_CHARS: usize = 200;
/// True when the response text looks structurally complete and
/// safe to accept without a `[DONE]` marker.
///
/// Heuristics applied in order:
/// - Strip trailing whitespace and walk back.
/// - If shorter than `MIN_COMPLETE_CHARS`, return false (too short
/// to confidently claim complete).
/// - If there's an unmatched ``` code fence, return false (model
/// started a code block and the stream cut before the closer).
/// - If the last non-whitespace character is a sentence terminator
/// (`.`, `!`, `?`), a closing bracket / quote, a list-item
/// period, an emoji, or a closing fence, return true.
/// - If the last non-whitespace character looks like a clear
/// mid-sentence marker (`:`, `,`, `;`, `-`, opening bracket /
/// quote), return false.
/// - Default: true. We've passed the size + fence checks; the
/// remaining cases (ends in a word, a number, etc.) are rare on
/// real completions but ALSO rare on truncations, and the cost
/// of a spurious retry is much higher than the cost of accepting
/// a slightly informal ending.
/// Count of ` ``` ` (triple-backtick) fences and report whether
/// it's odd (= one unclosed). A naive substring count is fine
/// because fences are line-anchored in practice and the model
/// rarely emits triple backticks inside inline code.
// std::ops::Not isn't in scope by default for bool; bring it in
// without polluting the module top.
use Not;