fix: SpawnPodの起動経路の問題・を修正
This commit is contained in:
parent
00755cf1b8
commit
fa84d48c62
|
|
@ -1,3 +1,5 @@
|
||||||
[memory]
|
[memory]
|
||||||
extract_threshold = 10000
|
extract_threshold = 20000
|
||||||
|
|
||||||
consolidation_threshold_files = 10
|
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)
|
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||||
- [ ] Pod CLI: マニフェスト関連フラグの整理 → [tickets/pod-cli-manifest-flags.md](tickets/pod-cli-manifest-flags.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)
|
- [ ] 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 オーケストレーション
|
- [ ] Pod オーケストレーション
|
||||||
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
|
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
|
||||||
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.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,
|
form: &mut Form,
|
||||||
overlay_toml: &str,
|
overlay_toml: &str,
|
||||||
) -> Result<SpawnReady, SpawnError> {
|
) -> 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 cwd = std::env::current_dir().map_err(SpawnError::Io)?;
|
||||||
|
|
||||||
let mut command = Command::new(&pod_bin);
|
let mut command = Command::new(&pod_bin);
|
||||||
command
|
command
|
||||||
|
.args(&pod_args)
|
||||||
.arg("--overlay")
|
.arg("--overlay")
|
||||||
.arg(overlay_toml)
|
.arg(overlay_toml)
|
||||||
.current_dir(&cwd)
|
.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")
|
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 let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND") {
|
||||||
if !cmd.is_empty() {
|
let mut tokens = cmd.split_whitespace();
|
||||||
return PathBuf::from(cmd);
|
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 Ok(exe) = std::env::current_exe() {
|
||||||
if let Some(dir) = exe.parent() {
|
if let Some(dir) = exe.parent() {
|
||||||
let candidate = dir.join("pod");
|
let candidate = dir.join("pod");
|
||||||
if candidate.is_file() {
|
if candidate.is_file() {
|
||||||
return candidate;
|
return (candidate, Vec::new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PathBuf::from("pod")
|
(PathBuf::from("pod"), Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StderrTail {
|
struct StderrTail {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ pkgs.mkShell {
|
||||||
pkg-config
|
pkg-config
|
||||||
openssl
|
openssl
|
||||||
];
|
];
|
||||||
|
INSOMNIA_POD_COMMAND = "cargo run -p pod --quiet --";
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
echo "dev-shell-loaded"
|
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