worker: reuse session reference for compaction reads
This commit is contained in:
parent
2dd9ef95dd
commit
92a825fd15
0
.yoi/tickets/00001KXS81AMT/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KXS81AMT/artifacts/.gitkeep
Normal file
39
.yoi/tickets/00001KXS81AMT/item.md
Normal file
39
.yoi/tickets/00001KXS81AMT/item.md
Normal file
|
|
@ -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` が通る。
|
||||||
11
.yoi/tickets/00001KXS81AMT/resolution.md
Normal file
11
.yoi/tickets/00001KXS81AMT/resolution.md
Normal file
|
|
@ -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`
|
||||||
68
.yoi/tickets/00001KXS81AMT/thread.md
Normal file
68
.yoi/tickets/00001KXS81AMT/thread.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!-- event: create author: "yoi ticket" at: 2026-07-17T23:54:22Z -->
|
||||||
|
|
||||||
|
## 作成
|
||||||
|
|
||||||
|
LocalTicketBackend によって作成されました。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-07-18T00:02:34Z -->
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T00:02:34Z from: inprogress to: done reason: cli_state field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
State changed to `done`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-07-18T00:02:34Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-07-18T00:02:34Z status: 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`
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -30,7 +30,10 @@ use tools::ScopedFs;
|
||||||
|
|
||||||
use crate::compact::usage_tracker::UsageTracker;
|
use crate::compact::usage_tracker::UsageTracker;
|
||||||
use crate::fs_view::{ReadRequirement, slice_lines};
|
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.
|
/// Aggregated output of a compact worker run.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
@ -241,17 +244,36 @@ impl Tool for ReadSessionItemsTool {
|
||||||
let offset = params.offset.min(self.state.items.len());
|
let offset = params.offset.min(self.state.items.len());
|
||||||
let limit = params.limit.clamp(1, SESSION_READ_MAX_ITEMS);
|
let limit = params.limit.clamp(1, SESSION_READ_MAX_ITEMS);
|
||||||
let end = offset.saturating_add(limit).min(self.state.items.len());
|
let end = offset.saturating_add(limit).min(self.state.items.len());
|
||||||
let mut blocks = Vec::new();
|
let detail = match mode {
|
||||||
for idx in offset..end {
|
SessionReadMode::Compact => ReadDetail::Compact,
|
||||||
blocks.push(format_session_item(
|
SessionReadMode::Full => ReadDetail::Full,
|
||||||
idx,
|
};
|
||||||
&self.state.items[idx],
|
let read = if offset >= end {
|
||||||
mode,
|
crate::session_reference::ReadResult {
|
||||||
4_000,
|
entries: Vec::new(),
|
||||||
));
|
truncated: false,
|
||||||
}
|
}
|
||||||
let mut content = blocks.join("\n\n");
|
} else {
|
||||||
let truncated = truncate_to_token_budget(&mut content, SESSION_TOOL_MAX_OUTPUT_TOKENS);
|
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 {
|
let summary = if truncated {
|
||||||
format!(
|
format!(
|
||||||
"Read session items {offset}..{end} in {mode:?} mode; output truncated. Narrow the range."
|
"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 {
|
fn truncate_to_token_budget(text: &mut String, max_tokens: u64) -> bool {
|
||||||
let max_bytes = max_tokens.saturating_mul(4) as usize;
|
let max_bytes = max_tokens.saturating_mul(4) as usize;
|
||||||
if text.len() <= max_bytes {
|
if text.len() <= max_bytes {
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,17 @@ pub(crate) enum ReadSelector<'a> {
|
||||||
EntryRange([u64; 2]),
|
EntryRange([u64; 2]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ReadDetail {
|
||||||
|
Compact,
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ReadOptions {
|
pub(crate) struct ReadOptions {
|
||||||
pub include_tools: bool,
|
pub include_tools: bool,
|
||||||
pub tool_part: ToolPart,
|
pub tool_part: ToolPart,
|
||||||
|
pub detail: ReadDetail,
|
||||||
pub max_items: usize,
|
pub max_items: usize,
|
||||||
pub max_bytes: usize,
|
pub max_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +141,7 @@ impl Default for ReadOptions {
|
||||||
Self {
|
Self {
|
||||||
include_tools: true,
|
include_tools: true,
|
||||||
tool_part: ToolPart::Both,
|
tool_part: ToolPart::Both,
|
||||||
|
detail: ReadDetail::Compact,
|
||||||
max_items: DEFAULT_READ_MAX_ITEMS,
|
max_items: DEFAULT_READ_MAX_ITEMS,
|
||||||
max_bytes: DEFAULT_READ_MAX_BYTES,
|
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 {
|
let Some(item) = self.items.get(entry.entry_range[0] as usize) else {
|
||||||
continue;
|
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());
|
bytes = bytes.saturating_add(text.len());
|
||||||
entries.push(ReadEntry {
|
entries.push(ReadEntry {
|
||||||
id: entry.id.clone(),
|
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 {
|
let text = match item {
|
||||||
Item::Message { role, content, .. } => {
|
Item::Message { role, content, .. } => {
|
||||||
let text = content
|
let text = content
|
||||||
|
|
@ -390,14 +403,28 @@ fn render_item(item: &Item, entry: &ReferenceEntry, max_bytes: usize) -> String
|
||||||
}
|
}
|
||||||
Item::ToolCall {
|
Item::ToolCall {
|
||||||
name, arguments, ..
|
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 {
|
Item::ToolResult {
|
||||||
summary, content, ..
|
summary,
|
||||||
} => format!(
|
content,
|
||||||
"[{} ToolOutput]\nsummary: {summary}\ncontent: {}",
|
is_error,
|
||||||
entry.id,
|
..
|
||||||
content.as_deref().unwrap_or_default()
|
} => 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),
|
Item::Reasoning { .. } => format!("[{} Reasoning omitted]", entry.id),
|
||||||
};
|
};
|
||||||
truncate_chars(&text, max_bytes)
|
truncate_chars(&text, max_bytes)
|
||||||
|
|
@ -488,6 +515,7 @@ mod tests {
|
||||||
ReadSelector::EntryRange([0, 3]),
|
ReadSelector::EntryRange([0, 3]),
|
||||||
ReadOptions {
|
ReadOptions {
|
||||||
include_tools: false,
|
include_tools: false,
|
||||||
|
detail: ReadDetail::Compact,
|
||||||
max_items: 10,
|
max_items: 10,
|
||||||
max_bytes: 1_000,
|
max_bytes: 1_000,
|
||||||
..ReadOptions::default()
|
..ReadOptions::default()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user