PregelRunner:スーパーステップ内のタスク実行器
役割
PregelRunner は一つのことだけします: PregelLoop.tick が準備した PregelExecutableTask のバッチを本当に走らせ、それらの writes を集めて PregelLoop に戻す。グラフの形や checkpoint、チャネルがどう reduce されるかには関与しません——「並列実行 + 失敗処理 + writes 落盤」の三つだけを担います。
位置は非常に狭いです: Pregel.astream のメインループ(sync main loop:2964-2984)で、while loop.tick() が True を返した後、直後に for _ in runner.tick([t for t in loop.tasks.values() if not t.writes], ...): が続き、走り終わったら loop.after_tick() が呼ばれます。つまり runner の入力は「tick で算出された、まだ writes を書いていない task」、出力は「これらの task の writes が checkpoint_pending_writes に入った状態」で、中間に第二層のバッファはありません。
PregelRunner 自身は非常に薄いです: 二つの公開メソッド tick(tick:176)と commit(commit:574)、加えて非同期版 atick。重仕事はすべて _call / _acall(_call:700)/(_acall:789)という二つの低層呼び出し器にあります——これらはノード関数をスケジュール可能な future にラップし、retry policy、子 task の Send fan-out、streaming chunk コールバックを処理し、最終的に writes を task オブジェクトに書き戻します。
設計動機
なぜ runner を普通のメソッドではなく generator にするのか?
- ストリーミング yield:runner は
for _ in runner.tick(...)という generator 形態です(yield return type:188)。一つの task を走らせ終わるごとに yield して制御権を上層に返します——上層はすぐに streaming chunk をクライアントへ送れます。スーパーステップ全体を待つ必要はありません。これが LangGraph のストリーミング (streaming) の底層基盤です。 - 単 task fast path:
len(tasks) == 1 and timeout is None and get_waiter is Noneのとき同期直接呼び出しでスレッドプールを立ち上げません(fast path:203-254)。大半のエージェント単分岐シナリオはこのパスに命中し、スレッドプールのオーバーヘッドを避けます。 commitを Future コールバックに:FuturesDict.on_done(on_done:116)は各 future 完成時に触発され、中身はweakref.WeakMethod(self.commit)——commitは per-task の writes 落盤フックで、batch 収尾ではありません。これにより writes がpending_writesに入る順序は自然と完成順になり、checkpoint も可能な限り早く書けます。- error handler ルーティング:
_should_route_to_error_handler(_should_route_to_error_handler:171-174)が失敗ノードにerror_handlerノードが設定されているか判定します。命中すれば exception id を_handled_exception_idsに追加し、handler task をスケジュールして元 task を置き換えます——失敗は即座に panic せず、別のノードで収尾します。 _should_stop_othersのキャンセル機構:いずれかの task が非GraphBubbleUp例外を投げると、runner は同バッチの他の in-flight task をキャンセルします(_should_stop_others:616-634)——同スーパーステップ内で「全部成功か handler に進むか」のセマンティクスを保証します。GraphInterruptは明示的に除外されます、中断は失敗ではないからです。
主要ファイル
class PregelRunner:135-138— クラス定義。docstring が一言で役割を示す: task を実行、commit writes、yield 制御、必要に応じて他 task を中断。__init__:140-169—submit/put_writesの weakref、node_error_handler_map、schedule_error_handler/aschedule_error_handlerコールバック、そして_handled_exception_idsという tick を跨ぐ例外重複除去集合を保持。FuturesDict:75-134— カスタム dict サブクラス。on_doneコールバックが各 future 完成時にcommitを触発;should_stopフィールドが_should_stop_otherspartial を保持。tick signature:176-188— 入力はIterable[PregelExecutableTask]、戻りはIterator[None]。signature が generator セマンティクスを露出。fast path:203-254— 単 task + timeout なし + waiter なしのとき直接同期実行。失敗時に必要に応じて error handler をスケジュール。schedule tasks:259-276— 複数 task パス: 各 task に一つの future を起動し、self.submit()(実体はPregelLoop.submit)でスケジュール。concurrent wait loop:282-323—concurrent.futures.wait(FIRST_COMPLETED)ループ。一つ完成するごとにcommit、出力 emit、必要に応じて handler task を起動。commit method:574-613— per-task 収尾: cancelled は ERROR 書き、GraphInterruptは INTERRUPT 書き、通常 exception はERROR+ERROR_SOURCE_NODE書き、正常時はtask.writes+NO_WRITESmarker を書く。_should_stop_others:616-634— 他 task をキャンセルするか判定。GraphBubbleUpと handled 済み例外を明示的に除外。_panic_or_proceed:650-697— 収尾関数。全 in-flight future をキャンセル、複数のGraphInterruptを一つにマージ、timeout 時はTimeoutErrorを投げる。_call:700-787— ノード呼び出しの低層ラップ: retry、stream chunk emit、Sendfan-out のschedule_taskコールバック、task.writes の蓄積。_should_route_to_error_handler:171-174—task.name in self.node_error_handler_mapのとき True を返す; error handler ノード自身はルーティングされない(再帰防止)。
データフロー
tick の冒頭で FuturesDict を構築します。これは強化版 dict で、各 future 完成時に自動で commit を呼びます:
def tick(
self,
tasks: Iterable[PregelExecutableTask],
*,
reraise: bool = True,
timeout: float | None = None,
retry_policy: Sequence[RetryPolicy] | None = None,
get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None,
schedule_task: Callable[
[PregelExecutableTask, int, Call | None],
PregelExecutableTask | None,
],
) -> Iterator[None]:
tasks = tuple(tasks)
futures = FuturesDict(
callback=weakref.WeakMethod(self.commit),
event=threading.Event(),
should_stop=partial(
_should_stop_others, handled_exception_ids=self._handled_exception_ids
),
future_type=concurrent.futures.Future,
)
# give control back to the caller
yieldこれは tick 頭部:176-199 からの引用です。最初の yield が即座に制御権を返すのに注目——上層の for _ in runner.tick(...) の最初の反復は「起動」だけで、本当の task 実行は以降の反復で起きます。これは generator コルーチンの簡易形態で、同期パスに async キーワードを持ち込まずに済ませます。
commit は per-task の収尾関数で、task の writes を pending_writes に落とします:
def commit(
self,
task: PregelExecutableTask,
exception: BaseException | None,
) -> None:
if isinstance(exception, asyncio.CancelledError):
task.writes.append((ERROR, exception))
self.put_writes()(task.id, task.writes)
elif exception:
if isinstance(exception, GraphInterrupt):
if exception.args[0]:
writes = [(INTERRUPT, exception.args[0])]
if resumes := [w for w in task.writes if w[0] == RESUME]:
writes.extend(resumes)
self.put_writes()(task.id, writes)
elif isinstance(exception, GraphBubbleUp):
pass
else:
task.writes.append((ERROR, exception))
if self._should_route_to_error_handler(task) and not isinstance(
exception, GraphBubbleUp
):
task.writes.append((ERROR_SOURCE_NODE, task.name))
self._handled_exception_ids.add(id(exception))
self.put_writes()(task.id, task.writes)
else:
if self.node_finished and (
task.config is None or TAG_HIDDEN not in task.config.get("tags", [])
):
self.node_finished(task.name)
if not task.writes:
task.writes.append((NO_WRITES, None))
self.put_writes()(task.id, task.writes)これは commit:574-613 からの引用です。put_writes は PregelLoop.put_writes の weakref(put_writes:415)で、writes を checkpoint_pending_writes に詰め込み checkpointer の future を起動して永続化します。いくつかの特殊 writes に注意:
CancelledError→(ERROR, exception)を書き、after_tick収尾時に task を「失敗だが記録済み」として扱い、panic しません。GraphInterrupt→(INTERRUPT, ...)+ 任意のRESUMEwrites を書く。中断の復帰値は interrupt と一緒に落盤し、次回 resume 時に一緒に読み出します。- 通常 exception + error handler 設定済み → 追加で
(ERROR_SOURCE_NODE, task.name)を書く。このマーカーはPregelLoop._resume_error_handlers_if_applicableで走査され(ERROR_SOURCE_NODE scan:775)、再開時に対応する handler ノードをスケジュールするのに使われます。 - 正常完了だが何も書いていない →
(NO_WRITES, None)を補う。prepare_next_tasksが「まだ走っていない」と誤認するのを防ぎます(NO_WRITES marker:609-611)。
runner 全体の実行フローはこのように描けます:
境界と失敗
_handled_exception_idsは tick を跨いで永続:__init__でこの集合は instance-level で、per-tick ではありません(_handled_exception_ids:169)。同じ例外オブジェクト id が集合に入ると、_should_stop_othersはもう失敗扱いしません。これにより error handler が走り終わった後に元 exception が再送されるのを防ぎます。GraphBubbleUpは失敗ではない:commitはGraphBubbleUpを明示的にpassし、writes を一切書きません(GraphBubbleUp pass:592-594)。この種の例外(ParentCommandなど)は親グラフへの信号で、真のエラーではありません。NO_WRITESで task の重複実行を防止:task が正常完了したが何も書かなかった場合も(NO_WRITES, None)を補います(NO_WRITES:609-611)。さもないとafter_tick後のprepare_next_tasksがこの task の writes がないのを見て「まだ走っていない」と誤認し、再開時にもう一度走らせてしまいます。schedule_error_handlerは None を返しうる:handler ノード自身が未設定または構築できないことがあり、その場合は task は通常の panic パスに進みます(handler optional:230-248)。commitのERROR_SOURCE_NODEwrite はこのパスと共存し、_resume_error_handlers_if_applicableが再開時に再度スケジュールを試みます。- timeout に当たっても GraphInterrupt を投げない:
_panic_or_proceedのif inflight: raise timeout_exc_cls("Timed out")(timeout:691-697)——timeout は真の例外で、Pregel から bubble up し、GraphInterrupt のように飲み込まれません。 - traceback の枝刈り:reraise の前に
EXCLUDED_FRAME_FNAMESリストのフレームをスキップし(tb trim:241-247)、langgraph 内部スタックフレームをフィルタし、ユーザが見る traceback が自身のノードコードを直接指すようにします。
まとめ
PregelRunner は薄い殻です: generator tick が並列実行をスケジュールし + per-task commit が writes を pending_writes に落とし、間に error handler ルーティングとキャンセル機構を挟みます。これを理解すれば「一つのスーパーステップで task がどう本当に走るか」を理解したことになります。上層 PregelLoop のループ駆動は /pregel/loop、prepare_next_tasks / apply_writes / should_interrupt の三アルゴリズム関数は /pregel/algo、全体組立は /pregel/pregel、runner の yield 間でストリーミング出力がどう加工されるかは /stream/run-stream を参照してください。
公式資料:LangGraph ドキュメント · README