9.3 KiB
Movement 制御フローツールと park シグナル
エンジン内部リファレンス。「ツールを呼ぶと movement が待機/終了状態に遷移するが、その遷移先はピース毎の rules に書かなくてよい」という仕組みのまとめ。BrowseWeb のログイン待ち・RequestTool の承認待ち・(提案中の)WaitSubTask が同じ骨格を共有する。今後この型を流用する時の下敷き。
要点
movement の遷移には 2 系統ある。
| 系統 | 遷移先の決まり方 | LLM への提示 | 例 |
|---|---|---|---|
| rules ベース | ピースの rules[].next を buildTransitionTool が enum 化 |
transition ツールの選択肢として提示 |
別 movement への中間遷移 |
sentinel next(park/terminal) |
ツールまたは engine が MovementResult.next に特別な値を直接セット |
専用ツール(complete 等)として提示。rules 不要 |
終了・各種待機 |
後者が今回の主題。ツールが「作業をする」のではなく「movement を畳む制御シグナルを出す」。complete がまさにこれで、BrowseWeb(ログイン待ち)・RequestTool(承認待ち)も同じ。rules に依存しないので、rules: [] の単一 movement ピース(chat 等)でも一律に効く。
仕組みの流れ
agent-loop.ts
LLM が制御フローツールを呼ぶ
└─ finishMovement({ next: '<SENTINEL>', resumeMovement?, waitReason? }) を返す
│ MovementResult.next に sentinel を載せる
▼
piece-runner.ts mapMovementResult(result, ...)
└─ next の値で分岐し PieceRunResult を返す
├─ 終了系 → kind:'done' / status: completed|aborted|cancelled
└─ 待機系 → kind:'done' / status: waiting_* + resumeMovement(+waitReason)
▼
worker.ts
└─ waiting_* のジョブを停車。条件成立で resumeMovement から再開
(waiting_subtasks=子全完了 / waiting_human=ユーザー回答・ログイン・承認)
ポイントは、待機系も「ジョブを停車してワーカーを解放」すること。ツール内で同期ブロックして待つのではない(後述の鉄則)。
sentinel カタログ(mapMovementResult の分岐・origin/main 時点)
next |
生成元 | 結果 status | resumeMovement | waitReason | 種別 |
|---|---|---|---|---|---|
COMPLETE |
complete ツール |
completed |
— | — | 終了 |
ABORT(cancelled 含む) |
キャンセル経路 | cancelled |
— | — | 終了 |
ABORT |
complete/各種ガード(text_only_limit・max_iterations 等) |
aborted |
— | — | 終了 |
ASK |
complete({status:'needs_user_input'}) |
waiting_human(上限超は停車せず、同じ movement へ戻して自己判断を促す。差し戻しが MAX_ASK_LIMIT_DEFLECTIONS 回に達したら ask_limit_reached で打ち切り。#1299) |
現在の movement | — | 待機 |
WAIT_SUBTASKS |
transition(rules 経由)/WaitSubTask ツール(提案) |
waiting_subtasks |
default_next(ツール版は現在の movement に変更予定) |
— | 待機 |
WAITING_HUMAN_BROWSER |
BrowseWeb(ログイン要求時) |
waiting_human |
現在の movement | browser_login |
待機 |
WAITING_HUMAN_BROWSER |
RequestCaptchaHelp(検索が CAPTCHA で全滅したとき・#1199 段階 2) |
waiting_human |
現在の movement | captcha |
待機 |
WAITING_HUMAN_TOOL_REQUEST |
RequestTool |
waiting_human |
現在の movement | tool_request |
待機 |
WAITING_HUMAN_SPAWN_REQUEST |
SpinOffTask(別ワークスペース指定時) |
waiting_human |
現在の movement | spawn_request |
待機 |
null |
異常(ツール未決着) | error |
— | — | 異常 |
WAITING_HUMAN_BROWSERは待ち理由違いで 2 つの入口を共有する例。人が解く相手が 「そのタスクのブラウザ」か「ワークスペース共有の確認画面」かだけが違い、下流の分岐 (piece-runner)は同じものを通る。再開の口は別で、captcha 側はPOST /captcha-pool/resolveが同じ画面で止まっている全ジョブをまとめて queued に 戻す(resumeCaptchaJobsForSpace)。どの画面を待っているかはjobs.captcha_space_idに残す——jobs.space_idは個人ワークスペースだと NULL のままなので代用できない。
WAIT_SUBTASKSだけは現状rules[].nextにも書ける(rules ベース経由)。ツール版(WaitSubTask)を足すと、**同じ sentinel に「ルール不要の入口」**が増える。下流のmapMovementResult分岐は共有。
新しい park ツールを足すレシピ
- ツール定義を該当モジュール(例
orchestration.ts)のTOOL_DEFSに追加。 - agent-loop で制御フローツールとして識別し、
finishMovement({ next: '<新SENTINEL>', resumeMovement, waitReason })を返す(WAITING_HUMAN_BROWSERの実装が雛形)。 mapMovementResultに分岐を追加(piece-runner.ts)。終了ならstatus: 'aborted'|'completed'、待機ならstatus: 'waiting_*'+resumeMovement(+waitReason)。- worker の再開条件を実装/再利用。既存の
waiting_human/waiting_subtasksのどちらかに寄せられるなら新 status は不要。 - 型(
MovementResult.nextのユニオン、waitReason)を更新。
既存の待機 status(waiting_human / waiting_subtasks)に相乗りできるなら、3〜4 は分岐追加だけで済むことが多い。
鉄則・gotcha
- ツール内で同期ブロックして待たない。 待機は必ず「停車=ワーカー解放」で表現する。in-tool で
awaitし続けると親ワーカーを占有し、最悪ワーカー枯渇でデッドロックする(WAIT_SUBTASKSが遷移=停車である理由)。 - resume 先を意識する。
default_nextがCOMPLETEのピース(単一 movement)で再開させると終了に飛ぶ。多くの待機ツールは現在の movement に再入させて、再開後に結果を読んでcompleteさせる。 - terminal と park を混同しない。
COMPLETE/ABORTはジョブを終わらせる(再開なし)。WAITING_*/WAIT_SUBTASKSは停車→再開。resumeMovementの有無が分かれ目。 - 停車から戻ったら同じ呼び出しが来る前提で書く。 再開は movement を頭からやり直すので、LLM は同じ引数で同じツールをもう一度呼ぶ。決着済みの申請を掴んで短絡する経路(
RequestPackageのdeclinedPackages/installedPackages、SpinOffTaskのfindDecidedSpawnRequestForJob)が無いと、却下で永久ループ・承認で 2 枚目のカードになる。 - sentinel は
rulesの enum に出さない。 これらは engine-internal。rules[].nextに書けるのは別 movement 名とWAIT_SUBTASKSのみ(loadPiece/lint がCOMPLETE/ABORT/ASKを reject する)。
KV キャッシュと tools 安定性(2026-07-22 実測・変更禁止事項)
transition ツールは movement 毎ではなく piece 全体で 1 つ(enum = collectPieceTransitionTargets による全 movement の遷移先の和集合、description は静的)。一見「その movement の rules だけを enum に出す」方が正しく見えるが、戻してはいけない。
根拠は llama-server (llama.cpp) への統制プローブと実タスク計測(return_progress の prompt_cache ログ、PR #931)で確定した以下の実測法則:
- 追記のみのリクエスト → ほぼ全キャッシュ再利用(評価は追記分のみ)
- 末尾付近の書き換え → 分岐点以降のみ再評価(部分再利用は健在)
- 共通接頭辞が全体の半分未満 → 全損(
slot_prompt_similarity既定 0.5 の門前払い) - tools 配列の変更 → 位置・大小問わず全損(description 1 文字でも)
- 描画順は system 本文 → tools → 会話(Qwen3 テンプレート)
movement 毎に transition ツールを作り替えると法則 4 に直撃し、遷移のたびに会話全体 (~20k トークン / ~12 秒) を再評価する(実測: 遷移直後の再利用率 0%、tools 安定なら 99%+)。movement 単位の遷移制約は runtime 検証(processTransitionCalls → validateTransition、無効なら有効選択肢つきエラーを tool result で返す)と、movement guidance の選択肢一覧(会話への追記なのでキャッシュ無害)が引き続き完全に担保している。
この構造を変更する場合は、開発サーバーで movement 遷移を跨ぐタスクを流し、[llm-stream] ... prompt_cache reused=/pct= で遷移直後の再利用率が 99% 台を維持することを必ず実測確認すること。
関連
- 設計:
docs/design/2026-06-26-subtask-wait-and-safety.html(WaitSubTask ツール=この型の新規適用例) - 実装:
src/engine/agent-loop.ts(finishMovement/ 制御フローツール識別)、src/engine/piece-runner.ts(mapMovementResult)、src/worker.ts(待機ジョブの停車・再開)