yoi/tickets/llm-worker-stream-continuation.md

4.5 KiB
Raw Blame History

llm-worker: ストリーム途中失敗時の継続

背景

LLM 応答の SSE ストリームを読んでいる途中で upstream が切れると、 crates/llm-worker/src/llm_client/transport.rs:231ClientError::Sse(...)(中身は eventsource_stream::Error::Transport、 さらに reqwest の error decoding response body)として上に投げられ、 worker.rs:933 stream_responseWorkerError に変換して Run 全体が中断する。

実例: セッション 019de419-6f8b-71a0-9e56-f0b9a6c7098b.jsonl:236。 直前まで text / tool_use を Timeline に積み続けていたが、 最後の SSE フレームが届く前に接続が切れ、Run が落ちた。

ここで重要な点:

  • 出力トークンは upstream 側で既に発生しており、課金済み。 単純に同じ request を再送すると二重出力 + 二重課金。
  • Anthropic / OpenAI のいずれの API も、途切れた SSE を resume するエンドポイントは持たない。継続したい場合は 「assistant turn として部分出力を history に置いた状態で再リクエスト」 という形を自作する必要がある。
  • 部分出力の質は内容依存:
    • 完成した text ブロックは原則そのまま history に置ける
    • tool_use の input_json が途中で切れたブロックは破損 JSON で、そのままは置けない
    • reasoning / thinking ブロックも provider 依存の扱いが要る

このため、これは「リトライ」ではなく「継続 (continuation)」。 history を編集する責務であり、transport.rs には収まらず、 worker.rs 層(または上位)の機能になる。 feedback_llm_worker_scope.md の方針llm-worker は低レベル基盤に留める)にも合致する。

なお worker.rs:973 付近で部分 flush_usage() だけは既に行っており、 半分くらいは継続を意識した作りになっている。あとは 「壊れていないブロックの確定」と「次ターンの起動条件」を足す形。

詰めたい論点(実装前に決める)

このチケットは仕様議論を含む。以下を確定させてから実装に入る。

  1. 部分ブロックの取り扱い基準

    • 完成した text ブロックは確定して history に push するか
    • 未完の tool_use は捨てるのが妥当か、暫定 stop で残すか
    • reasoning / thinking ブロックの扱いprovider 別)
  2. 継続の起動方式

    • 自動的に次ターンを回す(= retry-like 挙動)/ Pause で上位に判断委譲 / manifest で切替、のいずれか
    • デフォルトは何か(自動継続は意図しない出費を生む可能性あり)
  3. ループ防止

    • 同種の transport エラーが連続 N 回起きたら諦める閾値
    • 「ストリーム開始後ほぼ即座に切れる」が連続するパターンの検知
  4. 他の中断要因との優先度

    • Cancelled / Aborted / interceptor の Yield が同時に起きたときの順序
  5. 可観測性

    • 「途中で切れて継続した」事実をセッションログに残す形
    • ClientError::Sse(String)Sse { kind: Parse | Transport, msg } に 分割するかどうか(診断容易性のため)

要件(暫定。論点確定後に再記述)

  • ストリーム途中で transport 由来のエラーが出た場合、 worker.rs がそれを catch し、Timeline に積まれた完成ブロックだけを assistant items として確定する。
  • 未完ブロック(特に壊れた tool_useは破棄するか、 破棄したことを示す形で履歴に残す(決定は論点 1
  • 継続するか中断するかの判定が、論点 2 の決定に従って分岐する。
  • 連続失敗時に止まる(論点 3
  • 既存の Cancelled / Aborted パスが優先される(論点 4

完了条件

  • 上記論点が決まり、ドキュメント or チケット本文に反映されている。
  • ストリーム途中で切れるモックを使った integration test が、 決まった仕様どおりに継続 or 中断する。
  • 課金重複が起きないこと(自動継続でも、過去ターンの再生成は発生しない)が test または手動手順で確認されている。
  • cargo check / cargo testllm-worker で通る。

範囲外

  • pre-stream の transient リトライ → llm-worker-transient-retry
  • ストリーム resume API の実装(プロバイダ側に存在しないので不可能)
  • 課金額の自動上限制御