worker: reuse session reference for compaction reads

This commit is contained in:
2026-07-18 09:03:06 +09:00
parent 2dd9ef95dd
commit 92a825fd15
6 changed files with 189 additions and 108 deletions
+34 -99
View File
@@ -30,7 +30,10 @@ use tools::ScopedFs;
use crate::compact::usage_tracker::UsageTracker;
use crate::fs_view::{ReadRequirement, slice_lines};
use crate::session_reference::{ReferenceKind, SearchOptions, SessionReferenceView};
use crate::session_reference::{
ReadDetail, ReadOptions, ReadSelector, ReferenceKind, SearchOptions, SessionReferenceView,
ToolPart,
};
/// Aggregated output of a compact worker run.
#[derive(Debug, Default, Clone)]
@@ -241,17 +244,36 @@ impl Tool for ReadSessionItemsTool {
let offset = params.offset.min(self.state.items.len());
let limit = params.limit.clamp(1, SESSION_READ_MAX_ITEMS);
let end = offset.saturating_add(limit).min(self.state.items.len());
let mut blocks = Vec::new();
for idx in offset..end {
blocks.push(format_session_item(
idx,
&self.state.items[idx],
mode,
4_000,
));
}
let mut content = blocks.join("\n\n");
let truncated = truncate_to_token_budget(&mut content, SESSION_TOOL_MAX_OUTPUT_TOKENS);
let detail = match mode {
SessionReadMode::Compact => ReadDetail::Compact,
SessionReadMode::Full => ReadDetail::Full,
};
let read = if offset >= end {
crate::session_reference::ReadResult {
entries: Vec::new(),
truncated: false,
}
} else {
self.state.view.read(
ReadSelector::EntryRange([offset as u64, end.saturating_sub(1) as u64]),
ReadOptions {
include_tools: true,
tool_part: ToolPart::Both,
detail,
max_items: limit,
max_bytes: 48 * 1024,
},
)
};
let mut content = read
.entries
.iter()
.map(|entry| entry.text.clone())
.collect::<Vec<_>>()
.join("\n\n");
let token_truncated =
truncate_to_token_budget(&mut content, SESSION_TOOL_MAX_OUTPUT_TOKENS);
let truncated = read.truncated || token_truncated;
let summary = if truncated {
format!(
"Read session items {offset}..{end} in {mode:?} mode; output truncated. Narrow the range."
@@ -284,93 +306,6 @@ impl SessionReadMode {
}
}
fn session_item_search_text(item: &Item) -> String {
match item {
Item::Message { role, content, .. } => format!(
"{:?} {}",
role,
content
.iter()
.map(|p| p.as_text())
.collect::<Vec<_>>()
.join("")
),
Item::ToolCall {
name, arguments, ..
} => format!("tool_call {name} {arguments}"),
Item::ToolResult {
summary, content, ..
} => format!(
"tool_result {summary} {}",
content.as_deref().unwrap_or_default()
),
Item::Reasoning { text, summary, .. } => format!("reasoning {text} {}", summary.join(" ")),
}
}
fn format_session_item(idx: usize, item: &Item, mode: SessionReadMode, max_chars: usize) -> String {
match item {
Item::Message { role, content, .. } => {
let text = content
.iter()
.map(|p| p.as_text())
.collect::<Vec<_>>()
.join("");
format!(
"[{idx} Message {:?}] {}",
role,
truncate_chars(&text, max_chars)
)
}
Item::ToolCall {
name, arguments, ..
} => match mode {
SessionReadMode::Compact => format!("[{idx} ToolCall] {name} (arguments omitted)"),
SessionReadMode::Full => format!(
"[{idx} ToolCall] {name}\narguments: {}",
truncate_chars(arguments, max_chars)
),
},
Item::ToolResult {
summary,
content,
is_error,
..
} => match mode {
SessionReadMode::Compact => format!(
"[{idx} ToolResult{}] {} (content omitted)",
if *is_error { " error" } else { "" },
truncate_chars(summary, 800)
),
SessionReadMode::Full => format!(
"[{idx} ToolResult{}] {}\ncontent: {}",
if *is_error { " error" } else { "" },
truncate_chars(summary, 800),
truncate_chars(content.as_deref().unwrap_or(""), max_chars)
),
},
Item::Reasoning { summary, .. } => match mode {
SessionReadMode::Compact => format!(
"[{idx} Reasoning] {} (body omitted)",
truncate_chars(&summary.join(" "), 800)
),
SessionReadMode::Full => format!(
"[{idx} Reasoning] {} (body omitted)",
truncate_chars(&summary.join(" "), 800)
),
},
}
}
fn truncate_chars(text: &str, max_chars: usize) -> String {
if text.chars().count() <= max_chars {
return text.to_string();
}
let mut out = text.chars().take(max_chars).collect::<String>();
out.push_str("… [truncated]");
out
}
fn truncate_to_token_budget(text: &mut String, max_tokens: u64) -> bool {
let max_bytes = max_tokens.saturating_mul(4) as usize;
if text.len() <= max_bytes {
+37 -9
View File
@@ -121,10 +121,17 @@ pub(crate) enum ReadSelector<'a> {
EntryRange([u64; 2]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReadDetail {
Compact,
Full,
}
#[derive(Debug, Clone)]
pub(crate) struct ReadOptions {
pub include_tools: bool,
pub tool_part: ToolPart,
pub detail: ReadDetail,
pub max_items: usize,
pub max_bytes: usize,
}
@@ -134,6 +141,7 @@ impl Default for ReadOptions {
Self {
include_tools: true,
tool_part: ToolPart::Both,
detail: ReadDetail::Compact,
max_items: DEFAULT_READ_MAX_ITEMS,
max_bytes: DEFAULT_READ_MAX_BYTES,
}
@@ -344,7 +352,7 @@ impl SessionReferenceView {
let Some(item) = self.items.get(entry.entry_range[0] as usize) else {
continue;
};
let text = render_item(item, entry, max_bytes.saturating_sub(bytes));
let text = render_item(item, entry, options.detail, max_bytes.saturating_sub(bytes));
bytes = bytes.saturating_add(text.len());
entries.push(ReadEntry {
id: entry.id.clone(),
@@ -378,7 +386,12 @@ impl SessionReferenceView {
}
}
fn render_item(item: &Item, entry: &ReferenceEntry, max_bytes: usize) -> String {
fn render_item(
item: &Item,
entry: &ReferenceEntry,
detail: ReadDetail,
max_bytes: usize,
) -> String {
let text = match item {
Item::Message { role, content, .. } => {
let text = content
@@ -390,14 +403,28 @@ fn render_item(item: &Item, entry: &ReferenceEntry, max_bytes: usize) -> String
}
Item::ToolCall {
name, arguments, ..
} => format!("[{} ToolInput {name}]\narguments: {arguments}", entry.id),
} => match detail {
ReadDetail::Compact => format!("[{} ToolInput {name}] (arguments omitted)", entry.id),
ReadDetail::Full => format!("[{} ToolInput {name}]\narguments: {arguments}", entry.id),
},
Item::ToolResult {
summary, content, ..
} => format!(
"[{} ToolOutput]\nsummary: {summary}\ncontent: {}",
entry.id,
content.as_deref().unwrap_or_default()
),
summary,
content,
is_error,
..
} => match detail {
ReadDetail::Compact => format!(
"[{} ToolOutput{}]\nsummary: {summary}\ncontent: (omitted)",
entry.id,
if *is_error { " error" } else { "" }
),
ReadDetail::Full => format!(
"[{} ToolOutput{}]\nsummary: {summary}\ncontent: {}",
entry.id,
if *is_error { " error" } else { "" },
content.as_deref().unwrap_or_default()
),
},
Item::Reasoning { .. } => format!("[{} Reasoning omitted]", entry.id),
};
truncate_chars(&text, max_bytes)
@@ -488,6 +515,7 @@ mod tests {
ReadSelector::EntryRange([0, 3]),
ReadOptions {
include_tools: false,
detail: ReadDetail::Compact,
max_items: 10,
max_bytes: 1_000,
..ReadOptions::default()