historyを返すプロトコル
This commit is contained in:
parent
29e1bc8253
commit
0332d446cd
|
|
@ -3,5 +3,6 @@
|
|||
---
|
||||
|
||||
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
||||
ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
||||
Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
||||
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
|
||||
Ticketを追加する際は、合わせてTODOも書くこと。
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
---
|
||||
|
||||
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
||||
ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
||||
Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
||||
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
|
||||
Ticketを追加する際は、合わせてTODOも書くこと。
|
||||
|
|
|
|||
2
TODO.md
2
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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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::<Method>().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::<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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ impl App {
|
|||
self.push_status(format!("[run end] {result:?}"));
|
||||
}
|
||||
Event::ToolCallArgsDelta { .. } => {}
|
||||
Event::History { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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 チケットで扱う。
|
||||
Loading…
Reference in New Issue
Block a user