historyを返すプロトコル

This commit is contained in:
Keisuke Hirata 2026-04-12 03:37:49 +09:00
parent 29e1bc8253
commit 0332d446cd
9 changed files with 75 additions and 94 deletions

View File

@ -3,5 +3,6 @@
---
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
Ticketを追加する際は、合わせてTODOも書くこと。

View File

@ -3,5 +3,6 @@
---
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
Ticketを追加する際は、合わせてTODOも書くこと。

View File

@ -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)

View File

@ -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);

View File

@ -49,6 +49,10 @@ impl PodSharedState {
self.status.read().map(|s| *s).unwrap_or(PodStatus::Idle)
}
pub fn history(&self) -> Vec<Item> {
self.history.read().map(|h| h.clone()).unwrap_or_default()
}
pub fn update_history(&self, items: Vec<Item>) {
if let Ok(mut h) = self.history.write() {
*h = items;

View File

@ -66,18 +66,32 @@ 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 {
loop {
tokio::select! {
// Broadcast events → this client
event = rx.recv() => {
match event {
Ok(event) => {
if writer.write(&event).await.is_err() {
break;
}
}
});
// Method reader: socket → controller
loop {
match reader.next::<Method>().await {
Err(_) => break,
}
}
// Client methods → handle or forward to controller
method = reader.next::<Method>() => {
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;
}
@ -90,7 +104,6 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) {
}
}
}
// Client disconnected — stop the write task
write_task.abort();
}
}
}

View File

@ -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<serde_json::Value>,
},
}
// ---------------------------------------------------------------------------
@ -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 {

View File

@ -135,6 +135,7 @@ impl App {
self.push_status(format!("[run end] {result:?}"));
}
Event::ToolCallArgsDelta { .. } => {}
Event::History { .. } => {}
}
}

View File

@ -1,69 +0,0 @@
# Protocol: request-response パターン導入
## 背景
現在の Pod Protocol は fire-and-forgetMethod 送信)+ broadcastEvent 受信)のみ。
クライアントが 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<Item> }, // 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 チケットで扱う。