9.6 KiB
Pod の上流イベント報告 (子 → 親)
背景
spawned Pod(子)のライフサイクルに親 Pod が反応する仕組みが必要。反応には 2 系統ある:
- system 処理:
spawn_pods.json更新、scope.lock整合、孫 Pod の把握 - LLM 通知: 親 LLM に「子でこれが起きた、次どうする?」を考えさせる
これを実現する通信 primitive を分離して定義する。既存の Method::Notify(LLM 向け自由テキスト注入)と混ぜない。
依存
tickets/spawn-pod-tool.md: 済。callback address の受け渡しと spawn 記録tickets/scope-lock.md済に含まれる: allocation の delegate/reparent は既存Method::PodEventの追加はprotocolcrate の拡張
設計
新しい primitive
pub enum Method {
Run { input: String },
Notify { message: String }, // 人間・tool → LLM 文脈、副作用なし(本チケットで source を削除)
PodEvent(PodEvent), // 新: 子 → 親、typed なライフサイクル報告
Resume,
Cancel,
Shutdown,
GetHistory,
}
pub enum PodEvent {
/// 子が1ターン終えて IDLE になった
TurnEnded { pod_name: String },
/// 子で Worker 実行エラーが発生した
Errored { pod_name: String, message: String },
/// 子が停止した
ShutDown { pod_name: String },
/// 子が孫 Pod に scope を又貸しした
ScopeSubDelegated {
parent_pod: String, // 又貸し元(= 子自身)
sub_pod: String, // 孫 Pod の名前
sub_socket: PathBuf, // 孫 Pod の socket path
scope: Vec<ScopeRule>, // 又貸しされた scope(`protocol` crate に移動)
},
}
Method::Notify の source 削除
現状 Method::Notify { source: String, message: String } の source フィールドを削除して Method::Notify { message: String } にする。PodEvent が typed な子 → 親報告を担うようになったことで、Notify は本来の「人間・tool が LLM 文脈に自由テキストを注入する」役割に純化する(発信者の識別は不要になる)。
影響範囲:
protocol::Method::Notifyの定義変更、serde round-trip テストの更新- Controller main loop の
Method::Notify { source, message } => pod.push_notification(source, message)をpod.push_notification(message)形へ変更 Pod::push_notification(source, message)のシグネチャからsourceを落とす(呼び出し元を追って整理)- 既存テスト(
controller_test.rsの Notify ケース、pod_comm_tools_test.rsなど)の入力を新シグネチャに揃える
ScopeRule / Permission の移動
ScopeRule と Permission は wire 型として PodEvent::ScopeSubDelegated で protocol を経由する必要があるため、現状の manifest crate から protocol crate へ移動する。manifest は protocol::ScopeRule を re-export するか、単に protocol::ScopeRule を直接参照する形に切り替える。移動により protocol → manifest の逆依存が発生しないようにする。
子(発信側)
子の Controller が以下のタイミングで親 socket に一発接続して Method::PodEvent を送信し、即切断する(fire-and-forget)。
| タイミング | variant |
|---|---|
RunEnd { result: Finished } 発行時 |
TurnEnded |
| Worker 実行エラー発生時(後述) | Errored |
| shutdown シーケンス(controller loop 終了直前) | ShutDown |
SpawnPod tool 成功直後 |
ScopeSubDelegated |
Errored の対象は run_with_cancel_support 内で Worker 実行が失敗した Event::Error に限定する。AlreadyRunning / NotPaused / NotRunning のような method 拒否応答はクライアント向けの一時的なフィードバックであり、親に通知すべき子のライフサイクルイベントではない。実装時は発火点を worker エラー経路に絞ること。
送信は非同期 spawn で発射し、await しない。接続失敗はログのみで続行(親が落ちていても子は生きる)。
親 socket のアドレスは spawn 時に --callback <PATH> で受け取って保持している(既存)。
又貸しの親連鎖
ScopeSubDelegated は直接の親にのみ送る。曾孫が現れた場合:
- 孫 (C) が曾孫 (D) を
SpawnPodする - C は自分の親 (B) に
ScopeSubDelegated { parent_pod: C, sub_pod: D, ... }を送る - B は D を自分の
spawned_pods.jsonに追加し、さらに B の親 (A) へScopeSubDelegated { parent_pod: B, sub_pod: D, ... }を再発射する
これを再帰的に繰り返すことで、scope.lock の delegated_from チェーンと一致した形で root まで D の存在が登録される。各 Pod は「自分の直接の子+紹介された孫(= 更に下の Pod も含む)」を把握し、孫より下の階層は子経由で管理される。
再発射は ScopeSubDelegated 受信時の system 処理の一部として行う(後述の表に反映)。
親(受信側)
親 Controller の main loop に Method::PodEvent ハンドラを追加:
Method::PodEvent(event) => {
// (1) system 処理 — variant ごとに固有
apply_event_side_effects(&event, &spawned_registry, &lock_path).await;
// (2) LLM 通知 — variant を文字列にレンダリングしてバッファへ
let text = render_event(&event);
pod.push_notification(text);
}
variant 別の (1) の中身:
| variant | system 処理 |
|---|---|
TurnEnded |
なし |
Errored |
なし(LLM に判断させる) |
ShutDown |
spawned_registry.remove(pod_name)、scope lock を flock して該当 allocation を release_pod で解放 |
ScopeSubDelegated |
spawned_registry.add(SpawnedPodRecord { sub_pod, sub_socket, scope, ... }) で孫を追加。続けて自分の親(いれば)へ ScopeSubDelegated { parent_pod: self, sub_pod, ... } を再発射(親連鎖参照) |
(2) の render_event は一箇所に集約し、format!("Pod {pod_name} finished a turn.") のような短い human-readable 文字列を返す。
Controller の配線変更
現状 PodController::spawn 内で SpawnedPodRegistry は tool 登録のためだけに生成され、main loop の method_rx 処理ループには渡っていない。Method::PodEvent ハンドラを追加するには:
spawned_registry: Arc<SpawnedPodRegistry>を main loop にcloneで持ち込むscope_lock::default_lock_path()を event 処理時に呼ぶ(毎回 open するので保持は不要)- 親 callback socket(自分の親、トップ Pod では
None)をPod::from_manifest_spawned経由で既に保持している値から再利用(再発射で使う)
apply_event_side_effects は Controller の main loop から呼ぶ非同期関数として定義する。
失敗時のフォールバック
- 子 → 親の PodEvent 送信が失敗しても諦める(再試行しない)
- 親が再起動した場合や送信漏れた場合は、親の
ListPodsツール(既存)による health check +scope_lock::reclaim_staleの stale 回収で不整合を解消する - これは「コールバックは最適化、ポーリングが真のフォールバック」という方針の継続
決定事項
- 順序保証は求めず、ハンドラを冪等・遅延到着に強くする: fire-and-forget の unix socket 接続は順序を保証しない。
TurnEnded直後にShutDownが届いても、逆順で到着しても親側で成立するようにapply_event_side_effectsを設計する。具体的には:ShutDown受信時、すでに registry から削除済みでもエラーにしない(release_podのUnknownPodを swallow する既存挙動を踏襲)TurnEndedがShutDownより後に届いても、該当 Pod が既に registry にいなければ render だけして終える(LLM 向け通知は出る、system 処理は no-op)ScopeSubDelegatedで孫が既に registry にいたら上書きせず no-op(DuplicatePodNameを swallow)
ScopeSubDelegatedの親連鎖は直接の親のみ + 再発射: 上記「又貸しの親連鎖」セクション参照。曾孫以上は再帰的に再発射で root まで届く- 送信の接続タイムアウト:
SpawnPod/ pod-comm-tools と揃えて 5 秒固定
完了条件
Method::PodEvent(PodEvent)がprotocolcrate に追加され、serde round-trip テストが通る- 子の Controller が 4 種の variant を適切なタイミングで親 socket に送信する。
Erroredは Worker 実行エラーにのみ限定されることを確認 - 親の Controller が variant ごとの system 処理を実行し、レンダリングした文字列を LLM 通知バッファに流す
ScopeSubDelegated受信後、孫 Pod が親のspawned_pods.jsonに現れ、親が更に上位親を持つ場合は上位へ再発射されるShutDown受信後、該当 Pod が親の registry から消え、scope lock からも解放される- イベント到着順が入れ替わっても副作用が安全(冪等)であることを単体テストで確認
- 送信失敗しても子プロセスが続行する
- 各 variant の送受信を検証する単体テスト
範囲外
- リモート親への送信(SSH 越し)。ローカル Unix socket のみ
- 配信保証(at-least-once / exactly-once)
- 親再起動時の「見逃したイベント」の再送。ポーリングで補う前提