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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Auto-wires [`summarize_history`] into the runtime step loop (Item C).
//!
//! [`summarize_history`] existed as a caller-invoked helper that never
//! mutated short-term memory — callers had to remember to fold its
//! result into a future `system_prompt` themselves. [`maybe_compact`]
//! closes that gap: called once per step, before the request is built,
//! it summarizes the older portion of the thread's history (once
//! [`RunOptions::compaction`](super::RunOptions::compaction)'s
//! `trigger_token_budget` is crossed) and rewrites short-term memory to
//! `[summary message, ...last `keep_recent_messages` verbatim]`.
//!
//! # Provenance (CRUCIBLE-CRITICAL)
//!
//! Compaction only ever shortens the *working* short-term context that
//! gets fed to the next LLM call. It never touches `EpisodicMemory`:
//!
//! - Every message that ever reaches short-term memory on the main
//! agent path is first recorded to the audit trail by
//! `record_and_dispatch` (`Episode::LlmCall` /
//! `Episode::ToolCall`), *before* that same call site appends the
//! message to short-term memory. Episodic recording happens upstream
//! of, and independently from, whatever short-term truncation
//! [`maybe_compact`] performs on a later step.
//! - [`crate::memory::EpisodicMemory`] exposes only `record` and
//! `replay` — no delete or mutate method exists on the trait, so
//! nothing downstream of it (including this module) can alter or
//! remove a previously recorded episode. The audit trail is
//! structurally append-only.
//! - The summarizer LLM call itself is not an unaudited side channel:
//! [`summarize_history`] records an `Episode::SummaryCheckpoint` for
//! every summarization it performs, so the compaction step is
//! audited the same as any other LLM call.
//!
//! Net effect: short-term memory is lossy by design (that's the whole
//! point of compaction), but the episodic run history is not — it
//! always holds one entry per real step, regardless of how many times
//! [`maybe_compact`] rewrites the working context.
use crateAgentContext;
use crate;
use crateThreadId;
use crate;
use crate;
/// Prefix marking a short-term message as a compaction summary rather
/// than a verbatim turn, so a human skimming short-term content can
/// tell the two apart.
const SUMMARY_MESSAGE_PREFIX: &str = "[compacted summary of earlier turns]\n";
/// Run one auto-compaction check for `thread`. Called once per step,
/// before the request is built, by both the blocking and streaming
/// drivers.
///
/// - `compaction: None` (the compliance carve-out, set via
/// [`super::RunOptions::without_compaction`]) is a no-op.
/// - `Some(opts)` with `opts.trigger_token_budget == 0` is rejected
/// with `Error::Config` — a zero budget would make
/// [`summarize_history`]'s trigger check vacuous (`tokens < 0` is
/// never true), so every step would re-summarize the same content
/// instead of never triggering. This mirrors klieo's existing
/// reject-zero-duration convention for other budget/interval
/// configs rather than allowing that runaway behaviour.
/// - Otherwise, delegates to [`summarize_history`]; when it returns a
/// summary, rewrites short-term memory to keep the working context
/// bounded.
pub async
/// Replace `thread`'s short-term history with `[summary,
/// ...last `opts.keep_recent_messages` verbatim]`. The verbatim tail is
/// computed from a fresh load using the same split formula
/// [`summarize_history`] used internally — safe because nothing writes
/// to `thread` between that call and this one on the same step.
async