memory-search-tool完了

This commit is contained in:
Keisuke Hirata 2026-04-27 17:26:07 +09:00
parent 56c6758da5
commit d0a1eaeb57
5 changed files with 34 additions and 170 deletions

View File

@ -14,7 +14,6 @@
- [ ] TUI 補完 + 型付き atom 化 → [tickets/submit-tui-completion.md](tickets/submit-tui-completion.md)
- [ ] セッションログの Segment 保持 → [tickets/session-log-segments.md](tickets/session-log-segments.md)
- [ ] メモリ機構
- [ ] memory / Knowledge 検索ツール → [tickets/memory-search-tools.md](tickets/memory-search-tools.md)
- [ ] `model_invokation: ON` の常駐注入 → [tickets/memory-resident-injection.md](tickets/memory-resident-injection.md)
- [ ] Phase 1 活動抽出 → [tickets/memory-phase1-extract.md](tickets/memory-phase1-extract.md)
- [ ] Phase 2 consolidation → [tickets/memory-phase2-consolidation.md](tickets/memory-phase2-consolidation.md)

View File

@ -29,15 +29,18 @@ const DEFAULT_HIT_LIMIT: usize = 20;
const DEFAULT_EXCERPT_LINES: usize = 3;
const MEMORY_SEARCH_DESCRIPTION: &str = "Search memory records (summary / decisions / \
requests) for a substring. Returns up to `hit_limit` matches as `{slug, kind, excerpt}` \
entries with line context. Use the returned `slug` + `kind` with MemoryRead to fetch \
the full record. Workflow and staging directories are not searched.";
requests) for a substring. Returns up to a hit cap (configurable via the manifest's \
`[memory]` section) as `{slug, kind, excerpt}` entries with line context. Use the \
returned `slug` + `kind` with MemoryRead to fetch the full record. Workflow and \
staging directories are not searched.";
const KNOWLEDGE_SEARCH_DESCRIPTION: &str = "Search knowledge records for a substring. \
Optional `kind` filters by the Knowledge frontmatter's `kind` field. Returns up to \
`hit_limit` matches as `{slug, kind, description, model_invokation, excerpt}` entries \
with line context. Use the returned `slug` with MemoryRead (kind=knowledge) for the \
full record.";
Optional `kind` filters by the Knowledge frontmatter's `kind` field; records whose \
frontmatter fails to parse are skipped when `kind` is given (the body is still \
searched when `kind` is omitted). Returns up to a hit cap (configurable via the \
manifest's `[memory]` section) as `{slug, kind, description, model_invokation, \
excerpt}` entries with line context. Use the returned `slug` with MemoryRead \
(kind=knowledge) for the full record.";
/// Tunables passed in from the manifest.
#[derive(Debug, Clone, Copy)]
@ -56,6 +59,19 @@ impl Default for SearchConfig {
}
}
impl From<&manifest::MemoryConfig> for SearchConfig {
fn from(cfg: &manifest::MemoryConfig) -> Self {
let mut out = Self::default();
if let Some(n) = cfg.search_hit_limit {
out.hit_limit = n;
}
if let Some(n) = cfg.search_excerpt_lines {
out.excerpt_lines = n;
}
out
}
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct MemorySearchParams {
/// Substring to search for. Case-insensitive.
@ -110,15 +126,17 @@ impl Tool for MemorySearchTool {
let ctx = self.config.excerpt_lines;
// summary
let summary_path = self.layout.summary_path();
if summary_path.is_file() {
scan_file(&summary_path, &needle, ctx, limit - hits.len(), |excerpt| {
hits.push(MemoryHit {
slug: "summary".to_string(),
kind: "summary",
excerpt,
if hits.len() < limit {
let summary_path = self.layout.summary_path();
if summary_path.is_file() {
scan_file(&summary_path, &needle, ctx, limit - hits.len(), |excerpt| {
hits.push(MemoryHit {
slug: "summary".to_string(),
kind: "summary",
excerpt,
});
});
});
}
}
// decisions

View File

@ -246,13 +246,7 @@ impl PodController {
.clone()
.unwrap_or_else(|| pwd_for_tools.clone());
let layout = memory::WorkspaceLayout::new(workspace_root);
let mut search_cfg = memory::tool::SearchConfig::default();
if let Some(n) = mem.search_hit_limit {
search_cfg.hit_limit = n;
}
if let Some(n) = mem.search_excerpt_lines {
search_cfg.excerpt_lines = n;
}
let search_cfg = memory::tool::SearchConfig::from(mem);
worker.register_tool(memory::tool::read_tool(layout.clone()));
worker.register_tool(memory::tool::write_tool(layout.clone()));
worker.register_tool(memory::tool::edit_tool(layout.clone()));

View File

@ -1,84 +0,0 @@
# メモリ機構: memory / Knowledge 検索ツール
## 背景
`docs/plan/memory.md` §retrieval 経路 の 2 本の検索ツールを Pod から呼べる LLM ツールとして実装する。memory 検索と Knowledge 検索は対象ディレクトリが違うだけ。grep ベースで始め、FTS / vector は将来検討。
このツールは Phase 2 Pod の agentic 探索経路としても、通常 Pod の `#<slug>` 展開経路としても、使用頻度メトリクスの観測点としても使う(メトリクスの hook 挿入は本チケットの範囲外、経路だけ揃える)。
slug 完全一致 1 件返しは検索ツールではなく `MemoryRead`kind+slug 入力)が担当する。検索 → slug 入手 → Read で本文展開、という二段経路に整理する。
## 要件
### ツール構成
- **MemorySearch**: memory 配下の検索専用 Tool
- **KnowledgeSearch**: knowledge 配下の検索専用 Tool
- 単一ファイル取得slug 完全一致)は `MemoryRead`(既存、本チケットで `kind+slug` 入力に切り替え)
### `MemoryRead` の入力変更memory-file-format からの補正)
- 旧: `file_path`(絶対パス)
- 新: `kind: summary | decision | request | knowledge` + `slug`summary は slug なし)
- 同じ補正を `MemoryWrite` / `MemoryEdit` にも適用し、Search 出力をそのまま投げ込める一貫した経路にする
### MemorySearch ツール仕様
- Input:
- `query: string`自由文字列、必須、case-insensitive 部分一致)
- Output: `{ slug, kind, excerpt }[]`
- `kind` は record kind`summary` / `decision` / `request`
- summary は slug を `"summary"` 固定で返す
- `excerpt` はマッチ行の前後 N 行
- 対象: `memory/summary.md`, `memory/decisions/*.md`, `memory/requests/*.md`
- 除外: `memory/workflow/`, `memory/_staging/`(読みもしない)
### KnowledgeSearch ツール仕様
- Input:
- `query: string`(必須)
- `kind: string`任意、KnowledgeFrontmatter の `kind` フィールドで filter
- Output: `{ slug, kind, description, model_invokation, excerpt }[]`
- `kind` / `description` / `model_invokation` は frontmatter から
- frontmatter parse 失敗時は `null`(本文 grep は機能継続)
- 対象: `knowledge/*.md`
### 共通
- ソート: ファイル名昇順 → ファイル内行順grep 出現順)
- ヒット件数上限と excerpt 行数は manifest `[memory]` で tune`search_hit_limit` / `search_excerpt_lines`、デフォルト 20 / 前後 3 行)
- 対象ファイルは都度スキャン、派生 index は持たない
- frontmatter も検索対象に含める
### 登録
- 通常 Pod と Phase 2 Pod の両方に渡せる tool 定義
- Phase 2 Pod には Knowledge 検索を必ず渡す(全 Knowledge 本文を prompt に埋めない前提)
- 本チケットでは通常 Pod の controller への登録まで実装。Phase 2 Pod 側の配布は別チケット
## 範囲外
- 使用頻度メトリクス本体hook 点の予約のみ。カウント・レポートは別チケット)
- slug サジェスト補完 UITUI 側、別途)
- FTS / vector index
- 常駐注入(別チケット)
- Phase 2 Pod 側のツール配布Phase 2 チケットの責務)
## 完了条件
- 通常 Pod から MemorySearch / KnowledgeSearch / MemoryRead / MemoryWrite / MemoryEdit すべて呼べる
- Search が返す `slug` + `kind` をそのまま `MemoryRead` に渡して本文取得が成立する(`#<slug>` 展開経路)
- `query` 指定で frontmatter 含む全文から excerpt 付きでヒットが返る
- KnowledgeSearch の `kind` filterfrontmatter.kind 完全一致)が効く
- 対象外ディレクトリ(`memory/workflow/`, `memory/_staging/`)はヒットしない
- `search_hit_limit` / `search_excerpt_lines` の tuning が効く
## 参照
- `docs/plan/memory.md` §retrieval 経路 / §Knowledge の採択基準
- `tickets/memory-file-format.md`(依存: frontmatter スキーマ、本チケットで Read/Write/Edit 入力 schema を補正)
## Review
- 状態: Approve
- レビュー詳細: [./memory-search-tools.review.md](./memory-search-tools.review.md)
- 日付: 2026-04-27

