podのモジュール分割

This commit is contained in:
Keisuke Hirata 2026-04-24 11:48:27 +09:00
parent 30f9abacb8
commit 4763173f36
35 changed files with 238 additions and 94 deletions

View File

@ -7,6 +7,7 @@
- [ ] LLM モデルカタログ + マニフェスト ref 解決 → [tickets/llm-model-catalog.md](tickets/llm-model-catalog.md)
- [ ] Pod オーケストレーション
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
- [ ] pod クレートのモジュール分割 → [tickets/pod-module-layout.md](tickets/pod-module-layout.md)
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
- [ ] TUI 拡充
- [ ] フルスクリーン化によるオーバーホール → [tickets/tui-fullscreen-overhaul.md](tickets/tui-fullscreen-overhaul.md)

View File

@ -0,0 +1,5 @@
pub(crate) mod prune;
pub(crate) mod state;
pub(crate) mod token_counter;
pub(crate) mod usage_tracker;
pub(crate) mod worker;

View File

@ -12,7 +12,7 @@ use llm_worker::prune::{PruneConfig, SavingsEstimator};
use session_store::Store;
use crate::Pod;
use crate::token_counter::{EstimateSource, savings_for_prune_impl};
use crate::compact::token_counter::{EstimateSource, savings_for_prune_impl};
impl<C: LlmClient, St: Store> Pod<C, St> {
/// Enable prune projection on the underlying Worker.

View File

@ -6,17 +6,17 @@ use llm_worker::llm_client::client::LlmClient;
use session_store::Store;
use tokio::sync::{broadcast, mpsc, oneshot};
use crate::notification_buffer::NotificationBuffer;
use crate::notifier::Notifier;
use crate::ipc::notification_buffer::NotificationBuffer;
use crate::ipc::notifier::Notifier;
use crate::pod::{Pod, PodError, PodRunResult};
use crate::pod_comm_tools::{
use crate::spawn::comm_tools::{
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
};
use crate::runtime_dir::RuntimeDir;
use crate::runtime::dir::RuntimeDir;
use crate::shared_state::{PodSharedState, PodStatus};
use crate::socket_server::SocketServer;
use crate::spawn_pod::spawn_pod_tool;
use crate::spawned_pod_registry::SpawnedPodRegistry;
use crate::ipc::server::SocketServer;
use crate::spawn::tool::spawn_pod_tool;
use crate::spawn::registry::SpawnedPodRegistry;
use protocol::{ErrorCode, Event, Method, NotificationLevel, NotificationSource, RunResult, TurnResult};
// ---------------------------------------------------------------------------
@ -463,7 +463,7 @@ impl PodController {
// (1) system side effects — idempotent and
// tolerant of out-of-order delivery (e.g.
// `TurnEnded` arriving after `ShutDown`).
crate::pod_events::apply_event_side_effects(
crate::ipc::event::apply_event_side_effects(
&event,
&spawned_registry,
&spawner_name,
@ -474,7 +474,7 @@ impl PodController {
// into the notification buffer; the next LLM
// request will inject it as a system message
// via `PodInterceptor::pre_llm_request`.
let text = crate::pod_events::render_event(&event);
let text = crate::ipc::event::render_event(&event);
pod.push_notification(text);
// Auto-kick a turn if the Pod is idle so the
// notification is not stranded. Matches the
@ -529,7 +529,7 @@ impl PodController {
// `connect_and_send` helper enforces a 5 s timeout so a
// stuck parent cannot block process exit indefinitely.
if let Some(parent) = self_parent_socket.as_ref() {
if let Err(e) = crate::pod_events::send_pod_event(
if let Err(e) = crate::ipc::event::send_pod_event(
parent,
protocol::PodEvent::ShutDown {
pod_name: spawner_name.clone(),
@ -587,7 +587,7 @@ where
};
let _ = event_tx.send(Event::RunEnd { result: run_result });
if matches!(run_result, RunResult::Finished) {
crate::pod_events::fire_and_forget(
crate::ipc::event::fire_and_forget(
parent_socket.cloned(),
protocol::PodEvent::TurnEnded {
pod_name: self_name.to_string(),
@ -612,7 +612,7 @@ where
code,
message: message.clone(),
});
crate::pod_events::fire_and_forget(
crate::ipc::event::fire_and_forget(
parent_socket.cloned(),
protocol::PodEvent::Errored {
pod_name: self_name.to_string(),
@ -657,14 +657,14 @@ where
// notification buffer so the in-flight turn's
// next `pre_llm_request` surfaces it.
let self_parent_socket = parent_socket.cloned();
crate::pod_events::apply_event_side_effects(
crate::ipc::event::apply_event_side_effects(
&event,
spawned_registry,
self_name,
&self_parent_socket,
)
.await;
notification_buffer.push(crate::pod_events::render_event(&event));
notification_buffer.push(crate::ipc::event::render_event(&event));
}
None => {
let _ = cancel_tx.try_send(());

View File

@ -29,7 +29,7 @@ use std::path::{Path, PathBuf};
use manifest::{PodManifest, PodManifestConfig, ResolveError};
use crate::prompt_loader::PromptLoader;
use crate::prompt::loader::PromptLoader;
/// Errors raised while building a [`PodManifest`] from cascade layers.
#[derive(Debug, thiserror::Error)]
@ -614,7 +614,7 @@ permission = "write"
#[test]
fn resolve_produces_loader_with_workspace_prompts_dir() {
use crate::system_prompt::{SystemPromptContext, SystemPromptTemplate};
use crate::prompt::system::{SystemPromptContext, SystemPromptTemplate};
use manifest::{Permission, Scope, ScopeConfig, ScopeRule};
let tmp = TempDir::new().unwrap();
@ -664,7 +664,7 @@ permission = "write"
deny: Vec::new(),
};
let scope = Scope::from_config(&scope_cfg).unwrap();
let catalog = crate::prompts::PromptCatalog::builtins_only().unwrap();
let catalog = crate::prompt::catalog::PromptCatalog::builtins_only().unwrap();
let ctx = SystemPromptContext {
now: chrono::Utc::now(),
cwd: &root,

View File

@ -15,7 +15,7 @@ use session_store::Store;
use crate::pod::{Pod, PodError, PodRunResult};
#[cfg(test)]
use crate::prompts::PromptCatalog;
use crate::prompt::catalog::PromptCatalog;
impl<C: LlmClient, St: Store> Pod<C, St> {
/// Close out the current (paused) turn and start a new one with `input`.

View File

@ -26,10 +26,10 @@ use std::sync::Arc;
use protocol::{Method, PodEvent, ScopeRule};
use crate::pod_comm_tools::connect_and_send;
use crate::runtime_dir::SpawnedPodRecord;
use crate::scope_lock::{self, ScopeLockError};
use crate::spawned_pod_registry::SpawnedPodRegistry;
use crate::spawn::comm_tools::connect_and_send;
use crate::runtime::dir::SpawnedPodRecord;
use crate::runtime::scope_lock::{self, ScopeLockError};
use crate::spawn::registry::SpawnedPodRegistry;
/// Connect to `socket`, send a single `Method::PodEvent(event)`, and
/// return. Used by children to report up to their parent.

View File

@ -20,14 +20,14 @@ use llm_worker::tool::ToolOutput;
use session_store::UsageRecord;
use tracing::info;
use crate::compact_state::CompactState;
use crate::compact::state::CompactState;
use crate::hook::{
AbortInfo, HookRegistry, PreRequestInfo, PromptSubmitInfo, ToolCallSummary, ToolResultSummary,
TurnEndInfo,
};
use crate::notification_buffer::{NotificationBuffer, format_notification};
use crate::prompts::PromptCatalog;
use crate::token_counter::total_tokens_impl;
use crate::ipc::notification_buffer::{NotificationBuffer, format_notification};
use crate::prompt::catalog::PromptCatalog;
use crate::compact::token_counter::total_tokens_impl;
use tracing::warn;
/// Maximum number of bytes copied into `TurnEndInfo::final_text_preview`.

View File

@ -0,0 +1,6 @@
pub mod event;
pub mod notifier;
pub mod server;
pub(crate) mod interceptor;
pub(crate) mod notification_buffer;

View File

@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex};
use llm_worker::Item;
use tracing::warn;
use crate::prompts::{CatalogError, PromptCatalog};
use crate::prompt::catalog::{CatalogError, PromptCatalog};
/// Maximum queued notifications. Oldest entries are dropped beyond this.
const CAPACITY: usize = 128;

View File

@ -1,45 +1,30 @@
pub mod compact;
pub mod controller;
pub mod hook;
pub mod notifier;
pub mod runtime_dir;
pub mod scope_lock;
pub mod ipc;
pub mod prompt;
pub mod runtime;
pub mod shared_state;
pub mod pod_comm_tools;
pub mod pod_events;
pub mod socket_server;
pub mod spawn_pod;
pub mod spawned_pod_registry;
pub mod spawn;
mod agents_md;
mod compact_state;
mod compact_worker;
mod factory;
mod interrupt_and_run;
mod notification_buffer;
mod pod;
mod pod_interceptor;
mod prompt_loader;
mod prompts;
mod prune;
mod system_prompt;
mod token_counter;
mod usage_tracker;
pub use token_counter::{EstimateSource, SplitPoint, TokenEstimate};
pub use compact::token_counter::{EstimateSource, SplitPoint, TokenEstimate};
pub use controller::{PodController, PodHandle, ShutdownReceiver};
pub use factory::{FactoryError, PodFactory};
pub use notifier::Notifier;
pub use hook::{Hook, HookEventKind, HookRegistryBuilder};
pub use ipc::notifier::Notifier;
pub use ipc::server::SocketServer;
pub use manifest::{
AuthRef, ModelManifest, PodManifest, PodManifestConfig, PodMetaConfig, Scope, SchemeKind,
};
pub use pod::{Pod, PodError, PodRunResult, apply_worker_manifest};
pub use prompt_loader::PromptLoader;
pub use prompts::{CatalogError, PodPrompt, PromptCatalog};
pub use prompt::catalog::{CatalogError, PodPrompt, PromptCatalog};
pub use prompt::loader::PromptLoader;
pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
pub use protocol::{ErrorCode, Event, Method, TurnResult};
pub use provider::{ProviderError, build_client};
pub use runtime_dir::RuntimeDir;
pub use runtime::dir::RuntimeDir;
pub use shared_state::{PodSharedState, PodStatus};
pub use socket_server::SocketServer;
pub use system_prompt::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};

View File

@ -13,21 +13,21 @@ use tracing::{info, warn};
use manifest::{PodManifest, PodManifestConfig, ResolveError, Scope, ScopeError, WorkerManifest};
use crate::agents_md::read_agents_md;
use crate::compact_state::CompactState;
use crate::prompt::agents_md::read_agents_md;
use crate::compact::state::CompactState;
use crate::hook::{
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
PreRequestInfo, PreToolCall,
};
use crate::notification_buffer::NotificationBuffer;
use crate::notifier::Notifier;
use crate::pod_interceptor::PodInterceptor;
use crate::prompt_loader::PromptLoader;
use crate::prompts::{CatalogError, PromptCatalog};
use crate::runtime_dir;
use crate::scope_lock::{self, ScopeAllocationGuard, ScopeLockError};
use crate::system_prompt::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
use crate::usage_tracker::UsageTracker;
use crate::ipc::notification_buffer::NotificationBuffer;
use crate::ipc::notifier::Notifier;
use crate::ipc::interceptor::PodInterceptor;
use crate::prompt::loader::PromptLoader;
use crate::prompt::catalog::{CatalogError, PromptCatalog};
use crate::runtime::dir;
use crate::runtime::scope_lock::{self, ScopeAllocationGuard, ScopeLockError};
use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
use crate::compact::usage_tracker::UsageTracker;
use protocol::{Event, NotificationLevel, NotificationSource};
use tokio::sync::broadcast;
use async_trait::async_trait;
@ -848,7 +848,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
pub async fn compact(&mut self, retained_tokens: u64) -> Result<SessionId, PodError> {
use std::sync::atomic::{AtomicU64, Ordering};
use crate::compact_worker::{
use crate::compact::worker::{
CompactWorkerContext, CompactWorkerInterceptor, add_reference_tool,
mark_read_required_tool, slice_lines, write_summary_tool,
};
@ -1125,7 +1125,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
// Register this Pod in the machine-wide scope-lock registry
// before building anything else, so a spawn that conflicts on
// scope fails fast (and without having paid for client setup).
let socket_path = runtime_dir::default_base()
let socket_path = dir::default_base()
.map_err(ScopeLockError::from)?
.join(&manifest.pod.name)
.join("sock");

View File

@ -45,13 +45,13 @@ use serde::Deserialize;
use thiserror::Error;
use tracing::warn;
use crate::prompt_loader::PromptLoader;
use crate::prompt::loader::PromptLoader;
// Generated by build.rs from `resources/prompts/internal.toml`.
include!(concat!(env!("OUT_DIR"), "/internal_keys.rs"));
/// Source of the builtin pack. Baked in at compile time.
const INTERNAL_TOML: &str = include_str!("../../../resources/prompts/internal.toml");
const INTERNAL_TOML: &str = include_str!("../../../../resources/prompts/internal.toml");
/// Pod-level prompt injection point.
///

View File

@ -104,7 +104,7 @@ pub enum LoaderError {
/// libraries. Cheap to clone.
///
/// Also carries the auto-discovered `prompts.toml` pack file paths so
/// [`crate::prompts::PromptCatalog`] can read the same user/workspace
/// [`crate::prompt::catalog::PromptCatalog`] can read the same user/workspace
/// layers without a separate plumbing channel. These fields do not
/// affect `$prefix` asset resolution — they are purely metadata
/// consulted by the catalog loader.

View File

@ -0,0 +1,4 @@
pub(crate) mod agents_md;
pub(crate) mod catalog;
pub(crate) mod loader;
pub(crate) mod system;

View File

@ -22,8 +22,8 @@ use minijinja::value::Value;
use minijinja::{Environment, ErrorKind, UndefinedBehavior};
use thiserror::Error;
use crate::prompt_loader::{LoaderError, PromptLoader, PromptRef};
use crate::prompts::{CatalogError, PromptCatalog};
use crate::prompt::loader::{LoaderError, PromptLoader, PromptRef};
use crate::prompt::catalog::{CatalogError, PromptCatalog};
#[derive(Debug, Error)]
pub enum SystemPromptError {

View File

@ -0,0 +1,2 @@
pub mod dir;
pub mod scope_lock;

View File

@ -21,9 +21,9 @@ use protocol::{ErrorCode, Event, Method};
use serde::Deserialize;
use tokio::net::UnixStream;
use crate::runtime_dir::SpawnedPodRecord;
use crate::scope_lock::{self, LockFileGuard};
use crate::spawned_pod_registry::SpawnedPodRegistry;
use crate::runtime::dir::SpawnedPodRecord;
use crate::runtime::scope_lock::{self, LockFileGuard};
use crate::spawn::registry::SpawnedPodRegistry;
/// Timeout applied to each socket-level operation — connect, write,
/// read. Kept short so a stuck child doesn't block the spawner's turn.

View File

@ -0,0 +1,3 @@
pub mod comm_tools;
pub mod registry;
pub mod tool;

View File

@ -20,7 +20,7 @@ use std::sync::Arc;
use tokio::sync::Mutex;
use crate::runtime_dir::{RuntimeDir, SpawnedPodRecord};
use crate::runtime::dir::{RuntimeDir, SpawnedPodRecord};
pub struct SpawnedPodRegistry {
records: Mutex<Vec<SpawnedPodRecord>>,

View File

@ -24,10 +24,10 @@ use tokio::net::UnixStream;
use tokio::process::Command;
use tokio::time::sleep;
use crate::pod_events;
use crate::runtime_dir::SpawnedPodRecord;
use crate::scope_lock::{self, LockFileGuard, ScopeLockError};
use crate::spawned_pod_registry::SpawnedPodRegistry;
use crate::ipc::event;
use crate::runtime::dir::SpawnedPodRecord;
use crate::runtime::scope_lock::{self, LockFileGuard, ScopeLockError};
use crate::spawn::registry::SpawnedPodRegistry;
use protocol::PodEvent;
const DESCRIPTION: &str = "Spawn a new Pod process to work on a delegated task. \
@ -233,7 +233,7 @@ impl Tool for SpawnPodTool {
// Notify this Pod's own parent so the grandparent can register
// the new grandchild directly. Fire-and-forget; top-level Pods
// (with no parent) skip the send inside `fire_and_forget`.
pod_events::fire_and_forget(
event::fire_and_forget(
self.parent_socket.clone(),
PodEvent::ScopeSubDelegated {
parent_pod: self.spawner_name.clone(),

View File

@ -14,12 +14,12 @@ use std::sync::{Arc, LazyLock, Mutex};
use llm_worker::llm_client::types::{ContentPart, Item, Role};
use llm_worker::tool::ToolOutput;
use manifest::{Permission, ScopeRule};
use pod::pod_comm_tools::{
use pod::spawn::comm_tools::{
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
};
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
use pod::scope_lock::{self, LockFileGuard};
use pod::spawned_pod_registry::SpawnedPodRegistry;
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
use pod::runtime::scope_lock::{self, LockFileGuard};
use pod::spawn::registry::SpawnedPodRegistry;
use protocol::stream::{JsonLineReader, JsonLineWriter};
use protocol::{ErrorCode, Event, Greeting, Method};
use serde_json::json;

View File

@ -9,10 +9,10 @@ use std::path::PathBuf;
use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use pod::pod_events::{apply_event_side_effects, fire_and_forget, render_event};
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
use pod::scope_lock::{self, LockFileGuard};
use pod::spawned_pod_registry::SpawnedPodRegistry;
use pod::ipc::event::{apply_event_side_effects, fire_and_forget, render_event};
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
use pod::runtime::scope_lock::{self, LockFileGuard};
use pod::spawn::registry::SpawnedPodRegistry;
use protocol::stream::JsonLineReader;
use protocol::{Method, Permission, PodEvent, ScopeRule};
use tempfile::TempDir;

View File

@ -12,10 +12,10 @@ use std::sync::{LazyLock, Mutex};
use llm_worker::tool::{ToolError, ToolOutput};
use manifest::{AuthRef, ModelManifest, Permission, SchemeKind, ScopeRule};
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
use pod::scope_lock::{self, LockFileGuard};
use pod::spawn_pod::spawn_pod_tool;
use pod::spawned_pod_registry::SpawnedPodRegistry;
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
use pod::runtime::scope_lock::{self, LockFileGuard};
use pod::spawn::tool::spawn_pod_tool;
use pod::spawn::registry::SpawnedPodRegistry;
use protocol::Method;
use protocol::stream::JsonLineReader;
use serde_json::json;

View File

@ -0,0 +1,97 @@
# pod クレートのモジュール分割
## 背景
`crates/pod/src/` は 25 ファイル・約 10k 行がフラットに並んでおり、責務の軸が立っていない。命名も `pod_events` / `pod_interceptor` / `pod_comm_tools` のように、所属クレート名である `pod_` prefix を冗長に付けているものと、`spawn_pod` / `spawned_pod_registry` のように prefix なしのものが混在しており、どの単位で読むべきかが構造から読み取れない。
同規模の `llm-worker``llm_client/scheme/{anthropic,gemini,...}/``timeline/` でサブモジュール化されており、`provider` も `codex_oauth/` を切っている。`pod` だけが規模に対して階層を持っていない。
## 要件
### 目標レイアウト
```
crates/pod/src/
├── lib.rs
├── main.rs
├── pod.rs ← Pod 本体
├── controller.rs ← PodController / PodHandle
├── factory.rs ← manifest cascade からの組み立て
├── shared_state.rs ← 横断的な共有状態
├── ipc/ ← Pod ↔ クライアント(自 Pod の対外 socket
│ ├── mod.rs
│ ├── server.rs ← socket_server.rs
│ ├── event.rs ← pod_events.rs
│ ├── interceptor.rs ← pod_interceptor.rs
│ ├── notifier.rs
│ └── notification_buffer.rs
├── spawn/ ← Pod ↔ Pod親子関係とツール群
│ ├── mod.rs
│ ├── tool.rs ← spawn_pod.rs
│ ├── registry.rs ← spawned_pod_registry.rs
│ └── comm_tools.rs ← pod_comm_tools.rs
├── prompt/
│ ├── mod.rs
│ ├── catalog.rs ← prompts.rs
│ ├── loader.rs ← prompt_loader.rs
│ ├── system.rs ← system_prompt.rs
│ └── agents_md.rs
├── compact/
│ ├── mod.rs
│ ├── state.rs ← compact_state.rs
│ ├── worker.rs ← compact_worker.rs
│ ├── token_counter.rs
│ ├── usage_tracker.rs
│ └── prune.rs
├── hook.rs ← フック基盤(将来拡張予定だが現在 1 ファイルなのでフラットに残す)
├── interrupt_and_run.rs ← `impl Pod` の付加メソッドpod.rs の延長)
└── runtime/
├── mod.rs
├── dir.rs ← runtime_dir.rs
└── scope_lock.rs
```
### 軸の置き方
- **`ipc/``spawn/` を分離**する: 前者は「自 Pod 外界(クライアント)との通信」、後者は「他 Podとの通信」。対向が違うので混ぜない。
- **`compact/` に会計系を集約**する: `token_counter` / `usage_tracker` / `prune` は compaction の判断材料として利用側から見ると一塊。
- **トップレベルに残すのは核 4 個**: `pod` / `controller` / `factory` / `shared_state`。どのサブに入れても不自然になる「束ねる側」。
- **`pod_` prefix を全廃**する: `pod` crate 内で `pod::pod_events` は冗長。`pod::ipc::event` にする。
### 公開 API パスの変化
`lib.rs``pub mod` 経由で露出している型は外部からパスが変わる。例:
| 旧 | 新 |
|---|---|
| `pod::socket_server::SocketServer` | `pod::ipc::server::SocketServer` |
| `pod::pod_events::{...}` | `pod::ipc::event::{...}` |
| `pod::pod_comm_tools::{...}` | `pod::spawn::comm_tools::{...}` |
| `pod::spawn_pod::spawn_pod_tool` | `pod::spawn::tool::spawn_pod_tool` |
| `pod::spawned_pod_registry::SpawnedPodRegistry` | `pod::spawn::registry::SpawnedPodRegistry` |
| `pod::runtime_dir::{...}` | `pod::runtime::dir::{...}` |
| `pod::scope_lock::{...}` | `pod::runtime::scope_lock::{...}` |
現時点で `pod::XXX` パスを参照しているのは `crates/pod/tests/` のみで、他クレートからの `use pod::<submodule>` 参照は無い(`manifest` / `llm-worker` 内の該当箇所はコメントのみ)。
### スコープ外
- `pod.rs`1496 行)・`scope_lock.rs`1103 行)自体の内部分割は別チケット。本チケットは **ファイルの配置と命名の変更のみ** を扱い、1 ファイルの中身は移動だけに留める。
- `lib.rs``pub use` re-export で旧パスを残す互換レイヤは作らない(外部参照が tests のみで、一斉更新できるため)。
### 完了条件
- 新構造でビルドが通る (`cargo check`)
- `crates/pod/tests/` が全部緑 (`cargo test -p pod`)
- 既存の振る舞いに変更なし(純粋な再配置)
## Review
- 状態: Approve with follow-up
- レビュー詳細: [./pod-module-layout.review.md](./pod-module-layout.review.md)
- 日付: 2026-04-24

View File

@ -0,0 +1,41 @@
# Review: pod クレートのモジュール分割
## 前提・要件の確認
- **目標レイアウト**: ほぼチケット通り。`crates/pod/src/` 直下は `pod.rs` / `controller.rs` / `factory.rs` / `shared_state.rs` / `hook.rs` / `interrupt_and_run.rs` / `lib.rs` / `main.rs` のみとなり、`ipc/` / `spawn/` / `prompt/` / `compact/` / `runtime/` の 5 つのサブディレクトリに責務別に移動されている(`crates/pod/src/` ls 結果 + 各 `mod.rs` で確認)。
- **`ipc/``spawn/` の分離**: 自 Pod 対外 socket 系は `ipc/` (`server`, `event`, `interceptor`, `notifier`, `notification_buffer`)、親子関係系は `spawn/` (`tool`, `registry`, `comm_tools`) に分かれており、対向別の軸が保たれている。
- **`compact/` への会計系集約**: `state` / `worker` / `token_counter` / `usage_tracker` / `prune` がすべて `compact/` に収まっている。
- **`pod_` prefix 全廃**: `pod_events``ipc::event`、`pod_interceptor` → `ipc::interceptor`、`pod_comm_tools` → `spawn::comm_tools` に改名済み。`spawn_pod` / `spawned_pod_registry``spawn::tool` / `spawn::registry` に短縮。ソース・テストともに旧名は残っていないgrep 確認済み)。
- **公開 API パスの変化**: チケット表通りに移動済み。`crates/pod/tests/` 側も 3 本(`pod_comm_tools_test.rs` / `pod_events_test.rs` / `spawn_pod_test.rs`)を新パスに一斉更新。`pod::spawn::tool::spawn_pod_tool` 等は `pub mod` を経由して到達可能。
- **スコープ外(ファイル内部分割なし)**: `pod.rs` / `scope_lock.rs` の中身には手が入っておらず、差分は `use crate::...` の書き換えに限定されている(`git diff` 確認)。
- **完了条件**: `cargo check -p pod` 緑、`cargo test --workspace` が 558 件緑との申告とも整合(依存側・ビルド側でも stale な参照なし)。
## アーキテクチャ・スコープ
- **レイヤ境界**: 新しい軸は「自 Pod の外界ipc」「他 Podspawn」「プロンプト」「会計・圧縮」「ランタイム配置」という独立した関心事で、相互依存は必要最小限`ipc::event` → `runtime::scope_lock`、`ipc::interceptor` → `compact::token_counter` / `compact::state` など)。責務軸が立ち、`llm-worker` の `llm_client/scheme/` 等と釣り合う粒度になった。
- **純粋な再配置か**: 差分は (1) 物理移動、(2) `use crate::...` の書き換え、(3) `pod.rs` 内 1 箇所の `runtime_dir::default_base``dir::default_base`、(4) `interrupt_and_run.rs``#[cfg(test)] use` のパス、(5) `prompts.rs``include_str!("../../../...")` を 1 階層ぶんだけ `../../../../...` に延長(`realpath` で同一ファイルを指すことを確認)— の範囲に留まっている。振る舞い変化は混ざっていない。
- **hook/ サブディレクトリの取りやめ**: 現状 `hook.rs` が単体で ~200 行程度、かつ `interrupt_and_run.rs``impl Pod` の拡張メソッドであり `hook` の仲間ではない、という判断は妥当。無理にディレクトリを作ると「中身 1 ファイル意味の異なる隣人」という歪な構造になる。ticket 本文の現行版(行 5152は新判断と整合しており、乖離はない。
- **コードベースを歪める変更**: なし。新構造のために本体のロジックを曲げた箇所や、無駄な中間層の追加は見当たらない。
- **依存追加**: 本チケットでは外部 crate の追加はなく、`cargo add` ルールの対象外。
## 指摘事項
### Non-blocking / Follow-up
- **`pub mod` / `mod` の区別が一部で広がっている** — `crates/pod/src/lib.rs` と各 `mod.rs`。旧 `lib.rs` では次の 5 個は `mod ...;`crate-privateだった:
- `agents_md`, `prompt_loader`, `prompts`, `system_prompt`, `token_counter`
新レイアウトではそれぞれ `prompt/mod.rs``pub mod agents_md;` / `pub mod loader;` / `pub mod catalog;` / `pub mod system;``compact/mod.rs``pub mod token_counter;` になっており、`pod::prompt::catalog::PromptCatalog` のような深いパスが外部から到達可能になった(ルート `pub use` で代表型は既に公開されているため、副作用は「外から深いパスが見える」だけ)。
実害は発見できなかった(`grep -rn "pod::prompt\|pod::compact::token"` は 0 ヒット、利用側は全てルート再エクスポート経由)が、ユーザが質問 #2 で述べている「旧 lib.rs での `pub mod` / `mod` 区別を保持」という意図に厳密に従うなら、上記 5 個は `pub(crate) mod ...;` に落とす方が素直。現に `ipc::notification_buffer` / `ipc::interceptor` / `compact::state` / `compact::worker` / `compact::usage_tracker` / `compact::prune` については `pub(crate) mod` で区別を維持できており、プロンプト系だけが揃っていない。テスト側も `pod::PromptLoader` 等のルート経由で参照しているので、`pub(crate) mod` に絞っても既存テストは影響を受けないはず。
別チケット or 本チケット内の微修正のどちらでも対応可能。
### Nits
- **`lib.rs``pub use` ブロック内で `manifest`/`protocol`/`provider` の外部 crate 再エクスポートが自 crate モジュールの間に挟まっている** — `crates/pod/src/lib.rs:14-30`。旧版でも同様に混在していたので新規に悪化したわけではないが、責務軸で整理する今回のタイミングで「自 crate → 外部 crate」の順にグルーピングしても読みやすい。強い指摘ではない。
- **`pod.rs` 先頭 `use` の順序** — `crates/pod/src/pod.rs:16-30`。`crate::prompt::agents_md`, `crate::compact::state`, `crate::ipc::...`, `crate::prompt::...`, `crate::runtime::...`, `crate::compact::usage_tracker` がサブモジュール別にまとまっておらず行きつ戻りつしている。機能的には無害だが、rustfmt では並び替わらない範囲なので任意で整頓する余地がある。
## 判断
**Approve with follow-up** — チケット本文の要件配置・命名・prefix 全廃・スコープ外保持と完了条件ビルド緑・テスト緑・挙動不変はすべて満たされている。hook/ 取りやめの判断変更も合理的で本文と整合。唯一、旧 `lib.rs` での `mod`crate-privateを保持するなら `prompt/``compact::token_counter` の 5 モジュールを `pub(crate) mod` に狭める余地があるが、これはブロッキングではない。