ticket: イベントプロトコルと永続化におけるシステムイベントの統合

This commit is contained in:
Keisuke Hirata 2026-05-14 04:12:40 +09:00
parent 63e27b2dee
commit 65a5e68035
No known key found for this signature in database
2 changed files with 81 additions and 0 deletions

View File

@ -8,6 +8,7 @@
- Pod: 任意ターンからの Fork複数ターン巻き戻しを汎用化 → [tickets/pod-session-fork.md](tickets/pod-session-fork.md) - Pod: 任意ターンからの Fork複数ターン巻き戻しを汎用化 → [tickets/pod-session-fork.md](tickets/pod-session-fork.md)
- Pod: 子→親の TurnEnded/Errored callback を親由来ターンのみに絞る → [tickets/pod-parent-turn-callback.md](tickets/pod-parent-turn-callback.md) - Pod: 子→親の TurnEnded/Errored callback を親由来ターンのみに絞る → [tickets/pod-parent-turn-callback.md](tickets/pod-parent-turn-callback.md)
- Pod: セッションログをバックエンドにした Pod 単位の永続化 → [tickets/pod-persistent-state.md](tickets/pod-persistent-state.md) - Pod: セッションログをバックエンドにした Pod 単位の永続化 → [tickets/pod-persistent-state.md](tickets/pod-persistent-state.md)
- Pod: System 注入経路 (Notify / PodEvent / HookInjectedItems) を SystemItem 一本に統合 → [tickets/system-item-unify.md](tickets/system-item-unify.md)
- 永続化層のセマンティック整理 → [tickets/persistence-semantics.md](tickets/persistence-semantics.md) - 永続化層のセマンティック整理 → [tickets/persistence-semantics.md](tickets/persistence-semantics.md)
- Exchange / Turn / Call セマンティクス整理 → [tickets/exchange-turn-call-semantics.md](tickets/exchange-turn-call-semantics.md) - Exchange / Turn / Call セマンティクス整理 → [tickets/exchange-turn-call-semantics.md](tickets/exchange-turn-call-semantics.md)
- llm-worker のエラー耐性 - llm-worker のエラー耐性

View File

