8.9 KiB
8.9 KiB
Review: Invoke / Turn / LlmCall セマンティクス整理
レビュー対象実装: d0dbac1 feat: Invoke marker と LlmCall callback を導入し AgentTurn セマンティクスを明確化 (worktree invoke-turn-llmcall-semantics)。
前提・要件の確認
決定事項に明記されたスコープ B (コード反映) を順に対応付ける。
Event::InvokeStart { kind: InvokeKind }/Event::LlmCallStart/Event::LlmCallEnd追加:crates/protocol/src/lib.rs:248-296。serde tag は既存のevent名前空間に乗り、新 variant のみで既存 variant は無傷。round-trip テストevent_invoke_start_roundtrip/event_llm_call_start_end_roundtrip(crates/protocol/src/lib.rs:751-805) で 5 種すべての kind と llm_call カウンタを担保。InvokeKindenum:crates/protocol/src/lib.rs:521-540。UserSend/Notify/PodEvent/SystemReminder/Wakeupの 5 variants がsnake_caseで wire 化されている (チケット決定通り)。配置もprotocolcrate (session-storeが import する向き) で正しい。Worker.llm_call_count+on_llm_call_start/on_llm_call_endcallback:crates/llm-worker/src/worker.rs:166-175,:343-355,:1050-1065。- 増分位置は
stream_response直前後で 1 ペアずつ発火 → callback の対称性が保たれる (interceptor の Cancel/Yield 早期 return では fire しないため pair-balance OK)。 Mutable → Locked/Locked → Mutableの遷移時に新 field をすべて移送している (:1241-1252,:1503-1517,:1589-1603) — state machine の整合性は維持。
- 増分位置は
Worker::turn_countの doc 更新:crates/llm-worker/src/worker.rs:153-160,:498-503。AgentTurn 数として再定義 + 「today retry 未実装で LlmCall と 1:1」明記。実装上の増分点は:1070でllm_call_count += 1と同じ場所を共有する形で、retry が入ったら増分点を分離する旨が doc で示されている。LogEntry::Invoke { ts, trigger }追加 + 開始時 commit:crates/session-store/src/session_log.rs:121-136。field 名は trigger(serde tagkindとの衝突回避) の判断は doc コメントに明記されている (:130-131)。replay_invoke_marker_does_not_mutate_state(:706-735) で「replay は marker のみで state 不変」が担保。pod 側 commit はcrates/pod/src/pod.rs:1147-1153(run) と:1508-1515(run_for_notification)。順序はprepare_for_run → Invoke marker → UserInput / SystemItemで、ticket 「直後の Turn entry に payload を書く」に整合。- controller の wiring + broadcast:
crates/pod/src/controller.rs:306-313でon_llm_call_start/endをevent_txに橋渡し。PendingRun::RunForNotification(InvokeKind)(:97-103,:565-579) でMethod::Notify(kind=Notify) とMethod::PodEvent(kind=PodEvent) からそれぞれ正しい kind が伝搬している (:660,:730)。crates/pod/src/ipc/server.rs:118-120でLogEntry::Invoke → Event::InvokeStart変換、crates/pod/src/session_log_sink.rs:124で live broadcast filter に追加 — 新 client の snapshot 復元時には mirror 経由、live 接続中の client には broadcast 経由で同じ event が届く既存パターンを正しく踏襲。
完了条件のうち文書化系 (定義・retry 境界・SystemTurn 範囲・案 A 採択・persistence-semantics 整合) は元チケット本体に明記されており、コード側のコメント (session_log.rs:121-136, protocol/src/lib.rs:248-296, worker.rs:153-175) でも繰り返し引用されている。
アーキテクチャ・スコープ
- 層の境界: 新 callback は
Workerの純然たる observable として置かれ、Pod / controller 側で broadcast に橋渡しする既存パターンと一致。llm-workerは低レベル基盤に留めるという memory の方針 (feedback_llm_worker_scope) を尊重している。 - Provider policy: 触らない。
- Cargo.toml: 依存追加なし —
cargo add規則は無関係。 - 互換性: protocol の既存 variant は無変更、新 variant のみ追加。range 外と明示された
Hook::OnTurnEndrename /LogEntry::TurnEndcommit 位置移動 / retry 本実装 / TUI UI 設計のいずれにも踏み込んでいない。 run_for_notification(kind)の API 拡張: 引数追加は最小だが、debug_assert!でUserSendを弾く (crates/pod/src/pod.rs:1496-1505) —UserSendはpod.run()専用という呼び出し規約を実行時に強制している。型レベル分離 (例:NotifyInvokeKindnewtype) より軽量で、本チケット範囲では妥当な選択。PendingRun::RunForNotification(InvokeKind)はタプル variant に最小限 1 引数だけ持たせており、is_parent_originated()の判定にも影響しない (controller.rs:115)。Notify と PodEvent の kind 違いを controller 入口で確定させる方針は読み手にとっても自然。- Resume を Invoke 対象から除外:
Method::Resumeは IDLE → active ではなく Paused → active なので Invoke marker を打たない、という判断は明示されていないが、ticket の「IDLE → active 遷移時に追記」と一貫しており、判定として正しい。doc に注記があると親切だが blocking ではない。
指摘事項
Blocking
(なし)
Non-blocking / Follow-up
Event::UserMessageの doc コメントが実際の発火順序と食い違う:crates/protocol/src/lib.rs:212-214の "Fires exactly once per acceptedMethod::Run, afterInvokeStart { kind: UserSend }and before the firstTurnStart" は、実装上 controller がMethod::Run受信直後 (controller.rs:636-638) にevent_tx.send(UserMessage)を fire し、InvokeStart(entry broadcast 由来) はpod.run()内のprepare_for_run → commit_entry(Invoke)を経て後発で出る。同じ socket 上では IPC handler のselect!で multiplex されるため client から見た順序保証は弱いが、実態としてUserMessage → InvokeStartの順で届く。doc 側を実態に合わせる (順序は逆) か、controller 側をevent_tx.send(UserMessage)を Invoke commit の後に動かすかのどちらか。前者で十分 (TUI は今回 Invoke を no-op で受けるため UI 影響なし)。Resume経路に Invoke を打たない判断の doc 化:pod.resume()が IDLE → active ではない (Paused → active) ため Invoke marker 不要、をpod.rsのresume()doc またはLogEntry::Invokedoc に一行注記すると後続の persistence/fork 実装者が迷わない。- End-to-end の Invoke commit を担保するテストが薄い: protocol/session-store の round-trip + replay 不変性は単体テストで担保されているが、
pod.run()を実走させて session log mirror にLogEntry::Invokeがちゃんと並ぶ統合テストはない (controller_test.rs:history_from_sinkは Invoke を_ => {}で無視している)。pod-session-fork/at_turn_index整合の後続チケットで初めて触れる前に、ここで一段階アサーションを入れておく価値はある (今回の範囲外なら別チケットで OK)。
Nits
crates/pod/src/pod.rs:1147-1153と:1508-1515でself.session_id = self.session_head.lock().session_id;を Invoke 直前にも書いているが、この再代入はcommit_entry自体がsession_head.lock()からsession_idを取り直すため動作には不要 (元コードと同じく単なる bookkeeping)。意図的なら問題なしだが、pod.rs:1149のコメントには触れられていない点、Invoke commit を前置したことで「session_id同期が必要なタイミングが2回ある」ように読めるので、prepare_for_run内に1回まとめて寄せるリファクタも検討価値あり (本チケット範囲外)。crates/llm-worker/tests/worker_state_test.rs:355-366は AgentTurn:LlmCall 1:1 の現状を担保するが、tool_use を含む単一run()で複数 LlmCall が立つケース (= 同 run 内でllm_call_countが複数増える) のテストはない。retry 実装後に「retry が turn_count を増やさない」アサーションを追加するため、ベースとして tool_use 連鎖テストを近いうちに足しておくとリグレッション検出力が上がる。
判断
Approve — スコープ B の決定事項はすべて反映されており、protocol の既存互換も保たれ、Worker / Pod / IPC / TUI の各層に対する変更は最小かつ既存パターンを踏襲している。UserMessage doc の順序記述が実態と逆な点は文言修正で済む follow-up であり、機能面・型安全性・ビルド整合に blocking はない。cargo build --workspace および cargo test --workspace がいずれもクリーンに通ることを確認済み。