View File

@ -1,63 +0,0 @@
# Review: メモリ機構: memory / Knowledge 検索ツール
## 前提・要件の確認
### ツール構成
- MemorySearch / KnowledgeSearch を新規 Tool として実装、単一取得は MemoryRead に役割分離 — `crates/memory/src/tool/search.rs`、`tool/mod.rs:21-24` で `read_tool` / `write_tool` / `edit_tool` / `memory_search_tool` / `knowledge_search_tool` を re-export。チケット冒頭の「slug 完全一致は MemoryRead 側」という補正が公開 API・ドキュコメントどちらにも反映されている (`search.rs:31-40`, `read.rs:17-20`)。
### Read/Write/Edit signature 補正memory-file-format からの繰越)
- `MemoryToolKind``tool/mod.rs:28-90` に集約し、Read/Write/Edit 全部で `kind` + `slug` 入力に切り替え済み。`Workflow` を意図的に enum から外しているため、`workflow` を投げた瞬間 deserialize で弾ける(`write.rs:230-243`, `edit.rs:240-253` のテストで担保。Search 出力 `{slug, kind}` をそのまま Read/Edit に渡せる経路は完了条件どおり成立。
- summary は `slug` 禁止、その他は必須、というルールを `MemoryToolKind::resolve_path` に一箇所で集約しており、3 ツール全部が同じ振る舞いになる。テストも `summary_with_slug_rejected` / `decision_without_slug_rejected` で担保 (`read.rs:172-187`).
### MemorySearch 仕様
- 入力 `query: string` のみ、case-insensitive 部分一致 — `validate_query` で空文字拒否 + lower-case 化 (`search.rs:224-231`)、`scan_text` で行ごとに lower-case 化して比較 (`search.rs:289-300`)。
- 出力 `{slug, kind, excerpt}[]`、summary は slug を `"summary"` 固定で返す — `search.rs:115-122``memory_search_finds_summary` テスト。
- 対象/除外ディレクトリ: `summary_path` / `decisions_dir` / `requests_dir` のみ列挙し、`workflow_dir` / `staging_dir` はそもそも触らない構造。`memory_search_excludes_workflow_and_staging` テストで「needle を含めても 0 件」を担保。
### KnowledgeSearch 仕様
- `query` 必須 + `kind` 任意フィルタ。frontmatter が壊れていても本文 grep を継続、frontmatter フィールドは `None` を返す動作 (`search.rs:182-213`)。`knowledge_search_searches_frontmatter_too` で frontmatter 文字列にもヒットすることを担保。
- 出力スキーマ差別化description / model_invokation を持つ)はチケット改訂版にも記述あり、原チケットの「同型」を読み手にも明示している。
### 共通・ソート・上限
- `list_md_files` がスラグ昇順ソート (`search.rs:256`)、ファイル内は `lines().enumerate()` で出現順 — チケットの「ファイル名昇順 → 行順」と一致。
- `manifest [memory]` から `search_hit_limit` / `search_excerpt_lines` を tune できる経路あり (`crates/manifest/src/lib.rs:54-64`, `config.rs:204-212`, `controller.rs:249-260`)。デフォルト 20 / 3 行で原仕様と一致。
- `memory_search_respects_hit_limit` / `memory_search_excerpt_includes_context_lines` テストで両方が効くことを担保。
### 登録
- `pod/src/controller.rs:256-260` で memory enabled 時にのみ 5 ツールが並列登録される。Phase 2 Pod 側はチケットでも明示的に範囲外と書かれていて整合。
### 完了条件マッピング
- 通常 Pod から 5 ツール呼べる ✅ (`controller.rs`)
- Search → Read 経路成立 ✅(共通 `MemoryToolKind` で型一致)
- frontmatter 含む全文ヒット ✅ (`scan_text` は raw 全体を走査、専用テストあり)
- `kind` filter ✅ (`knowledge_search_kind_filter` テスト)
- 対象外ディレクトリ非ヒット ✅
- tuning 可 ✅
## アーキテクチャ・スコープ
- `crates/memory/src/tool/` にファイル分割で同居、既存 read/write/edit と完全に同じパターン。`MemoryToolKind` を `mod.rs` に置いて 4 ツール全部が共有しているのは適切。
- 依存追加なし(`std::fs` / `serde_yaml` / 既存の `schema::*` のみ)。`cargo add` ルールに抵触しない。
- `ToolDefinition``Arc<Fn>` で返す既存ファクトリ規約を踏襲し、`SearchConfig` を closure に move するだけのミニマルな受け渡し。
- `manifest::MemoryConfig` への 2 フィールド追加は `Option<usize>` + cascade で merge する既存パターンに従っている。
- スコープ越境なしllm-worker / pod / scope の責務境界に変更なし。「memory crate は self-contained」という既存ポリシー (`lib.rs:1-7`) もそのまま守られている。
- 派生 index を持たず都度スキャンとしている点も、原チケットの「FTS / vector は将来」とブレなし。
## 指摘事項
### Blocking
なし。
### Non-blocking / Follow-up任意
- **`search.rs:115-122` の summary 走査が `hit_limit` 上限ガードを通っていない**。現状は `limit - hits.len()``scan_file` に渡しているので `limit == 0` でも 0 が伝わって動作上は問題ないが、可読性として decisions / requests と同じ `if hits.len() < limit` ガードを前置きしておく方が一貫し、将来の修正で誤って `usize` underflow を踏み込む隙を消せる。
- **`SearchConfig` 適用ロジックが controller 側に展開コードとして書かれている** (`controller.rs:249-255`)。`MemoryConfig` → `SearchConfig` の変換を `From` impl で memory crate 側に持たせるとテストしやすく、controller を細くできる。manifest を memory に依存させたくない場合は逆向き (`manifest` 側に `to_search_config`) でも可。今のままでも動くので任意。
- **frontmatter の YAML パース失敗時、`kind` filter 指定があるとそのファイルが完全に excluded になる** (`search.rs:189-198`)。仕様上「frontmatter parse 失敗時は本文 grep 継続」と書かれているので、kind filter 指定時のみ skip するのは spec 通りfilter で絞り込みたいユーザーが unknown-kind のファイルを取りたいケースは無い。ただしドキュコメントに「kind filter ありで frontmatter 壊れているファイルは skip」と一行足しておくと混乱しない。
- **`KnowledgeSearchTool` は frontmatter parse 失敗ファイルも逐次 lower-case 比較するため負荷がやや高い**。本チケットの「都度スキャン」「FTS は将来」前提なので問題視はしないが、`hit_limit` を 0 や巨大値にしたユーザーに対して description などに足切り入れる余地は将来検討。
### Nits
- `MEMORY_SEARCH_DESCRIPTION` / `KNOWLEDGE_SEARCH_DESCRIPTION``hit_limit` に言及しているが、tool input にはこのフィールドが無いmanifest 側 tunable。LLM が `hit_limit` を引数に入れたくならないか軽く心配。「(configurable via manifest)」のような括弧書きで明示してもよい。
- `parse_hits` テストヘルパーが `OwnedMemoryHit` / `OwnedKnowledgeHit` で「Owned」プレフィックスを持つが、テスト内のシリアライズ用 struct を「Owned」と呼ぶ意図が明示されていない。`parsed_*` などでも可。
## 判断
**Approve完了可** — 原チケットの要件と完了条件はすべて実装に対応付けが取れており、補正Read/Write/Edit signature 切り替え・出力スキーマ差別化・slug 完全一致を Read 側へ移譲)はチケット本文にも明確に反映されている。コードベースの既存パターンを踏襲しており、歪みも gold-plating も無し。指摘は全て任意の follow-up。