diff --git a/.yoi/tickets/00001KXS81AMT/artifacts/.gitkeep b/.yoi/tickets/00001KXS81AMT/artifacts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.yoi/tickets/00001KXS81AMT/item.md b/.yoi/tickets/00001KXS81AMT/item.md new file mode 100644 index 00000000..b3eca28c --- /dev/null +++ b/.yoi/tickets/00001KXS81AMT/item.md @@ -0,0 +1,39 @@ +--- +title: 'Compactionのsession探索toolsをSessionReferenceViewへ移行する' +state: 'closed' +created_at: '2026-07-17T23:54:22Z' +updated_at: '2026-07-18T00:02:34Z' +assignee: null +--- + +## 背景 + +`SessionReferenceView` は extract / compact の共通基盤として導入されたが、直後の実装では compaction の `search_session_log` のみが view を使い、`read_session_items` は旧 compact 専用 formatter / direct item traversal のままだった。 + +既存 compact も explore tool 群を利用するものに寄せるため、この Ticket では compaction の session read/search tools を `SessionReferenceView` へ統合する。 + +## 実装要件 + +- compaction の `search_session_log` は `SessionReferenceView::search` を使う。 +- compaction の `read_session_items` は `SessionReferenceView::read` を使う。 +- `read_session_items` の existing `mode = compact | full` semantics は維持する。 + - compact: tool arguments / tool result content を omitted にする。 + - full: bounded に tool arguments / tool result content を読める。 +- 旧 compact 専用の session search / format helper を削除する。 +- `SessionReferenceView` 側に read detail mode を追加し、compact / full を表現できるようにする。 +- 既存 compact tests を維持または更新する。 + +## 非目標 + +- compaction prompt を大きく変えない。 +- compact worker の tool 名を変更しない。 +- extract の `session-explore` feature 本体はこの Ticket では実装しない。 + +## 受け入れ条件 + +- `search_session_log` と `read_session_items` がどちらも `SessionReferenceView` を経由する。 +- compact mode で raw tool content が出ない。 +- full mode で bounded tool content が読める。 +- `cargo test -p worker compact::worker` が通る。 +- `cargo test -p worker` が通る。 +- code 変更として `nix build .#yoi` が通る。 diff --git a/.yoi/tickets/00001KXS81AMT/resolution.md b/.yoi/tickets/00001KXS81AMT/resolution.md new file mode 100644 index 00000000..1fb9171e --- /dev/null +++ b/.yoi/tickets/00001KXS81AMT/resolution.md @@ -0,0 +1,11 @@ +Compaction session exploration now uses `SessionReferenceView` for both search and read paths. + +`read_session_items` delegates to `SessionReferenceView::read`, preserving compact/full behavior through a new read detail mode. Old compact-specific search/format helpers were removed. + +Validation passed: +- `cargo fmt --check` +- `cargo test -p worker compact::worker` +- `cargo test -p worker session_reference` +- `cargo test -p worker` +- `cargo test -p memory` +- `nix build .#yoi` diff --git a/.yoi/tickets/00001KXS81AMT/thread.md b/.yoi/tickets/00001KXS81AMT/thread.md new file mode 100644 index 00000000..993597bd --- /dev/null +++ b/.yoi/tickets/00001KXS81AMT/thread.md @@ -0,0 +1,68 @@ + + +## 作成 + +LocalTicketBackend によって作成されました。 + +--- + + + +## Implementation report + +Migrated compaction session exploration to `SessionReferenceView`. + +Changes: +- Added `ReadDetail` to `SessionReferenceView` so reads can preserve compact/full semantics. +- Updated `read_session_items` to use `SessionReferenceView::read` with `ReadSelector::EntryRange`. +- Kept compact mode omitting tool arguments / tool result content and full mode bounded. +- Removed old compact-specific session search/format helpers. +- `search_session_log` already used `SessionReferenceView::search` and remains on that path. + +Validation: +- `cargo fmt --check` +- `cargo test -p worker compact::worker` +- `cargo test -p worker session_reference` +- `cargo test -p worker` +- `cargo test -p memory` +- `nix build .#yoi` + + +--- + + + +## State changed + +State changed to `done`. + + +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +Compaction session exploration now uses `SessionReferenceView` for both search and read paths. + +`read_session_items` delegates to `SessionReferenceView::read`, preserving compact/full behavior through a new read detail mode. Old compact-specific search/format helpers were removed. + +Validation passed: +- `cargo fmt --check` +- `cargo test -p worker compact::worker` +- `cargo test -p worker session_reference` +- `cargo test -p worker` +- `cargo test -p memory` +- `nix build .#yoi` + + +--- diff --git a/crates/worker/src/compact/worker.rs b/crates/worker/src/compact/worker.rs index 3001eb06..35245c29 100644 --- a/crates/worker/src/compact/worker.rs +++ b/crates/worker/src/compact/worker.rs @@ -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::>() + .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::>() - .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::>() - .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::(); - 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 { diff --git a/crates/worker/src/session_reference.rs b/crates/worker/src/session_reference.rs index 49784617..845f2312 100644 --- a/crates/worker/src/session_reference.rs +++ b/crates/worker/src/session_reference.rs @@ -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()