historyを返すプロトコル
This commit is contained in:
parent
29e1bc8253
commit
0332d446cd
|
|
@ -3,5 +3,6 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
||||||
ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
||||||
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
|
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
|
||||||
|
Ticketを追加する際は、合わせてTODOも書くこと。
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
||||||
ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
Ticketは完了したら削除され、`TODO.md`はチェックを付けて積まれていく。
|
||||||
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
|
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] 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)
|
- [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)
|
- [ ] コンテキスト圧縮 (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)
|
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,10 @@ impl PodController {
|
||||||
message: "Pod is not running".into(),
|
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(),
|
message: "Pod is already executing a turn".into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Some(Method::GetHistory) => {
|
||||||
|
// Handled at socket layer; ignore here.
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let _ = cancel_tx.try_send(());
|
let _ = cancel_tx.try_send(());
|
||||||
shared_state.set_status(PodStatus::Idle);
|
shared_state.set_status(PodStatus::Idle);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ impl PodSharedState {
|
||||||
self.status.read().map(|s| *s).unwrap_or(PodStatus::Idle)
|
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>) {
|
pub fn update_history(&self, items: Vec<Item>) {
|
||||||
if let Ok(mut h) = self.history.write() {
|
if let Ok(mut h) = self.history.write() {
|
||||||
*h = items;
|
*h = items;
|
||||||
|
|
|
||||||
|
|
@ -66,31 +66,44 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) {
|
||||||
let mut writer = JsonLineWriter::new(writer);
|
let mut writer = JsonLineWriter::new(writer);
|
||||||
let mut rx = handle.subscribe();
|
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 {
|
loop {
|
||||||
match reader.next::<Method>().await {
|
tokio::select! {
|
||||||
Ok(Some(method)) => {
|
// Broadcast events → this client
|
||||||
let _ = handle.send(method).await;
|
event = rx.recv() => {
|
||||||
|
match event {
|
||||||
|
Ok(event) => {
|
||||||
|
if writer.write(&event).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => break,
|
// Client methods → handle or forward to controller
|
||||||
Err(e) => {
|
method = reader.next::<Method>() => {
|
||||||
let _ = handle.send_event(Event::Error {
|
match method {
|
||||||
code: protocol::ErrorCode::Internal,
|
Ok(Some(Method::GetHistory)) => {
|
||||||
message: format!("invalid method: {e}"),
|
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 },
|
Run { input: String },
|
||||||
Resume,
|
Resume,
|
||||||
Cancel,
|
Cancel,
|
||||||
|
GetHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -63,6 +64,9 @@ pub enum Event {
|
||||||
code: ErrorCode,
|
code: ErrorCode,
|
||||||
message: String,
|
message: String,
|
||||||
},
|
},
|
||||||
|
History {
|
||||||
|
items: Vec<serde_json::Value>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -138,6 +142,25 @@ mod tests {
|
||||||
assert_eq!(parsed["data"]["result"], "limit_reached");
|
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]
|
#[test]
|
||||||
fn event_error_format() {
|
fn event_error_format() {
|
||||||
let event = Event::Error {
|
let event = Event::Error {
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ impl App {
|
||||||
self.push_status(format!("[run end] {result:?}"));
|
self.push_status(format!("[run end] {result:?}"));
|
||||||
}
|
}
|
||||||
Event::ToolCallArgsDelta { .. } => {}
|
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