Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/rig-memory/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- *(memory)* `TemplateCompactor::without_max_bytes` clears a configured size
cap, restoring the default unbounded rollup. It is the inverse of
`with_max_bytes` and the migration target for callers that passed `0`.

### Changed

- *(memory)* [**breaking**] `TemplateCompactor::with_max_bytes(0)` no longer
means "unbounded" — the cap is now taken literally. Use `without_max_bytes`
for unbounded behaviour. The signature is unchanged, so this does not break
compilation: callers that passed `0` as an off switch keep building and
silently lose the rolled-up carry-over instead of seeing an error.

The sentinel made the cap non-monotonic in its argument, so a bound computed
rather than typed (`budget.saturating_sub(overhead)`, a config field left at
its zero default) could land on the one input meaning the opposite of what
was asked for. Since the summary spliced by `CompactingMemory` sits
**outside** the wrapped `MemoryPolicy`'s budget, an accidentally-unbounded
compactor is precisely the failure the cap exists to prevent; an accidental
`0` now fails safe instead.

## [0.42.0](https://github.com/0xPlaygrounds/rig/compare/rig-memory-v0.41.0...rig-memory-v0.42.0) - 2026-08-16

### Other
Expand Down
51 changes: 40 additions & 11 deletions crates/rig-memory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,11 @@ where
/// // Custom header plus a 4 KiB cap for use with token-budgeted policies.
/// let _bounded = TemplateCompactor::with_header("Earlier context")
/// .with_max_bytes(4 * 1024);
///
/// // Opt back out of a cap inherited from a shared default.
/// let _unbounded = TemplateCompactor::with_header("Earlier context")
/// .with_max_bytes(4 * 1024)
/// .without_max_bytes();
/// ```
#[derive(Debug, Clone)]
pub struct TemplateCompactor {
Expand All @@ -1243,17 +1248,20 @@ impl TemplateCompactor {
/// Cap the rolled-up summary at `max_bytes` bytes (UTF-8). When the
/// assembled body exceeds the cap, the oldest portion after the
/// header is dropped at a char boundary and replaced with a
/// `"[…truncated…]"` marker.
///
/// `max_bytes` of `0` disables truncation (equivalent to the default
/// unbounded behaviour). The header line plus the marker are always
/// preserved even if they exceed the cap.
/// `"[…truncated…]"` marker. The header line plus the marker are
/// always preserved even if they exceed the cap, so a `max_bytes` of
/// `0` collapses the summary to just the header and marker rather
/// than disabling truncation — use [`Self::without_max_bytes`] for
/// unbounded behaviour.
pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
self.max_bytes = if max_bytes == 0 {
None
} else {
Some(max_bytes)
};
self.max_bytes = Some(max_bytes);
self
}

/// Remove any configured size cap, restoring the default unbounded
/// behaviour.
pub fn without_max_bytes(mut self) -> Self {
self.max_bytes = None;
self
}
}
Expand Down Expand Up @@ -2855,14 +2863,35 @@ mod tests {
}

#[tokio::test]
async fn template_compactor_with_max_bytes_zero_is_unbounded() {
async fn template_compactor_with_max_bytes_zero_collapses_to_header_and_marker() {
let compactor = TemplateCompactor::new().with_max_bytes(0);
let mut evicted = Vec::new();
for i in 0..200 {
evicted.push(user(&format!("msg {i}")));
}
let summary = compactor.compact("c", &evicted, None).await.unwrap();
assert!(summary.as_str().contains("[\u{2026}truncated\u{2026}]"));
assert!(!summary.as_str().contains("msg 199"));
assert!(
summary
.as_str()
.starts_with("[Conversation summary so far]\n")
);
}

#[tokio::test]
async fn template_compactor_without_max_bytes_restores_unbounded() {
let compactor = TemplateCompactor::new()
.with_max_bytes(32)
.without_max_bytes();
let mut evicted = Vec::new();
for i in 0..200 {
evicted.push(user(&format!("msg {i}")));
}
let summary = compactor.compact("c", &evicted, None).await.unwrap();
assert!(!summary.as_str().contains("[\u{2026}truncated\u{2026}]"));
assert!(summary.as_str().contains("msg 0"));
assert!(summary.as_str().contains("msg 199"));
}

#[tokio::test]
Expand Down