fix: SpawnPodの起動経路の問題・を修正
This commit is contained in:
parent
8e24b3c607
commit
5479b14411
|
|
@ -1,3 +1,5 @@
|
|||
[memory]
|
||||
extract_threshold = 10000
|
||||
extract_threshold = 20000
|
||||
|
||||
consolidation_threshold_files = 10
|
||||
# consolidation_threshold_bytes = 0
|
||||
|
|
|
|||
3
TODO.md
3
TODO.md
|
|
@ -7,6 +7,9 @@
|
|||
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||
- [ ] Pod CLI: マニフェスト関連フラグの整理 → [tickets/pod-cli-manifest-flags.md](tickets/pod-cli-manifest-flags.md)
|
||||
- [ ] OpenAI Responses: sampling パラメータの取り扱い → [tickets/responses-sampling-params.md](tickets/responses-sampling-params.md)
|
||||
- [ ] llm-worker のエラー耐性
|
||||
- [ ] HTTP transient リトライ → [tickets/llm-worker-transient-retry.md](tickets/llm-worker-transient-retry.md)
|
||||
- [ ] ストリーム途中失敗時の継続 → [tickets/llm-worker-stream-continuation.md](tickets/llm-worker-stream-continuation.md)
|
||||
- [ ] Pod オーケストレーション
|
||||
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
|
||||
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
|
||||
|
|
|
|||
|
|
@ -268,11 +268,12 @@ async fn wait_for_ready(
|
|||
form: &mut Form,
|
||||
overlay_toml: &str,
|
||||
) -> Result<SpawnReady, SpawnError> {
|
||||
let pod_bin = resolve_pod_command();
|
||||
let (pod_bin, pod_args) = resolve_pod_command();
|
||||
let cwd = std::env::current_dir().map_err(SpawnError::Io)?;
|
||||
|
||||
let mut command = Command::new(&pod_bin);
|
||||
command
|
||||
.args(&pod_args)
|
||||
.arg("--overlay")
|
||||
.arg(overlay_toml)
|
||||
.current_dir(&cwd)
|
||||
|
|
@ -374,21 +375,28 @@ fn build_overlay_toml(form: &Form) -> String {
|
|||
toml::to_string(&toml::Value::Table(root)).expect("overlay serialisation cannot fail")
|
||||
}
|
||||
|
||||
fn resolve_pod_command() -> PathBuf {
|
||||
/// Resolves the program (and any leading args) used to launch a child Pod.
|
||||
///
|
||||
/// `INSOMNIA_POD_COMMAND` is split on whitespace so devshells can point it
|
||||
/// at e.g. `cargo run -p pod --quiet --`; the first token is the program
|
||||
/// and the rest are prepended before `--overlay` and friends.
|
||||
fn resolve_pod_command() -> (PathBuf, Vec<String>) {
|
||||
if let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND") {
|
||||
if !cmd.is_empty() {
|
||||
return PathBuf::from(cmd);
|
||||
let mut tokens = cmd.split_whitespace();
|
||||
if let Some(program) = tokens.next() {
|
||||
let args = tokens.map(str::to_owned).collect();
|
||||
return (PathBuf::from(program), args);
|
||||
}
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let candidate = dir.join("pod");
|
||||
if candidate.is_file() {
|
||||
return candidate;
|
||||
return (candidate, Vec::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from("pod")
|
||||
(PathBuf::from("pod"), Vec::new())
|
||||
}
|
||||
|
||||
struct StderrTail {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ pkgs.mkShell {
|
|||
pkg-config
|
||||
openssl
|
||||
];
|
||||
INSOMNIA_POD_COMMAND = "cargo run -p pod --quiet --";
|
||||
shellHook = ''
|
||||
echo "dev-shell-loaded"
|
||||
'';
|
||||
|
|
|
|||
87
tickets/llm-worker-stream-continuation.md
Normal file
87
tickets/llm-worker-stream-continuation.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# 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 の実装(プロバイダ側に存在しないので不可能)
|
||||
- 課金額の自動上限制御
|
||||
70
tickets/llm-worker-transient-retry.md
Normal file
70
tickets/llm-worker-transient-retry.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# llm-worker: HTTP transient リトライ
|
||||
|
||||
## 背景
|
||||
|
||||
`crates/llm-worker/src/llm_client/transport.rs` はリトライを持たず、
|
||||
upstream が一時的に不調だったときのエラーがそのまま `WorkerError` に
|
||||
伝播して Run が中断する。実セッションでも以下が観測されている:
|
||||
|
||||
- セッション `019de419-6f8b-71a0-9e56-f0b9a6c7098b.jsonl:85` で
|
||||
`API error (status: 503): upstream connect error ... Connection refused`。
|
||||
これは `transport.rs:194-216` で `response.status().is_success()` 前に
|
||||
返される pre-stream の経路。リクエストはまだ消費されていない。
|
||||
- Anthropic の `overloaded_error` (529) や Codex backend の 503 も
|
||||
同経路で観測される transient な事象。
|
||||
|
||||
これらは「ヘッダが返る前の段階」で出るため、SSE を読み始めて
|
||||
出力トークンを発生させる前であり、素朴な再送でべき等に復旧できる典型ケース。
|
||||
ストリームが途中で切れた場合のリカバリは別の話(→ `llm-worker-stream-continuation`)。
|
||||
|
||||
## 方針
|
||||
|
||||
`transport.rs` の HTTP 送信層に transient エラー向けの再送を追加する。
|
||||
SSE 読み出し開始後 (`response.bytes_stream()` 以降) のエラーは対象外で、
|
||||
従来どおり `ClientError::Sse` として上に流す。
|
||||
|
||||
scheme(OpenAI / Anthropic / Responses 等)に依存しない共通処理として、
|
||||
すべての client から同じ振る舞いで使える形にする。
|
||||
|
||||
## 要件
|
||||
|
||||
### リトライ対象
|
||||
|
||||
- HTTP ステータス: 408 / 425 / 429 / 500 / 502 / 503 / 504 / 529
|
||||
- `reqwest::Error::is_connect()` / `is_timeout()` 由来の送信失敗
|
||||
- それ以外の `ClientError::Api { status }` および `ClientError::Json`、
|
||||
ストリーム開始後の `ClientError::Sse` は対象外
|
||||
|
||||
判定は `is_retryable(&ClientError) -> bool` を `error.rs` に置いて一箇所に集約する。
|
||||
|
||||
### バックオフ
|
||||
|
||||
- フルジッタ付き指数(base/cap は実装時に妥当な値で固定。後で manifest 化したくなったら別ticket)
|
||||
- `Retry-After` ヘッダがあれば指数バックオフを上書きしてその時間待つ
|
||||
- 上限: 最大試行回数 + 累積タイムアウトの両方を持つ
|
||||
|
||||
### ログ
|
||||
|
||||
- リトライ発火ごとに `warn!`(ステータス、attempt 番号、次の wait)
|
||||
|
||||
### 既存挙動の温存
|
||||
|
||||
- ストリーム途中で切れた場合の挙動には手を入れない
|
||||
(`transport.rs:231` の `ClientError::Sse` 経路はそのまま)
|
||||
- 成功時のレイテンシに観測可能なオーバヘッドを足さない
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `is_retryable` のテーブル駆動 unit test
|
||||
- 503 / 529 / connect refused をモックした unit test が、
|
||||
規定回数までリトライして「最終的に成功」「上限到達でエラー」の両ケースを通る
|
||||
- `Retry-After: 5` を返すモックでは指数を上書きして 5s 待っている
|
||||
(仮想時間で検証)
|
||||
- mid-stream で `ClientError::Sse` を起こすモックでリトライが発火しない
|
||||
- `cargo check` / `cargo test` が `llm-worker` で通る
|
||||
|
||||
## 範囲外
|
||||
|
||||
- mid-stream(SSE 読み中)の継続再開 → `llm-worker-stream-continuation`
|
||||
- プロバイダ別の細かい retry policy(共通既定で十分)
|
||||
- リトライ上限値の manifest からの上書き(必要になったら別ticket)
|
||||
Loading…
Reference in New Issue
Block a user