StreamMux:複数モードストリームのイベントディスパッチャ
役割
LangGraph v3 のストリーミングプロトコルでは、ユーザーが graph.stream_events(version="v3") を呼んで取得するのは生の (namespace, mode, payload) タプルの羅列ではなく、GraphRunStream オブジェクトです。その上に run.values / run.messages / run.updates / run.subgraphs / run.lifecycle といった「投影 (projection)」がぶら下がります (GraphRunStream:31-49)。これらの生イベントをモードごとに異なる投影に振り分け、ユーザーが普通の for ループで引っ張れるようにするのが StreamMux、イベントディスパッチセンターです。コードは libs/langgraph/langgraph/stream/_mux.py にあります (StreamMux:26)。
StreamMux 自身はどのモードの具体的論理も実装しません。3 つのことだけを行います:transformer の登録、イベントの push、クローズ。push 時は登録順に各 StreamTransformer.process(event) を呼び (push:269-296)、transformer がイベントを自身の projection channel に詰めるかどうかを決めます。その後 mux 自身がイベントを _events という主ログに追加します。主ログは生ストリームの「完全な監査副本」です。transformer が開いた projection はすべて StreamChannel インスタンスで、名前付き channel は mux によって自動的に主ログに転送されます。したがって transformer の副作用は自身の channel にも現れ、主ログにも残ります。
StreamMux はサブグラフのネストの基盤でもあります。subgraph が発見されるたび、SubgraphTransformer は mux._make_child(scope) (_make_child:193-225) を呼んでサブグラフ用に mini-mux を建てます。この mini-mux は親 mux と同じ factory 群を共有し(transformer インスタンスは scope ごとに 1 つ)、親 mux のポンプバインディングも共有します。ルートポンプが動くと、すべてのサブグラフのイベントが 1 格子前に進みます。
設計動機
- caller-driven ポンプ、バックグラウンドスレッドなし:
GraphRunStreamのイテレータがポンプです (_pump_next:107-130)。ユーザーがfor event in run.valuesすると、各イテレートが_pump_nextを発火し、生イベントを 1 つ引いて mux に与えます。バックグラウンドスレッドを開かず、メモリは消費速率で決まります。bind_pumpはこのポンプ関数を mux に登(bind_pump:162-180)し、mini-mux のサブグラフは_make_childで自動的に継承します。 - transformer は登録順に直列:
pushはfor transformer in self._transformers: if not transformer.process(event): keep = False(push loop:288-292)。前の transformer の副作用が先に起き、後の transformer がそれを見えます。ある transformer がFalseを返しても主ログへの追記を抑制するだけで、前の transformer はすでにイベントを受け取っています。 - native vs extension 投影の区別:
_native = Trueの transformer 投影は mux によってrun.valuesのような直接属性としてGraphRunStreamにぶら下げられます (native attrs:78-79)。非 native はrun.extensionsにしか入りません。native_keys集合 (native_keys:109) は v3 プロトコルが SDK に晒す「直接属性」ホワイトリストです。 before_builtinsがコンテンツ書き換え transformer に道を譲る:PII フィルタやコンテンツ審査のような transformer はMessagesTransformerなどの組込み transformer より前に生の text フィールドを見る必要があります (before_builtins:94-109)。mux は登録時にbefore_builtins = Trueで 2 レーンに再構成し (partition:131-148)、レーン内の順序は保ったままです。- seq はルート mux だけが割り当て:
_assign_seqはデフォルト True で、child mux は_make_childでFalseを渡されます (_assign_seq:215-220)。これによりサブグラフイベントがルートログに直接転送されるとき envelope が改変されず、seqがルートの書き込み順に単調に対応します。
主要ファイル
StreamMux 类:26-46— 中心スケジューラのプロパティと docstring。StreamMux.__init__:48-148— transformers list または factories を受け取り、before_builtinsでレーン分けして登録します。_make_child:193-225— サブグラフ発見時に mini-mux を建て、ポンプバインディングを継承します。_register:227-267—transformer.init()を呼んで projection dict を取り、key 衝突を検査し、名前付き StreamChannel をバインドします。push:269-296— 同期パス:順序よくprocessを呼び、主ログに入れるか決めます。apush:351-378— 非同期パス:aprocessを直列に await し、後続 transformer が前の async 結果を見えるようにします。close:298-324— 同期クローズ。各 transformer のfinalizeを呼び、エラーでも全 channel を閉じることを保証します。aclose:380-404— 非同期クローズ。schedule された task をすべてgatherしてからafinalizeします。_collect_stream_modes:398-414— mux に登録された transformer のrequired_stream_modesから、グラフに要求するモードを逆推します。_pregel_stream_v3:3533-3558— v3 同期エントリ。StreamMux + GraphRunStream をインスタンス化し、mux の stream_mode を下層stream()に逆流します。
データフロー
StreamMux.push の核心はこれだけです。登録順に transformer を走らせ、全部通ってまだ生きていれば主ログに詰めます:
def push(self, event: ProtocolEvent) -> None:
keep = True
for transformer in self._transformers:
if not transformer.process(event):
keep = False
if keep:
if self._assign_seq:
self._seq += 1
event["seq"] = self._seq
self._events.push(event)(push body:288-296) 質素に見えますが、transformer.process の中で自身の StreamChannel に push することがあり、名前付き channel は mux によって自動的に主ログに wire されます (_register:227-267)。したがって 1 つの生 values イベントが ValuesTransformer で run.values に詰められると同時に、ユーザー定義 transformer で変換されて主ログに戻される、ということが起きます。イベントは transformer 間を直列リンクで流れ、順序は厳密に保たれます。
境界と失敗
- async transformer は sync モードで即座に raise:
_registerはtransformer_requires_async検査を呼び (requires_async check:234-239)、transformer がaprocessをオーバーライドしているのにsupports_sync=Trueを設定していない場合、sync mux への登録はエラーになります。stream()ではなくastream()を使うよう促します。 - projection key 衝突は即
ValueError:_registerはset(projection) & set(self.extensions)を検査し (conflict check:246-256)、衝突する key と帰属する transformer 名を列挙します。カスタム transformer の projection key は組込みと同名にできません。 _make_childは pre-built transformers を拒絶:mux がtransformers=パラメータで構築されたときは子 mux を建てられません (factory required:209-214)。pre-built インスタンスは新しい scope 下で再初期化できないためです。SubgraphTransformerはfactories=で構築された mux 上でしか走れません。- finalize のエラーは他のクリーンアップをブロックしない:
closeはfirst_errorで最初の例外を取り、残りの transformer と channel を閉じ続け、最後に raise し (close error handling:312-324)、リソース漏れを防ぎます。 bind_pumpは channel 読み取り前に呼ぶ必要あり:GraphRunStream.__init__のwire_pump=Trueはデフォルトで_wire_request_moreを走らせ (_wire_request_more:80-92)、子 mux は_make_childで親のバインディングが自動的に伝わります。しかしユーザーが自分で mux インスタンスを組み立てるとき、pump の bind 忘れはStreamChannel.__iter__の_request_moreが永遠にNoneになり (_request_more:191-192)、イテレートが即座に終了します。schedule(coro)は async モードでしか使えない:StreamTransformer.scheduleはイベントループを必要とし (schedule:233-263)、mux はaclose時に schedule された task をすべてgatherし、async が走り終わってからafinalizeします。sync モードで schedule を呼ぶと失敗します。
まとめ
StreamMux は v3 ストリーミングプロトコルの中枢です。モード論理は一切実装せず、順序よく transformer をスケジュールし、scope ごとに子 mux を建て、生イベントと transformer 投影を主ログに収録するだけです。各モードがどうイベントをユーザーフレンドリーな形状に変えるかは stream/transformers を、生イベントが PregelLoop からどう流れ込むかは Pregel エンジン を参照してください。
公式資料:LangGraph 文档 · README