@ -0,0 +1,80 @@
# Event / LogEntry: System 注入経路を SystemItem 一本に統合する
## 背景
エージェントシステム (= ユーザー由来でも LLM 由来でもない、Pod 自身) が LLM context に注入する `role:system``Item::Message` は、現状 3 系統の ad-hoc 経路で並走している:
1. **`Method::Notify`** — 外部からの非同期メッセージ
- controller → `Event::Notify { message }` (生 message echo)
- `pod.push_notify(message)``NotifyBuffer``pending_history_appends``[Notification] <msg>` の system_message として history に commit
2. **`Method::PodEvent`** — 子 pod のライフサイクル通知
- controller → `Event::PodEvent(event)` (typed echo)
- `render_event` で 1 行整形 → `NotifyBuffer` (Notify と合流) → 同じく `[Notification] <rendered>` として commit
3. **Interceptor 内部注入**`@<path>` / `#<slug>` / `/<slug>` の解決結果
- `PodInterceptor::on_prompt_submit``ContinueWith``[File: <path>]` / `[Knowledge: <slug>]` / workflow 本文の system_message を history に append
- wire echo は無し
これらは全部 「**人でも LLM でもなく、エージェントシステムが LLM に与えた情報**」 という同一カテゴリで、history への commit 形 (`role:system` の `Item::Message`) もほぼ同じだが、wire event 側は echo/typed/未送信が混在し、TUI 側のブロックも `Block::Notify` / `Block::PodEvent` / `Block::SystemMessage` の 3 つに分かれている。
加えて `LogEntry::HookInjectedItems` という命名が誤称: 実際に注入しているのは公開 `Hook` ではなく **`Interceptor`** で、内部機構専用の経路。`hook.rs` モジュール doc でも 「Hook は read-only な公開 extension surface」 「内部機構は Interceptor を使う」 と明確に分離されている。
このばらつきの結果:
- wire 上、同じ通知が `Event::Notify` (生) + `Event::HookInjectedItems` (整形版) の 2 重に流れて TUI が重複描画した (`pod-state-from-session-log` 改修中に表面化)
- kind 判別がテキストプレフィックス (`[Notification] ...` / `[File: ...]`) 頼みで脆い
- 新しい注入種 (`<system-reminder>` 等) を足すたびに 1 系統増える設計圧力
- `Method::Notify` の "Notify" 語感が view-only な `Alerter` (本来 "Notification" 寄り) とぶつかっている
LLM は `role:system` を生成しないため、worker.history 中の `role:system` 項目は構造的にすべてこのエージェント注入経路に由来する。この性質を型として表に出す。
## 方針
`Tool` パターンに倣って 「**1 つの concept + kind ベース dispatch**」 に統合する。
- wire event は 1 種類: `Event::SystemItem { kind, payload }` (1 件ずつライブ配信)
- LogEntry は kind 揃いで batch する単一バリアントに置き換え、`Hook` 命名を捨てる: `LogEntry::SystemItems { ts, items: Vec<SystemItem> }`
- Pod 内部の注入路 (NotifyBuffer / `format_notify` / `render_event` / Interceptor.ContinueWith) は **全部「kind 付き `SystemItem` を作って worker.history に commit」 という単一形式に合流**
- TUI は kind 別に Block を出し分け (現 `ToolCallBlock` がツール別に見た目を出すのと同じ構造)
単数/複数の使い分けは既存パターンに揃える:
- 1 件単位の wire event は `Event::SystemItem` (`Event::TextDelta` と同じ呼吸)
- 永続バッチは `LogEntry::SystemItems``Vec<SystemItem>` を内包 (`LogEntry::AssistantItems` / `ToolResults` と同じ呼吸)
`Method::Notify` / `Method::PodEvent` は外部 API としてはそのまま残す (入口の意味付けは別)。 中で `SystemItem::Notification` / `SystemItem::PodEvent` に変換されて以後は単一経路、という整理。
`Event::Alert` (= LLM context に乗らない純 UI 通知) は **別経路として明確に残す**。 view-only な persistent stream (Alerter の subscribe_with_snapshot) としてすでに正しく機能している。 "Notification" 語感の衝突は、本チケットで context 注入側を `SystemItem` に rename することで解消する (Notification は `SystemItem` の一 kind に格下げ、`Alerter` が "Notification" 語感の本来のオーナーに戻る)。
## 要件
- wire event は 1 種類: `Event::SystemItem { kind, payload }` で全注入が乗る。 `Event::Notify` / `Event::PodEvent` / `Event::HookInjectedItems` は protocol から削除
- LogEntry は `HookInjectedItems` を rename + items を kind 付き typed shape に置換。 新名 `LogEntry::SystemItems { ts, items: Vec<SystemItem> }` で wire tag は `system_items`
- `SystemItem` の kind 列挙は最低限以下を含む:
- `Notification { message }` (`Method::Notify` 由来)
- `PodEvent { event: PodEvent }` (子 pod ライフサイクル)
- `FileAttachment { path, content }` (`@<path>` 解決)
- `Knowledge { slug, body }` (`#<slug>` 解決)
- `Workflow { slug, body }` (`/<slug>` 解決)
- 将来追加可能 (`Reminder` 等) を見越した拡張点
- Pod 側の `NotifyBuffer` / `format_notify` / `render_event` / `Interceptor::on_prompt_submit ContinueWith``SystemItem` を中間表現として通る。 worker.history への append は最終的に `Item::system_message` + 対応する `SystemItem` 1 件を `LogEntry::SystemItems` として commit
- TUI は `Event::SystemItem` を kind で dispatch して描画する。 既存 `Block::Notify` / `Block::PodEvent` / `Block::SystemMessage``Block::SystemItem(SystemItemBlock)` に集約 (or 既存 Block を再利用しつつ駆動イベントだけ統一)
- `Method::Notify` / `Method::PodEvent` (外部入口 API) は名前を維持し、内部で `SystemItem::Notification` / `SystemItem::PodEvent` に変換される
- `Event::Alert` / `Alerter` は無変更
## 完了条件
- `Event::Notify` / `Event::PodEvent` / `Event::HookInjectedItems` が protocol から削除されている
- `LogEntry::HookInjectedItems` が削除され、`LogEntry::SystemItems` に置き換わっている (旧 wire tag を deserialize alias で残すかは実装判断)
- TUI が `Event::SystemItem` 駆動で system 系ブロックを構築している。 ライブ通知の二重描画が起きない
- `Method::Notify``Method::PodEvent` は外部 API としては変わらず動く
- `Event::Alert` / `Alerter` 経路は無変更
## 範囲外
- `Method::Notify` / `Method::PodEvent` の rename (入口名の整理は別の話)
- `Event::Alert` / `Alerter` 系の変更
- 旧 session log (`hook_injected_items` を含む) のファイル変換: deserialize alias で読めるところまでで、ファイル書き換えは行わない
- TUI 内の `Block::SystemItem` 詳細な視覚設計
## 関連
- 前提となる `tickets/pod-state-from-session-log.md` (state 正本を session log に統合) の後続。 同チケット内で `Event::HookInjectedItems` を導入したが、 直後に「Hook 命名は誤り」「Notify/PodEvent と二重」と判明したため本チケットで整理する
- CLAUDE.md の 「context に乗せる前に history に commit する」 加工原則に整合する整理 (現実装の経路を統一形にするだけで、原則自体は変わらない)