From 0332d446cd12aba09e708e453f3c58ec6448c1ea Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 12 Apr 2026 03:37:49 +0900 Subject: [PATCH] =?UTF-8?q?history=E3=82=92=E8=BF=94=E3=81=99=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=88=E3=82=B3=E3=83=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 3 +- CLAUDE.md | 3 +- TODO.md | 2 +- crates/pod/src/controller.rs | 7 +++ crates/pod/src/shared_state.rs | 4 ++ crates/pod/src/socket_server.rs | 57 ++++++++++++++--------- crates/protocol/src/lib.rs | 23 ++++++++++ crates/tui/src/app.rs | 1 + tickets/request-response-protocol.md | 69 ---------------------------- 9 files changed, 75 insertions(+), 94 deletions(-) delete mode 100644 tickets/request-response-protocol.md diff --git a/AGENTS.md b/AGENTS.md index d48d3ffe..374932f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,5 +3,6 @@ --- `TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。 -ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。 +Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。 TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。 +Ticketを追加する際は、合わせてTODOも書くこと。 diff --git a/CLAUDE.md b/CLAUDE.md index d48d3ffe..374932f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,5 +3,6 @@ --- `TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。 -ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。 +Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。 TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。 +Ticketを追加する際は、合わせてTODOも書くこと。 diff --git a/TODO.md b/TODO.md index 1c5b6d18..d13297a7 100644 --- a/TODO.md +++ b/TODO.md @@ -14,5 +14,5 @@ - [x] Hook モジュールの llm-worker からの除去 → [tickets/remove-hook-module.md](tickets/remove-hook-module.md) - [x] api_key_file: ファイルパスによるAPIキー解決 → [tickets/api-key-file.md](tickets/api-key-file.md) - [ ] コンテキスト圧縮 (Prune + Compact) → [tickets/context-compaction.md](tickets/context-compaction.md) -- [ ] Protocol: request-response パターン (GetHistory等) → [tickets/request-response-protocol.md](tickets/request-response-protocol.md) +- [x] Protocol: request-response パターン (GetHistory等) → [tickets/request-response-protocol.md](tickets/request-response-protocol.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) diff --git a/crates/pod/src/controller.rs b/crates/pod/src/controller.rs index 72af0182..ea114aba 100644 --- a/crates/pod/src/controller.rs +++ b/crates/pod/src/controller.rs @@ -231,6 +231,10 @@ impl PodController { message: "Pod is not running".into(), }); } + + // GetHistory is handled at the socket layer (direct response). + // If it somehow reaches the controller, ignore it. + Method::GetHistory => {} } } }); @@ -287,6 +291,9 @@ where message: "Pod is already executing a turn".into(), }); } + Some(Method::GetHistory) => { + // Handled at socket layer; ignore here. + } None => { let _ = cancel_tx.try_send(()); shared_state.set_status(PodStatus::Idle); diff --git a/crates/pod/src/shared_state.rs b/crates/pod/src/shared_state.rs index 1bf09121..a4c47235 100644 --- a/crates/pod/src/shared_state.rs +++ b/crates/pod/src/shared_state.rs @@ -49,6 +49,10 @@ impl PodSharedState { self.status.read().map(|s| *s).unwrap_or(PodStatus::Idle) } + pub fn history(&self) -> Vec { + self.history.read().map(|h| h.clone()).unwrap_or_default() + } + pub fn update_history(&self, items: Vec) { if let Ok(mut h) = self.history.write() { *h = items; diff --git a/crates/pod/src/socket_server.rs b/crates/pod/src/socket_server.rs index 13d79968..b22f8b0f 100644 --- a/crates/pod/src/socket_server.rs +++ b/crates/pod/src/socket_server.rs @@ -66,31 +66,44 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) { let mut writer = JsonLineWriter::new(writer); let mut rx = handle.subscribe(); - // Event writer: broadcast events → socket - let write_task = tokio::spawn(async move { - while let Ok(event) = rx.recv().await { - if writer.write(&event).await.is_err() { - break; - } - } - }); - - // Method reader: socket → controller loop { - match reader.next::().await { - Ok(Some(method)) => { - let _ = handle.send(method).await; + tokio::select! { + // Broadcast events → this client + event = rx.recv() => { + match event { + Ok(event) => { + if writer.write(&event).await.is_err() { + break; + } + } + Err(_) => break, + } } - Ok(None) => break, - Err(e) => { - let _ = handle.send_event(Event::Error { - code: protocol::ErrorCode::Internal, - message: format!("invalid method: {e}"), - }); + // Client methods → handle or forward to controller + method = reader.next::() => { + match method { + Ok(Some(Method::GetHistory)) => { + let items = handle.shared_state.history(); + let values = items + .iter() + .map(|item| serde_json::to_value(item).expect("Item is Serialize")) + .collect(); + if writer.write(&Event::History { items: values }).await.is_err() { + break; + } + } + Ok(Some(method)) => { + let _ = handle.send(method).await; + } + Ok(None) => break, + Err(e) => { + let _ = handle.send_event(Event::Error { + code: protocol::ErrorCode::Internal, + message: format!("invalid method: {e}"), + }); + } + } } } } - - // Client disconnected — stop the write task - write_task.abort(); } diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 2b2f8dd9..a49cff47 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -12,6 +12,7 @@ pub enum Method { Run { input: String }, Resume, Cancel, + GetHistory, } // --------------------------------------------------------------------------- @@ -63,6 +64,9 @@ pub enum Event { code: ErrorCode, message: String, }, + History { + items: Vec, + }, } // --------------------------------------------------------------------------- @@ -138,6 +142,25 @@ mod tests { assert_eq!(parsed["data"]["result"], "limit_reached"); } + #[test] + fn method_get_history() { + let json = r#"{"method":"get_history"}"#; + let method: Method = serde_json::from_str(json).unwrap(); + assert!(matches!(method, Method::GetHistory)); + } + + #[test] + fn event_history_format() { + let event = Event::History { + items: vec![serde_json::json!({"type": "message", "role": "user"})], + }; + let json = serde_json::to_string(&event).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed["event"], "history"); + assert!(parsed["data"]["items"].is_array()); + assert_eq!(parsed["data"]["items"][0]["role"], "user"); + } + #[test] fn event_error_format() { let event = Event::Error { diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 38a4c9f6..9e4f5264 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -135,6 +135,7 @@ impl App { self.push_status(format!("[run end] {result:?}")); } Event::ToolCallArgsDelta { .. } => {} + Event::History { .. } => {} } } diff --git a/tickets/request-response-protocol.md b/tickets/request-response-protocol.md deleted file mode 100644 index f829a5ce..00000000 --- a/tickets/request-response-protocol.md +++ /dev/null @@ -1,69 +0,0 @@ -# Protocol: request-response パターン導入 - -## 背景 - -現在の Pod Protocol は fire-and-forget(Method 送信)+ broadcast(Event 受信)のみ。 -クライアントが Pod に問い合わせて応答を受け取る request-response パターンがない。 - -これが必要になるケース: -1. **GetHistory**: TUI 接続時にセッション履歴を取得 -2. **Permission ask**: ツール実行の許可をクライアントに問い合わせ(permission-extension-point チケット) - -## 設計 - -### 方式: handle_connection 内での直接応答 - -broadcast channel を変更せず、接続ごとの writer に直接返す。 - -```rust -// handle_connection 内 -match method { - Method::GetHistory => { - // broadcast を経由せず、要求元の writer に直接返す - let items = handle.shared_state.history(); - writer.write(&Event::History { items }).await; - } - other => handle.send(other).await, // 既存: controller へ転送 -} -``` - -- broadcast の仕組みに手を入れない -- 読み取り系は SharedState から直接返せる -- controller を経由する必要がない - -### Protocol 変更 - -```rust -// Method 追加 -enum Method { - Run { input: String }, - Resume, - Cancel, - GetHistory, // NEW - // 将来: PermissionReply // permission チケットで追加 -} - -// Event 追加 -enum Event { - // ... 既存 ... - History { items: Vec }, // NEW: GetHistory への応答 -} -``` - -### TUI 接続フロー - -``` -TUI connect - → send GetHistory - ← recv History { items } ← 直接応答(この接続のみ) - → 履歴表示 - ← recv TextDelta, ... ← broadcast(通常のイベントストリーム) -``` - -## 将来の拡張 - -Permission の `ask` は双方向のやりとりが必要で、より複雑: -- Pod → Client: `Event::PermissionRequest { id, tool, args }` -- Client → Pod: `Method::PermissionReply { id, allow: bool }` - -これは request-response の逆方向(Pod が要求元)になるが、同じソケット上の双方向通信として自然に実現できる。詳細は permission-extension-point チケットで扱う。