88 lines
4.5 KiB
Markdown
88 lines
4.5 KiB
Markdown
# llm-worker: ストリーム途中失敗時の継続
|
||
|
||
## 背景
|
||
|
||
LLM 応答の SSE ストリームを読んでいる途中で upstream が切れると、
|
||
`crates/llm-worker/src/llm_client/transport.rs:231` で
|
||
`ClientError::Sse(...)`(中身は `eventsource_stream::Error::Transport`、
|
||
さらに reqwest の `error decoding response body`)として上に投げられ、
|
||
`worker.rs:933 stream_response` が `WorkerError` に変換して 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 test` が `llm-worker` で通る。
|
||
|
||
## 範囲外
|
||
|
||
- pre-stream の transient リトライ → `llm-worker-transient-retry`
|
||
- ストリーム resume API の実装(プロバイダ側に存在しないので不可能)
|
||
- 課金額の自動上限制御
|