Command:resume / goto / update の 3 in 1 プリミティブ
役割
Command は LangGraph の「グラフ横断制御プリミティブ」で、libs/langgraph/langgraph/types.py で定義されています(Command class:759)。1 つの Command インスタンスは 3 つの意味を同時に運べます——resume (中断からの実行再開)、goto (次ステップでどのノードに飛ぶか)、update (現在のグラフの状態チャネルへの値書き込み)。ノード関数が Command(...) を返すか、呼び出し側が graph.invoke(Command(...)) の入力として使い、Pregel がこれを pending writes に分解してチャネルに書き込みます。
位置としてはノードと Pregel のメインループの間にあります。ノードが Command を産出し、Pregel は map_command(map_command:56) でそのフィールドを (task_id, channel, value) の 3 要組に分解して pending writes に書き込みます。次ラウンドの prepare_next_tasks がこれらの writes を自然に見て、スケジュールします——goto は Send になって TASKS チャネルに書き込まれ、update は対応チャネルに直接書き込まれ、resume は RESUME チャネル経由で interrupt() に渡されます。
設計動機
- 「副作用」の統一表現:ノードは状態に値を書くだけでなく、「非デフォルトの次ノードに飛ぶ」「親グラフにメッセージを伝える」「中断を回復する」こともできます。以前はこれらが分散した API でしたが、
Commandが単一の戻り値型にパッケージします。ノード関数シグネチャ-> Command[Literal["node_a", "node_b"]]でコンパイル時に有効な遷移先を検証できます。 Command.PARENTでグラフ階層を横断:サブグラフ (subgraph) のノードがreturn Command(graph=Command.PARENT, update=...)で更新を親グラフの状態に書けます——map_commandはgraph == "__parent__"を見ると (PARENT guard:58)raise InvalidUpdateError("There is no parent graph")します。実際に親グラフに届くのは親グラフの_firstでサブグラフからの報告 writes を処理する時です。gotoはSendを受け取る:Command(goto=Send("node", arg))はファンアウト (fan-out) の PUSH タスクと等価です。goto="node"はgoto=Send("node", START)と等価です (そのノードの PULL を発火)。そのためCommandは conditional edges の能力をノード戻り値に内包し、別途add_conditional_edgesを書く必要がありません。resumeの単一エントリ:Command(resume=...)はinterrupt()の唯一の回復パスです——単一値Command(resume="answer")で唯一の pending interrupt に自動ルーティングするほか、dictCommand(resume={interrupt_id: value, ...})で複数 interrupt の並行回復もサポートします。updateは reducer と協調:Command(update={"messages": [msg]})はチャネルの reducer を経由します——messagesチャネルがoperator.addなら、この update は上書きではなく append になります。Overwriteでラップすると reducer を回避でき (Overwrite:938)、BinaryOperatorAggregate チャネルの直書きパスを使います。
主要ファイル
Command class:759—@dataclass class Command(graph, update, resume, goto)。ジェネリックNは goto のノード名型を表します。Command fields:780—graph/update/resume/gotoフィールド定義と docstring。_update_as_tuples:791—updateを[(channel, value), ...]に分解し、dict、list[tuple]、annotated dataclass の 3 形式をサポートします。map_command:56—Commandを pending writes シーケンスに分解するコア関数。PARENT guard:58—cmd.graph == Command.PARENTの時に「親グラフがない」エラーを送出します。goto as sends:62—cmd.gotoをSendリストに分解し、SendはTASKSチャネルに、strはbranch:to:<node>に書いて START を発火させます。resume write:72—cmd.resumeをRESUMEチャネルに書き込み、interrupt()が再入時に読み取ります。update writes:74—cmd.updateを(channel, value)に分解して対応チャネルに書き込み、reducer を経由します。Command resume mapping:904— 親グラフがCommand(resume=...)を受け取った際、resume を対応する pending interrupt の入り口にルーティングします。Overwrite:938—Overwrite(value=...)ラッパー。BinaryOperatorAggregateチャネルと組み合わせて reducer をバイパスし直書きします。
データフロー
map_command が Command 解析のコアです(map_command:56):
def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is no parent graph")
if cmd.goto:
if isinstance(cmd.goto, (tuple, list)):
sends = cmd.goto
else:
sends = [cmd.goto]
for send in sends:
if isinstance(send, Send):
yield (NULL_TASK_ID, TASKS, send)
elif isinstance(send, str):
yield (NULL_TASK_ID, f"branch:to:{send}", START)
else:
raise TypeError(
f"In Command.goto, expected Send/str, got {type(send).__name__}"
)
if cmd.resume is not None:
yield (NULL_TASK_ID, RESUME, cmd.resume)
if cmd.update:
for k, v in cmd._update_as_tuples():
yield (NULL_TASK_ID, k, v)task_id はすべて NULL_TASK_ID です——これらの writes はどの具象タスクにも属さず、グラフレベルの「外発」です。goto の Send は TASKS チャネルに入り (次ラウンドの prepare_next_tasks が PUSH タスクとしてスケジュール)、goto の str は branch:to:<node> チャネルを経由し (条件辺専用チャネル、START を書くと「そのノードの入口を発火」を意味);resume は RESUME チャネルに入り、ノード再入時に interrupt() が消費;update の各 (k, v) は対応する状態チャネルに入り、reducer でマージされます。
Command が graph.stream(...) の入力として使われる場合、PregelLoop._first(Command resume mapping:904) がまずこれを pending writes に変換し、task_id ごとにグループ化して checkpoint_pending_writes に書き込み、prepare_next_tasks が自然に処理します。Command がノード戻り値の場合、attach_node がノードの writers に _get_updates mapper を取り付け (attach_node:1431)、Command._update_as_tuples() から update を抽出して状態チャネルに書き込み、goto と resume は _control_branch mapper で branch:to: チャネルと RESUME にルーティングします。
境界と失敗
Command.PARENTはルートグラフでエラー:map_commandが直接raise InvalidUpdateError("There is no parent graph")します(PARENT guard:58)——Command.PARENTはサブグラフのノードでのみ意味を持ち、ルートグラフでの呼び出しは即座にエラーになります。gotoの型制限:gotoはSend/strまたはそれらのシーケンスのみを受け付け、他の型はraise TypeErrorします(goto type:69)——整数や dict を直接渡すことはできません。- resume で id 指定がない場合は単一 pending を強制:
Command(resume="single")は複数の pending interrupt があるとraise RuntimeErrorします(multi interrupt error:917)。Command(resume={id: value, ...})に切り替える必要があります。 updateは reducer を経由し直書きできない:BinaryOperatorAggregateチャネル (Annotated[list, operator.add]など) はデフォルトで reducer を経由し、複数 update をマージします。reducer をバイパスするにはOverwrite(value=...)を使います (Overwrite:938)。ただし同一スーパーステップで同一チャネルに複数回OverwriteするとInvalidUpdateErrorになります。Commandとentrypoint.finalは排他的:@entrypoint関数がentrypoint.final(value=..., save=...)を返す場合、内部はEND/PREVIOUSチャネルを使います。Commandを返す場合は制御チャネルを使います——両者は同時に使えず、entrypoint 関数はCommandかfinalのどちらかを返し、混在させません。- 空
Commandはエラー:Command()の 3 フィールドがすべてNone/()の場合、map_commandは何も yield せず、_firstが writes が空であることを検出してraise EmptyInputError("Received empty Command input")します(empty command:928)。
まとめ
Command は resume / goto / update の 3 in 1 プリミティブで、map_command でチャネル writes に分解され、次ラウンドの prepare_next_tasks が自然に処理します。conditional edges、interrupt resume、状態更新を 1 つの戻り値に統合します。対応する interrupt() は /interrupt/interrupt、Send のファンアウトセマンティクスは /subgraph/send-command を参照してください。公式資料:LangGraph ドキュメント · README